Merge "[Overlay] Add a #drawFrame() method to processor" into androidx-main
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
index 692175f..5a08436 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AudioUnderrunQuery.kt
@@ -22,7 +22,7 @@
 internal object AudioUnderrunQuery {
     @Language("sql")
     private fun getFullQuery() = """
-        SELECT track.name, counter.value, counter.ts
+        SELECT counter.value, counter.ts
         FROM track
         JOIN counter ON track.id = counter.track_id
         WHERE track.type = 'process_counter_track' AND track.name LIKE 'nRdy%'
@@ -38,7 +38,6 @@
     ): SubMetrics {
         val queryResult = session.query(getFullQuery())
 
-        var trackName: String? = null
         var lastTs: Long? = null
         var totalNs: Long = 0
         var zeroNs: Long = 0
@@ -46,16 +45,9 @@
         queryResult
             .asSequence()
             .forEach { lineVals ->
-
                 if (lineVals.size != EXPECTED_COLUMN_COUNT)
                     throw IllegalStateException("query failed")
 
-                if (trackName == null) {
-                    trackName = lineVals[VAL_NAME] as String?
-                } else if (trackName!! != lineVals[VAL_NAME]) {
-                    throw RuntimeException("There could be only one AudioTrack per measure")
-                }
-
                 if (lastTs == null) {
                     lastTs = lineVals[VAL_TS] as Long
                 } else {
@@ -74,8 +66,7 @@
         return SubMetrics((totalNs / 1_000_000).toInt(), (zeroNs / 1_000_000).toInt())
     }
 
-    private const val VAL_NAME = "name"
     private const val VAL_VALUE = "value"
     private const val VAL_TS = "ts"
-    private const val EXPECTED_COLUMN_COUNT = 3
+    private const val EXPECTED_COLUMN_COUNT = 2
 }
diff --git a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
index 8229714..9a9791e 100644
--- a/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/main/java/androidx/benchmark/integration/macrobenchmark/AudioUnderrunBenchmark.kt
@@ -27,7 +27,6 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,7 +34,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalMetricApi::class)
-class AudioUnderrunBenchmark() {
+class AudioUnderrunBenchmark {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
@@ -48,7 +47,6 @@
     }
 
     @Test
-    @Ignore("b/297916125")
     fun start() {
         benchmarkRule.measureRepeated(
             packageName = PACKAGE_NAME,
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
index f27a1e5..8c6a2e1 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
@@ -34,7 +34,7 @@
 @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 class RobolectricAdvertiseTest {
     private val context: Context = RuntimeEnvironment.getApplication()
-    private var bluetoothLe = BluetoothLe.getInstance(context)
+    private var bluetoothLe = BluetoothLe(context)
 
     @Test
     fun advertiseSuccess() = runTest {
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
index ab35eda..32729d4 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
@@ -28,6 +28,7 @@
 import android.bluetooth.BluetoothGattService as FwkService
 import android.bluetooth.BluetoothManager
 import android.content.Context
+import android.os.Build
 import androidx.bluetooth.BluetoothDevice
 import androidx.bluetooth.BluetoothLe
 import androidx.bluetooth.GattClient
@@ -37,9 +38,10 @@
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runTest
-import org.junit.After
 import org.junit.Assert
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -99,16 +101,11 @@
 
     @Before
     fun setUp() {
-        bluetoothLe = BluetoothLe.getInstance(context)
+        bluetoothLe = BluetoothLe(context)
         clientAdapter = StubClientFrameworkAdapter(bluetoothLe.client.fwkAdapter)
         bluetoothLe.client.fwkAdapter = clientAdapter
     }
 
-    @After
-    fun tearDown() {
-        bluetoothLe.client.fwkAdapter = clientAdapter.baseAdapter
-    }
-
     @Test
     fun connectGatt() = runTest {
         val device = createDevice("00:11:22:33:44:55")
@@ -117,9 +114,9 @@
         acceptConnect()
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
+            Assert.assertEquals(sampleServices.size, services.size)
             sampleServices.forEachIndexed { index, service ->
-                Assert.assertEquals(service.uuid, getServices()[index].uuid)
+                Assert.assertEquals(service.uuid, services[index].uuid)
             }
             closed.complete(Unit)
         }
@@ -156,10 +153,10 @@
         }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
+            Assert.assertEquals(sampleServices.size, services.size)
             Assert.assertEquals(testValue,
                 readCharacteristic(
-                    getServices()[0].getCharacteristic(readCharUuid)!!
+                    services[0].getCharacteristic(readCharUuid)!!
                 ).getOrNull()?.toInt())
             closed.complete(Unit)
         }
@@ -178,10 +175,10 @@
             }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
+            Assert.assertEquals(sampleServices.size, services.size)
             assertTrue(
                 readCharacteristic(
-                    getServices()[0].getCharacteristic(noPropertyCharUuid)!!
+                    services[0].getCharacteristic(noPropertyCharUuid)!!
                 ).exceptionOrNull()
                 is IllegalArgumentException)
         }
@@ -223,8 +220,8 @@
         }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
-            val characteristic = getServices()[0].getCharacteristic(writeCharUuid)!!
+            Assert.assertEquals(sampleServices.size, services.size)
+            val characteristic = services[0].getCharacteristic(writeCharUuid)!!
 
             Assert.assertEquals(initialValue,
                 readCharacteristic(characteristic).getOrNull()?.toInt())
@@ -249,10 +246,10 @@
             }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
+            Assert.assertEquals(sampleServices.size, services.size)
             assertTrue(
                 writeCharacteristic(
-                    getServices()[0].getCharacteristic(readCharUuid)!!,
+                    services[0].getCharacteristic(readCharUuid)!!,
                     48.toByteArray()
                 ).exceptionOrNull()
                 is IllegalArgumentException)
@@ -297,8 +294,8 @@
         }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
-            val characteristic = getServices()[0].getCharacteristic(notifyCharUuid)!!
+            Assert.assertEquals(sampleServices.size, services.size)
+            val characteristic = services[0].getCharacteristic(notifyCharUuid)!!
 
             Assert.assertEquals(initialValue,
                 readCharacteristic(characteristic).getOrNull()?.toInt())
@@ -324,9 +321,9 @@
             }
 
         bluetoothLe.connectGatt(device) {
-            Assert.assertEquals(sampleServices.size, getServices().size)
+            Assert.assertEquals(sampleServices.size, services.size)
             subscribeToCharacteristic(
-                getServices()[0].getCharacteristic(readCharUuid)!!,
+                services[0].getCharacteristic(readCharUuid)!!,
             ).collect {
                 // Should not be notified
                 fail()
@@ -334,6 +331,41 @@
         }
     }
 
+    @Test
+    fun servicesFlow_emittedWhenServicesChange() = runTest {
+        val device = createDevice("00:11:22:33:44:55")
+
+        val newServiceUuid = UUID.randomUUID()
+        val newService = FwkService(newServiceUuid, FwkService.SERVICE_TYPE_PRIMARY)
+        val newServices = sampleServices + newService
+
+        acceptConnect()
+
+        clientAdapter.onDiscoverServicesListener =
+            StubClientFrameworkAdapter.OnDiscoverServicesListener {
+                if (clientAdapter.gattServices.isEmpty()) {
+                    clientAdapter.gattServices = sampleServices
+                }
+                clientAdapter.callback?.onServicesDiscovered(
+                    clientAdapter.bluetoothGatt,
+                    BluetoothGatt.GATT_SUCCESS
+                )
+            }
+
+        bluetoothLe.connectGatt(device) {
+            launch {
+                clientAdapter.gattServices = newServices
+                if (Build.VERSION.SDK_INT >= 31) {
+                    clientAdapter.callback?.onServiceChanged(clientAdapter.bluetoothGatt!!)
+                }
+            }
+            val servicesEmitted = servicesFlow.take(2).toList()
+            Assert.assertEquals(sampleServices.size, servicesEmitted[0].size)
+            Assert.assertEquals(sampleServices.size + 1, servicesEmitted[1].size)
+            Assert.assertEquals(newServiceUuid, servicesEmitted[1][sampleServices.size].uuid)
+        }
+    }
+
     private fun acceptConnect() {
         clientAdapter.onConnectListener =
             StubClientFrameworkAdapter.OnConnectListener { device, _ ->
@@ -372,7 +404,7 @@
     }
 
     class StubClientFrameworkAdapter(
-        internal val baseAdapter: GattClient.FrameworkAdapter
+        private val baseAdapter: GattClient.FrameworkAdapter
     ) : GattClient.FrameworkAdapter {
         var gattServices: List<FwkService> = listOf()
         var callback: BluetoothGattCallback? = null
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt
index 07e8583..741377c 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt
@@ -41,7 +41,6 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runTest
-import org.junit.After
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
@@ -91,16 +90,11 @@
 
     @Before
     fun setUp() {
-        bluetoothLe = BluetoothLe.getInstance(context)
+        bluetoothLe = BluetoothLe(context)
         serverAdapter = StubServerFrameworkAdapter(bluetoothLe.server.fwkAdapter)
         bluetoothLe.server.fwkAdapter = serverAdapter
     }
 
-    @After
-    fun tearDown() {
-        bluetoothLe.server.fwkAdapter = serverAdapter.baseAdapter
-    }
-
     @Test
     fun openGattServer() = runTest {
         val device = createDevice("00:11:22:33:44:55")
@@ -504,7 +498,7 @@
     }
 
     class StubServerFrameworkAdapter(
-        val baseAdapter: GattServer.FrameworkAdapter
+        private val baseAdapter: GattServer.FrameworkAdapter
     ) : GattServer.FrameworkAdapter {
         val shadowGattServer: ShadowBluetoothGattServer
             get() = shadowOf(gattServer)
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt
index a983408..189873a 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt
@@ -39,7 +39,7 @@
 @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 class RobolectricScanTest {
     private val context: Context = RuntimeEnvironment.getApplication()
-    private var bluetoothLe = BluetoothLe.getInstance(context)
+    private var bluetoothLe = BluetoothLe(context)
     private companion object {
         private const val TIMEOUT_MS: Long = 2_000
     }
diff --git a/bluetooth/bluetooth/api/current.txt b/bluetooth/bluetooth/api/current.txt
index 2618c72..65b4cb4 100644
--- a/bluetooth/bluetooth/api/current.txt
+++ b/bluetooth/bluetooth/api/current.txt
@@ -22,16 +22,12 @@
   }
 
   public final class AdvertiseResult {
-    ctor public AdvertiseResult();
     field public static final int ADVERTISE_FAILED_DATA_TOO_LARGE = 102; // 0x66
     field public static final int ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 103; // 0x67
     field public static final int ADVERTISE_FAILED_INTERNAL_ERROR = 104; // 0x68
     field public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 105; // 0x69
     field public static final int ADVERTISE_STARTED = 101; // 0x65
-    field public static final androidx.bluetooth.AdvertiseResult.Companion Companion;
-  }
-
-  public static final class AdvertiseResult.Companion {
+    field public static final androidx.bluetooth.AdvertiseResult INSTANCE;
   }
 
   public final class BluetoothAddress {
@@ -61,24 +57,22 @@
   }
 
   public final class BluetoothLe {
+    ctor public BluetoothLe(android.content.Context context);
     method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public suspend Object? advertise(androidx.bluetooth.AdvertiseParams advertiseParams, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>? block, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
-    method public static androidx.bluetooth.BluetoothLe getInstance(android.content.Context context);
     method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
     method @RequiresPermission("android.permission.BLUETOOTH_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<androidx.bluetooth.ScanFilter> filters);
-    field public static final androidx.bluetooth.BluetoothLe.Companion Companion;
-  }
-
-  public static final class BluetoothLe.Companion {
-    method public androidx.bluetooth.BluetoothLe getInstance(android.content.Context context);
   }
 
   public static interface BluetoothLe.GattClientScope {
     method public androidx.bluetooth.GattService? getService(java.util.UUID uuid);
-    method public java.util.List<androidx.bluetooth.GattService> getServices();
+    method public default java.util.List<androidx.bluetooth.GattService> getServices();
+    method public kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.bluetooth.GattService>> getServicesFlow();
     method public suspend Object? readCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, kotlin.coroutines.Continuation<? super kotlin.Result<? extends byte[]>>);
     method public kotlinx.coroutines.flow.Flow<byte[]> subscribeToCharacteristic(androidx.bluetooth.GattCharacteristic characteristic);
     method public suspend Object? writeCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, byte[] value, kotlin.coroutines.Continuation<? super kotlin.Result<? extends kotlin.Unit>>);
+    property public default java.util.List<androidx.bluetooth.GattService> services;
+    property public abstract kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.bluetooth.GattService>> servicesFlow;
   }
 
   public static final class BluetoothLe.GattServerConnectRequest {
diff --git a/bluetooth/bluetooth/api/restricted_current.txt b/bluetooth/bluetooth/api/restricted_current.txt
index 2618c72..65b4cb4 100644
--- a/bluetooth/bluetooth/api/restricted_current.txt
+++ b/bluetooth/bluetooth/api/restricted_current.txt
@@ -22,16 +22,12 @@
   }
 
   public final class AdvertiseResult {
-    ctor public AdvertiseResult();
     field public static final int ADVERTISE_FAILED_DATA_TOO_LARGE = 102; // 0x66
     field public static final int ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 103; // 0x67
     field public static final int ADVERTISE_FAILED_INTERNAL_ERROR = 104; // 0x68
     field public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 105; // 0x69
     field public static final int ADVERTISE_STARTED = 101; // 0x65
-    field public static final androidx.bluetooth.AdvertiseResult.Companion Companion;
-  }
-
-  public static final class AdvertiseResult.Companion {
+    field public static final androidx.bluetooth.AdvertiseResult INSTANCE;
   }
 
   public final class BluetoothAddress {
@@ -61,24 +57,22 @@
   }
 
   public final class BluetoothLe {
+    ctor public BluetoothLe(android.content.Context context);
     method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public suspend Object? advertise(androidx.bluetooth.AdvertiseParams advertiseParams, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>? block, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
-    method public static androidx.bluetooth.BluetoothLe getInstance(android.content.Context context);
     method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
     method @RequiresPermission("android.permission.BLUETOOTH_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<androidx.bluetooth.ScanFilter> filters);
-    field public static final androidx.bluetooth.BluetoothLe.Companion Companion;
-  }
-
-  public static final class BluetoothLe.Companion {
-    method public androidx.bluetooth.BluetoothLe getInstance(android.content.Context context);
   }
 
   public static interface BluetoothLe.GattClientScope {
     method public androidx.bluetooth.GattService? getService(java.util.UUID uuid);
-    method public java.util.List<androidx.bluetooth.GattService> getServices();
+    method public default java.util.List<androidx.bluetooth.GattService> getServices();
+    method public kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.bluetooth.GattService>> getServicesFlow();
     method public suspend Object? readCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, kotlin.coroutines.Continuation<? super kotlin.Result<? extends byte[]>>);
     method public kotlinx.coroutines.flow.Flow<byte[]> subscribeToCharacteristic(androidx.bluetooth.GattCharacteristic characteristic);
     method public suspend Object? writeCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, byte[] value, kotlin.coroutines.Continuation<? super kotlin.Result<? extends kotlin.Unit>>);
+    property public default java.util.List<androidx.bluetooth.GattService> services;
+    property public abstract kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.bluetooth.GattService>> servicesFlow;
   }
 
   public static final class BluetoothLe.GattServerConnectRequest {
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothAddressTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothAddressTest.kt
index beaa6f1..3c0e6e4 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothAddressTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothAddressTest.kt
@@ -18,6 +18,7 @@
 
 import junit.framework.TestCase.assertEquals
 import kotlin.test.assertFailsWith
+import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -66,10 +67,9 @@
     fun constructorWithInvalidAddressType() {
         val invalidAddressType = -1
 
-        val bluetoothAddress = BluetoothAddress(TEST_ADDRESS_UNKNOWN, invalidAddressType)
+        val result = runCatching { BluetoothAddress(TEST_ADDRESS_UNKNOWN, invalidAddressType) }
 
-        assertEquals(TEST_ADDRESS_UNKNOWN, bluetoothAddress.address)
-        assertEquals(BluetoothAddress.ADDRESS_TYPE_UNKNOWN, bluetoothAddress.addressType)
+        assertTrue(result.exceptionOrNull() is IllegalArgumentException)
     }
 
     @Test
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
index 8e1f074..822fd61 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
@@ -59,7 +59,7 @@
         context = ApplicationProvider.getApplicationContext()
         bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
         bluetoothAdapter = bluetoothManager.adapter
-        bluetoothLe = BluetoothLe.getInstance(context)
+        bluetoothLe = BluetoothLe(context)
 
         Assume.assumeNotNull(bluetoothAdapter)
         Assume.assumeTrue(bluetoothAdapter.isEnabled)
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseResult.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseResult.kt
index 69f8687..304bdd7 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseResult.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseResult.kt
@@ -24,7 +24,7 @@
  * An advertise result indicates the result of a request to start advertising, whether success
  * or failure.
  */
-class AdvertiseResult {
+object AdvertiseResult {
     @Target(AnnotationTarget.TYPE)
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @Retention(AnnotationRetention.SOURCE)
@@ -37,20 +37,18 @@
     )
     annotation class ResultType
 
-    companion object {
-        /** Advertise started successfully. */
-        const val ADVERTISE_STARTED: Int = 101
+    /** Advertise started successfully. */
+    const val ADVERTISE_STARTED: Int = 101
 
-        /** Advertise failed to start because the data is too large. */
-        const val ADVERTISE_FAILED_DATA_TOO_LARGE: Int = 102
+    /** Advertise failed to start because the data is too large. */
+    const val ADVERTISE_FAILED_DATA_TOO_LARGE: Int = 102
 
-        /** Advertise failed to start because the advertise feature is not supported. */
-        const val ADVERTISE_FAILED_FEATURE_UNSUPPORTED: Int = 103
+    /** Advertise failed to start because the advertise feature is not supported. */
+    const val ADVERTISE_FAILED_FEATURE_UNSUPPORTED: Int = 103
 
-        /** Advertise failed to start because of an internal error. */
-        const val ADVERTISE_FAILED_INTERNAL_ERROR: Int = 104
+    /** Advertise failed to start because of an internal error. */
+    const val ADVERTISE_FAILED_INTERNAL_ERROR: Int = 104
 
-        /** Advertise failed to start because of too many advertisers. */
-        const val ADVERTISE_FAILED_TOO_MANY_ADVERTISERS: Int = 105
-    }
+    /** Advertise failed to start because of too many advertisers. */
+    const val ADVERTISE_FAILED_TOO_MANY_ADVERTISERS: Int = 105
 }
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
index b3c7a70..ecbca45 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothAddress.kt
@@ -24,12 +24,10 @@
 /**
  * Represents a Bluetooth address for a remote device.
  *
- * @property address valid Bluetooth MAC address
- * @property addressType valid address type
- *
+ * @property address a valid Bluetooth MAC address
+ * @property addressType a valid address type
  */
-class BluetoothAddress(val address: String, @AddressType addressType: Int) {
-    @AddressType val addressType: Int
+class BluetoothAddress(val address: String, @AddressType val addressType: Int) {
     companion object {
         /** Address type is public and registered with the IEEE. */
         const val ADDRESS_TYPE_PUBLIC: Int = 0
@@ -62,12 +60,13 @@
             throw IllegalArgumentException("$address is not a valid Bluetooth address")
         }
 
-        this.addressType = when (addressType) {
+        when (addressType) {
             ADDRESS_TYPE_PUBLIC,
             ADDRESS_TYPE_RANDOM_STATIC,
             ADDRESS_TYPE_RANDOM_RESOLVABLE,
-            ADDRESS_TYPE_RANDOM_NON_RESOLVABLE -> addressType
-            else -> ADDRESS_TYPE_UNKNOWN
+            ADDRESS_TYPE_RANDOM_NON_RESOLVABLE,
+            ADDRESS_TYPE_UNKNOWN -> Unit
+            else -> throw IllegalArgumentException("$addressType is not a valid address type")
         }
     }
 
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
index 5b34edc..c4dcf2c 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
@@ -42,6 +42,7 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.job
 
@@ -49,20 +50,10 @@
  * Entry point for BLE related operations. This class provides a way to perform Bluetooth LE
  * operations such as scanning, advertising, and connection with a respective [BluetoothDevice].
  */
-class BluetoothLe private constructor(private val context: Context) {
+class BluetoothLe constructor(private val context: Context) {
 
-    companion object {
+    private companion object {
         private const val TAG = "BluetoothLe"
-        @Volatile
-        @JvmStatic
-        private var instance: BluetoothLe? = null
-
-        @Suppress("VisiblySynchronized")
-        @JvmStatic
-        fun getInstance(context: Context) =
-            instance ?: synchronized(this) {
-                instance ?: BluetoothLe(context.applicationContext).also { instance = it }
-            }
     }
 
     @RequiresApi(34)
@@ -226,9 +217,20 @@
     interface GattClientScope {
 
         /**
-         * Gets the services discovered from the remote device.
+         * A flow of GATT services discovered from the remote device.
+         *
+         * If the services of the remote device has changed, the new services will be
+         * discovered and emitted automatically.
          */
-        fun getServices(): List<GattService>
+        val servicesFlow: StateFlow<List<GattService>>
+
+        /**
+         * GATT services recently discovered from the remote device.
+         *
+         * Note that this can be changed, subscribe to [servicesFlow] to get notified
+         * of services changes.
+         */
+        val services: List<GattService> get() = servicesFlow.value
 
         /**
          * Gets the service of the remote device by UUID.
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
index d2a173b..01456ac 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
@@ -39,7 +39,10 @@
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filter
@@ -141,6 +144,7 @@
         val subscribeMap: MutableMap<FwkCharacteristic, SubscribeListener> = mutableMapOf()
         val subscribeMutex = Mutex()
         val attributeMap = AttributeMap()
+        val servicesFlow = MutableStateFlow<List<GattService>>(listOf())
 
         val callback = object : BluetoothGattCallback() {
             override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
@@ -163,6 +167,16 @@
                 attributeMap.updateWithFrameworkServices(fwkAdapter.getServices())
                 if (status == BluetoothGatt.GATT_SUCCESS) connectResult.complete(Unit)
                 else cancel("service discover failed")
+                servicesFlow.tryEmit(attributeMap.getServices())
+                if (connectResult.isActive) {
+                    if (status == BluetoothGatt.GATT_SUCCESS) connectResult.complete(Unit)
+                    else connectResult.cancel("service discover failed")
+                }
+            }
+
+            override fun onServiceChanged(gatt: BluetoothGatt) {
+                // TODO: under API 31, we have to subscribe to the service changed characteristic.
+                fwkAdapter.discoverServices()
             }
 
             override fun onCharacteristicRead(
@@ -236,9 +250,7 @@
                 }
             }
 
-            override fun getServices(): List<GattService> {
-                return attributeMap.getServices()
-            }
+            override val servicesFlow: StateFlow<List<GattService>> = servicesFlow.asStateFlow()
 
             override fun getService(uuid: UUID): GattService? {
                 return fwkAdapter.getService(uuid)?.let { attributeMap.fromFwkService(it) }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
index a8b1eb1..3e1623b 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
@@ -136,7 +136,7 @@
         container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View {
-        bluetoothLe = BluetoothLe.getInstance(requireContext())
+        bluetoothLe = BluetoothLe(requireContext())
 
         _binding = FragmentAdvertiserBinding.inflate(inflater, container, false)
 
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
index b65bbde..186431f 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
@@ -140,7 +140,7 @@
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        bluetoothLe = BluetoothLe.getInstance(requireContext())
+        bluetoothLe = BluetoothLe(requireContext())
 
         binding.tabLayout.addOnTabSelectedListener(onTabSelectedListener)
 
@@ -258,10 +258,10 @@
 
             try {
                 bluetoothLe.connectGatt(deviceConnection.bluetoothDevice) {
-                    Log.d(TAG, "connectGatt result: getServices() = ${getServices()}")
+                    Log.d(TAG, "connectGatt result: services() = $services")
 
                     deviceConnection.status = Status.CONNECTED
-                    deviceConnection.services = getServices()
+                    deviceConnection.services = services
                     launch(Dispatchers.Main) {
                         updateDeviceUI(deviceConnection)
                     }
diff --git a/buildSrc-tests/src/test/java/androidx/build/SourceJarTaskHelperTest.kt b/buildSrc-tests/src/test/java/androidx/build/SourceJarTaskHelperTest.kt
new file mode 100644
index 0000000..3e82e00
--- /dev/null
+++ b/buildSrc-tests/src/test/java/androidx/build/SourceJarTaskHelperTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+import com.google.common.truth.Truth.assertThat
+import org.gradle.testfixtures.ProjectBuilder
+import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
+import org.junit.Test
+
+class SourceJarTaskHelperTest {
+    @Test
+    fun generateMetadata() {
+        val project = ProjectBuilder.builder().build()
+        project.plugins.apply(KotlinMultiplatformPluginWrapper::class.java)
+        val extension = project.multiplatformExtension!!
+        extension.jvm()
+        val commonMain = extension.sourceSets.getByName("commonMain")
+        val jvmMain = extension.sourceSets.getByName("jvmMain")
+        val extraMain = extension.sourceSets.create("extraMain")
+        extraMain.dependsOn(commonMain)
+        jvmMain.dependsOn(commonMain)
+        jvmMain.dependsOn(extraMain)
+
+        val result = createSourceSetMetadata(extension)
+        assertThat(result).isEqualTo("""
+        {
+          "sourceSets": [
+            {
+              "name": "commonMain",
+              "dependencies": [],
+              "analysisPlatform": "common"
+            },
+            {
+              "name": "extraMain",
+              "dependencies": [
+                "commonMain"
+              ],
+              "analysisPlatform": "jvm"
+            },
+            {
+              "name": "jvmMain",
+              "dependencies": [
+                "commonMain",
+                "extraMain"
+              ],
+              "analysisPlatform": "jvm"
+            }
+          ]
+        }
+        """.trimIndent())
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 73b8580..5fe690c 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -460,6 +460,9 @@
                 it.artRewritingWorkaround()
             }
         }
+
+        project.buildOnServerDependsOnAssembleRelease()
+        project.buildOnServerDependsOnLint()
     }
 
     private fun configureWithTestPlugin(project: Project, androidXExtension: AndroidXExtension) {
@@ -473,6 +476,30 @@
         project.addToProjectMap(androidXExtension)
     }
 
+    private fun Project.buildOnServerDependsOnAssembleRelease() {
+        project.addToBuildOnServer("assembleRelease")
+    }
+
+    private fun Project.buildOnServerDependsOnLint() {
+        if (!project.usingMaxDepVersions()) {
+            project.agpVariants.all { variant ->
+                // in AndroidX, release and debug variants are essentially the same,
+                // so we don't run the lintRelease task on the build server
+                if (!variant.name.lowercase(Locale.getDefault()).contains("release")) {
+                    val taskName =
+                        "lint${variant.name.replaceFirstChar {
+                        if (it.isLowerCase()) {
+                            it.titlecase(Locale.getDefault())
+                        } else {
+                            it.toString()
+                        }
+                    }}"
+                    project.addToBuildOnServer(taskName)
+                }
+            }
+        }
+    }
+
     private fun HasAndroidTest.configureTests() {
         configureLicensePackaging()
         excludeVersionFilesFromTestApks()
@@ -595,6 +622,9 @@
         )
 
         project.addToProjectMap(androidXExtension)
+
+        project.buildOnServerDependsOnAssembleRelease()
+        project.buildOnServerDependsOnLint()
     }
 
     private fun configureWithJavaPlugin(project: Project, extension: AndroidXExtension) {
@@ -657,6 +687,8 @@
             configuration.resolutionStrategy.preferProjectModules()
         }
 
+        project.addToBuildOnServer("jar")
+
         project.addToProjectMap(extension)
     }
 
@@ -792,14 +824,20 @@
             File(project.buildDir, "../nativeBuildStaging")
     }
 
+    @Suppress("UnstableApiUsage") // finalizeDsl, minCompileSdkExtension
     private fun LibraryExtension.configureAndroidLibraryOptions(
         project: Project,
         androidXExtension: AndroidXExtension
     ) {
-        // Note, this should really match COMPILE_SDK_VERSION, however
-        // this API takes an integer and we are unable to set it to a
-        // pre-release SDK.
-        defaultConfig.aarMetadata.minCompileSdk = project.defaultAndroidConfig.targetSdk
+        // Propagate the compileSdk value into minCompileSdk. Don't propagate compileSdkExtension,
+        // since only one library actually depends on the extension APIs and they can explicitly
+        // declare that in their build.gradle. Note that when we're using a preview SDK, the value
+        // for compileSdk will be null and the resulting AAR metadata won't have a minCompileSdk --
+        // this is okay because AGP automatically embeds forceCompileSdkPreview in the AAR metadata
+        // and uses it instead of minCompileSdk.
+        project.extensions.findByType<LibraryAndroidComponentsExtension>()!!.finalizeDsl {
+            it.defaultConfig.aarMetadata.minCompileSdk = it.compileSdk
+        }
 
         // The full Guava artifact is very large, so they split off a special artifact containing a
         // standalone version of the commonly-used ListenableFuture interface. However, they also
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index fc33853..9eb83d2 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -29,16 +29,12 @@
 import androidx.build.uptodatedness.TaskUpToDateValidator
 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
 import com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
-import com.android.build.gradle.AppPlugin
-import com.android.build.gradle.LibraryPlugin
 import java.io.File
-import java.util.Locale
 import java.util.concurrent.ConcurrentHashMap
 import org.gradle.api.GradleException
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.artifacts.component.ModuleComponentSelector
-import org.gradle.api.plugins.JavaPlugin
 import org.gradle.api.plugins.JvmEcosystemPlugin
 import org.gradle.api.tasks.bundling.Zip
 import org.gradle.api.tasks.bundling.ZipEntryCompression
@@ -101,37 +97,6 @@
         }
 
         extra.set("projects", ConcurrentHashMap<String, String>())
-        subprojects { project ->
-            project.afterEvaluate {
-                if (
-                    project.plugins.hasPlugin(LibraryPlugin::class.java) ||
-                        project.plugins.hasPlugin(AppPlugin::class.java)
-                ) {
-
-                    buildOnServerTask.dependsOn("${project.path}:assembleRelease")
-                    if (!project.usingMaxDepVersions()) {
-                        project.agpVariants.all { variant ->
-                            // in AndroidX, release and debug variants are essentially the same,
-                            // so we don't run the lintRelease task on the build server
-                            if (!variant.name.lowercase(Locale.getDefault()).contains("release")) {
-                                val taskName =
-                                    "lint${variant.name.replaceFirstChar {
-                                    if (it.isLowerCase()) {
-                                        it.titlecase(Locale.getDefault())
-                                    } else {
-                                        it.toString()
-                                    }
-                                }}"
-                                buildOnServerTask.dependsOn("${project.path}:$taskName")
-                            }
-                        }
-                    }
-                }
-            }
-            project.plugins.withType(JavaPlugin::class.java) {
-                buildOnServerTask.dependsOn("${project.path}:jar")
-            }
-        }
 
         // NOTE: this task is used by the Github CI as well. If you make any changes here,
         // please update the .github/workflows files as well, if necessary.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
index 1e6ed89..04c4da3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
@@ -258,7 +258,7 @@
             "commonMain" to
                 mapOf(
                     "name" to commonMain.name,
-                    "dependencies" to commonMain.dependsOn.map { it.name },
+                    "dependencies" to commonMain.dependsOn.map { it.name }.sorted(),
                     "analysisPlatform" to DokkaAnalysisPlatform.COMMON.jsonName
                 )
         )
@@ -267,13 +267,15 @@
             sourceSetsByName.getOrPut(it.name) {
                 mapOf(
                     "name" to it.name,
-                    "dependencies" to it.dependsOn.map { it.name },
+                    "dependencies" to it.dependsOn.map { it.name }.sorted(),
                     "analysisPlatform" to target.docsPlatform().jsonName
                 )
             }
         }
     }
-    val sourceSetMetadata = mutableMapOf("sourceSets" to sourceSetsByName.values)
+    val sourceSetMetadata = mapOf(
+        "sourceSets" to sourceSetsByName.keys.sorted().map { sourceSetsByName[it] }
+    )
     val gson = GsonBuilder().setPrettyPrinting().create()
     return gson.toJson(sourceSetMetadata)
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
index 669aa02..7b470d9 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
@@ -239,7 +239,7 @@
 
         task.taskExtension.set(
             object : DefaultSpdxSbomTaskExtension() {
-                override fun mapRepoUri(repoUri: URI, artifact: ModuleVersionIdentifier): URI {
+                override fun mapRepoUri(repoUri: URI?, artifact: ModuleVersionIdentifier): URI {
                     val uriString = repoUri.toString()
                     for (repo in repos) {
                         val ourRepoUrl = repo.key
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt b/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
index c29f465..99ed273 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt
@@ -22,12 +22,12 @@
 
 const val BUILD_ON_SERVER_TASK = "buildOnServer"
 
-/** Configures the root project's buildOnServer task to run the specified task. */
+/** Configures the project's buildOnServer task to run the specified task. */
 fun <T : Task> Project.addToBuildOnServer(taskProvider: TaskProvider<T>) {
     tasks.named(BUILD_ON_SERVER_TASK).configure { it.dependsOn(taskProvider) }
 }
 
-/** Configures the root project's buildOnServer task to run the specified task. */
-fun <T : Task> Project.addToBuildOnServer(taskPath: String) {
+/** Configures the project's buildOnServer task to run the specified task. */
+fun Project.addToBuildOnServer(taskPath: String) {
     tasks.named(BUILD_ON_SERVER_TASK).configure { it.dependsOn(taskPath) }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index f8d6362..6fd951c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -210,6 +210,14 @@
         return setOf(SDR)
     }
 
+    override fun isPreviewStabilizationSupported(): Boolean {
+        return false
+    }
+
+    override fun isVideoStabilizationSupported(): Boolean {
+        return false
+    }
+
     private fun profileSetToDynamicRangeSet(profileSet: Set<Long>): Set<DynamicRange> {
         return profileSet.map { profileToDynamicRange(it) }.toSet()
     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
index ce74b04..33fac2e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/GuaranteedConfigurationsUtil.kt
@@ -718,6 +718,56 @@
         return surfaceCombinations
     }
 
+    /**
+     * Returns the minimally guaranteed stream combinations when one or more
+     * streams are configured as a 10-bit input.
+     */
+    @JvmStatic
+    fun get10BitSupportedCombinationList(): List<SurfaceCombination> {
+        return listOf(
+            // (PRIV, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.MAXIMUM))
+            },
+            // (YUV, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.MAXIMUM))
+            },
+            // (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.JPEG, ConfigSize.MAXIMUM))
+            },
+            // (PRIV, PREVIEW) + (YUV, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.MAXIMUM))
+            },
+            // (YUV, PREVIEW) + (YUV, MAXIMUM)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.MAXIMUM))
+            },
+            // (PRIV, PREVIEW) + (PRIV, RECORD)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.RECORD))
+            },
+            // (PRIV, PREVIEW) + (PRIV, RECORD) + (YUV, RECORD)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.RECORD))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.YUV, ConfigSize.RECORD))
+            },
+            // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+            SurfaceCombination().apply {
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.PREVIEW))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.PRIV, ConfigSize.RECORD))
+                addSurfaceConfig(SurfaceConfig.create(ConfigType.JPEG, ConfigSize.RECORD))
+            },
+        )
+    }
+
     @JvmStatic
     fun generateConcurrentSupportedCombinationList(): List<SurfaceCombination> {
         val surfaceCombinations: MutableList<SurfaceCombination> = arrayListOf()
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index daa54a8..2122ec2 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -18,20 +18,19 @@
 
 import android.annotation.SuppressLint
 import android.content.Context
+import android.content.pm.PackageManager
 import android.content.pm.PackageManager.FEATURE_CAMERA_CONCURRENT
 import android.graphics.ImageFormat
-import android.graphics.Point
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.params.StreamConfigurationMap
-import android.hardware.display.DisplayManager
 import android.media.CamcorderProfile
 import android.media.MediaRecorder
 import android.os.Build
 import android.util.Pair
+import android.util.Range
 import android.util.Rational
 import android.util.Size
-import android.view.Display
 import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
 import androidx.camera.camera2.pipe.CameraMetadata
@@ -41,6 +40,8 @@
 import androidx.camera.camera2.pipe.integration.compat.workaround.ResolutionCorrector
 import androidx.camera.camera2.pipe.integration.compat.workaround.TargetAspectRatio
 import androidx.camera.camera2.pipe.integration.impl.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.internal.DynamicRangeResolver
+import androidx.camera.core.DynamicRange
 import androidx.camera.core.impl.AttachedSurfaceInfo
 import androidx.camera.core.impl.CameraMode
 import androidx.camera.core.impl.EncoderProfilesProxy
@@ -60,6 +61,8 @@
 import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
 import java.util.Arrays
 import java.util.Collections
+import kotlin.math.floor
+import kotlin.math.min
 
 /**
  * Camera device supported surface configuration combinations
@@ -85,22 +88,22 @@
     private val concurrentSurfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
     private val surfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
     private val ultraHighSurfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
-    private val cameraModeToSupportedCombinationsMap: MutableMap<Int, List<SurfaceCombination>> =
-        mutableMapOf()
+    private val featureSettingsToSupportedCombinationsMap:
+        MutableMap<FeatureSettings, List<SurfaceCombination>> = mutableMapOf()
+    private val surfaceCombinations10Bit: MutableList<SurfaceCombination> = mutableListOf()
     private var isRawSupported = false
     private var isBurstCaptureSupported = false
     private var isConcurrentCameraModeSupported = false
     private var isUltraHighResolutionSensorSupported = false
     internal lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
     private val surfaceSizeDefinitionFormats = mutableListOf<Int>()
-    private val displayManager: DisplayManager =
-        (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
     private val streamConfigurationMapCompat = getStreamConfigurationMapCompat()
     private val extraSupportedSurfaceCombinationsContainer =
         ExtraSupportedSurfaceCombinationsContainer()
     private val displayInfoManager = DisplayInfoManager(context)
     private val resolutionCorrector = ResolutionCorrector()
     private val targetAspectRatio: TargetAspectRatio = TargetAspectRatio()
+    private val dynamicRangeResolver: DynamicRangeResolver = DynamicRangeResolver(cameraMetadata)
 
     init {
         checkCapabilities()
@@ -113,6 +116,10 @@
         if (isConcurrentCameraModeSupported) {
             generateConcurrentSupportedCombinationList()
         }
+
+        if (dynamicRangeResolver.is10BitDynamicRangeSupported()) {
+            generate10BitSupportedCombinationList()
+        }
         generateSurfaceSizeDefinition()
     }
 
@@ -120,48 +127,50 @@
      * Check whether the input surface configuration list is under the capability of any combination
      * of this object.
      *
-     * @param cameraMode        the working camera mode.
+     * @param featureSettings  the settings for the camera's features/capabilities.
      * @param surfaceConfigList the surface configuration list to be compared
+     *
      * @return the check result that whether it could be supported
      */
     fun checkSupported(
-        cameraMode: Int,
+        featureSettings: FeatureSettings,
         surfaceConfigList: List<SurfaceConfig>
     ): Boolean {
-        // TODO(b/262772650): camera-pipe support for concurrent camera
-        val targetSurfaceCombinations = getSurfaceCombinationsByCameraMode(cameraMode)
-        for (surfaceCombination in targetSurfaceCombinations) {
-            if (surfaceCombination
-                    .getOrderedSupportedSurfaceConfigList(surfaceConfigList) != null
-            ) {
-                return true
-            }
+        return getSurfaceCombinationsByFeatureSettings(featureSettings).any {
+            it.getOrderedSupportedSurfaceConfigList(surfaceConfigList) != null
         }
-        return false
     }
 
     /**
-     * Returns the supported surface combinations according to the specified camera mode.
+     * Returns the supported surface combinations according to the specified feature
+     * settings.
      */
-    private fun getSurfaceCombinationsByCameraMode(
-        @CameraMode.Mode cameraMode: Int
+    private fun getSurfaceCombinationsByFeatureSettings(
+        featureSettings: FeatureSettings
     ): List<SurfaceCombination> {
-        if (cameraModeToSupportedCombinationsMap.containsKey(cameraMode)) {
-            return cameraModeToSupportedCombinationsMap[cameraMode]!!
+        if (featureSettingsToSupportedCombinationsMap.containsKey(featureSettings)) {
+            return featureSettingsToSupportedCombinationsMap[featureSettings]!!
         }
         var supportedSurfaceCombinations: MutableList<SurfaceCombination> = mutableListOf()
-        when (cameraMode) {
-            CameraMode.CONCURRENT_CAMERA -> supportedSurfaceCombinations =
-                concurrentSurfaceCombinations
+        if (featureSettings.requiredMaxBitDepth == DynamicRange.BIT_DEPTH_8_BIT) {
+            when (featureSettings.cameraMode) {
+                CameraMode.CONCURRENT_CAMERA -> supportedSurfaceCombinations =
+                    concurrentSurfaceCombinations
 
-            CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA -> {
-                supportedSurfaceCombinations.addAll(ultraHighSurfaceCombinations)
-                supportedSurfaceCombinations.addAll(surfaceCombinations)
+                CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA -> {
+                    supportedSurfaceCombinations.addAll(ultraHighSurfaceCombinations)
+                    supportedSurfaceCombinations.addAll(surfaceCombinations)
+                }
+
+                else -> supportedSurfaceCombinations.addAll(surfaceCombinations)
             }
-
-            else -> supportedSurfaceCombinations.addAll(surfaceCombinations)
+        } else if (featureSettings.requiredMaxBitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+            // For 10-bit outputs, only the default camera mode is currently supported.
+            if (featureSettings.cameraMode == CameraMode.DEFAULT) {
+                supportedSurfaceCombinations.addAll(surfaceCombinations10Bit)
+            }
         }
-        cameraModeToSupportedCombinationsMap[cameraMode] = supportedSurfaceCombinations
+        featureSettingsToSupportedCombinationsMap[featureSettings] = supportedSurfaceCombinations
         return supportedSurfaceCombinations
     }
 
@@ -188,7 +197,7 @@
      * Finds the suggested stream specification of the newly added UseCaseConfig.
      *
      * @param cameraMode        the working camera mode.
-     * @param existingSurfaces  the existing surfaces.
+     * @param attachedSurfaces  the existing surfaces.
      * @param newUseCaseConfigsSupportedSizeMap newly added UseCaseConfig to supported output sizes
      * map.
      * @return the suggested stream specs, which is a mapping from UseCaseConfig to the suggested
@@ -198,15 +207,34 @@
      */
     fun getSuggestedStreamSpecifications(
         cameraMode: Int,
-        existingSurfaces: List<AttachedSurfaceInfo>,
+        attachedSurfaces: List<AttachedSurfaceInfo>,
         newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>
     ): Pair<Map<UseCaseConfig<*>, StreamSpec>, Map<AttachedSurfaceInfo, StreamSpec>> {
+        // Refresh Preview Size based on current display configurations.
         refreshPreviewSize()
-        val surfaceConfigs: MutableList<SurfaceConfig> = ArrayList()
-        for (scc in existingSurfaces) {
+        val surfaceConfigs: MutableList<SurfaceConfig> = mutableListOf()
+        for (scc in attachedSurfaces) {
             surfaceConfigs.add(scc.surfaceConfig)
         }
         val newUseCaseConfigs = newUseCaseConfigsSupportedSizeMap.keys.toList()
+
+        // Get the index order list by the use case priority for finding stream configuration
+        val useCasesPriorityOrder = getUseCasesPriorityOrder(newUseCaseConfigs)
+        val resolvedDynamicRanges = dynamicRangeResolver.resolveAndValidateDynamicRanges(
+            attachedSurfaces,
+            newUseCaseConfigs, useCasesPriorityOrder
+        )
+        val requiredMaxBitDepth: Int = getRequiredMaxBitDepth(resolvedDynamicRanges)
+        val featureSettings = FeatureSettings(cameraMode, requiredMaxBitDepth)
+        require(
+            !(cameraMode != CameraMode.DEFAULT &&
+                requiredMaxBitDepth == DynamicRange.BIT_DEPTH_10_BIT)
+        ) {
+            "No supported surface combination is " +
+                "found for camera device - Id : $cameraId. 10 bit dynamic range is not " +
+                "currently supported in ${CameraMode.toLabelString(cameraMode)} camera mode."
+        }
+
         // Use the small size (640x480) for new use cases to check whether there is any possible
         // supported combination first
         for (useCaseConfig in newUseCaseConfigs) {
@@ -220,83 +248,476 @@
             )
         }
 
-        if (!checkSupported(cameraMode, surfaceConfigs)) {
+        if (!checkSupported(featureSettings, surfaceConfigs)) {
             throw java.lang.IllegalArgumentException(
                 "No supported surface combination is found for camera device - Id : " + cameraId +
                     ".  May be attempting to bind too many use cases. " + "Existing surfaces: " +
-                    existingSurfaces + " New configs: " + newUseCaseConfigs
+                    attachedSurfaces + " New configs: " + newUseCaseConfigs
             )
         }
-        // Get the index order list by the use case priority for finding stream configuration
-        val useCasesPriorityOrder: List<Int> = getUseCasesPriorityOrder(
-            newUseCaseConfigs
+
+        val targetFpsRange =
+            getTargetFpsRange(attachedSurfaces, newUseCaseConfigs, useCasesPriorityOrder)
+        val maxSupportedFps = getMaxSupportedFps(attachedSurfaces)
+
+        val bestSizesAndFps = findBestSizesAndFps(
+            newUseCaseConfigsSupportedSizeMap,
+            attachedSurfaces,
+            newUseCaseConfigs,
+            maxSupportedFps,
+            useCasesPriorityOrder,
+            targetFpsRange,
+            featureSettings
         )
-        val supportedOutputSizesList: MutableList<List<Size>> = ArrayList()
+
+        val suggestedStreamSpecMap = generateSuggestedStreamSpecMap(
+            bestSizesAndFps.first,
+            targetFpsRange,
+            bestSizesAndFps.second,
+            newUseCaseConfigs,
+            useCasesPriorityOrder,
+            resolvedDynamicRanges,
+        )
+
+        return Pair.create(suggestedStreamSpecMap, mapOf<AttachedSurfaceInfo, StreamSpec>())
+    }
+
+    private fun getSupportedOutputSizesList(
+        newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>,
+    ): List<List<Size>> {
+        val supportedOutputSizesList: MutableList<List<Size>> = mutableListOf()
 
         // Collect supported output sizes for all use cases
         for (index in useCasesPriorityOrder) {
-            var supportedOutputSizes: List<Size> =
-                newUseCaseConfigsSupportedSizeMap[newUseCaseConfigs[index]]!!
+            var supportedOutputSizes = newUseCaseConfigsSupportedSizeMap[newUseCaseConfigs[index]]!!
             supportedOutputSizes = applyResolutionSelectionOrderRelatedWorkarounds(
                 supportedOutputSizes,
                 newUseCaseConfigs[index].inputFormat
             )
             supportedOutputSizesList.add(supportedOutputSizes)
         }
-        // Get all possible size arrangements
-        val allPossibleSizeArrangements: List<List<Size>> = getAllPossibleSizeArrangements(
-            supportedOutputSizesList
-        )
+        return supportedOutputSizesList
+    }
 
-        var suggestedStreamSpecMap: Map<UseCaseConfig<*>, StreamSpec>? = null
+    private fun getTargetFpsRange(
+        attachedSurfaces: List<AttachedSurfaceInfo>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>
+    ): Range<Int>? {
+        var targetFrameRateForConfig: Range<Int>? = null
+        for (attachedSurfaceInfo in attachedSurfaces) {
+            // init target fps range for new configs from existing surfaces
+            targetFrameRateForConfig = getUpdatedTargetFrameRate(
+                attachedSurfaceInfo.targetFrameRate,
+                targetFrameRateForConfig
+            )
+        }
+        // update target fps for new configs using new use cases' priority order
+        for (index in useCasesPriorityOrder) {
+            targetFrameRateForConfig = getUpdatedTargetFrameRate(
+                newUseCaseConfigs[index].getTargetFrameRate(null),
+                targetFrameRateForConfig
+            )
+        }
+        return targetFrameRateForConfig
+    }
+
+    private fun getMaxSupportedFps(
+        attachedSurfaces: List<AttachedSurfaceInfo>,
+    ): Int {
+        var existingSurfaceFrameRateCeiling = Int.MAX_VALUE
+        for (attachedSurfaceInfo in attachedSurfaces) {
+            // get the fps ceiling for existing surfaces
+            existingSurfaceFrameRateCeiling = getUpdatedMaximumFps(
+                existingSurfaceFrameRateCeiling,
+                attachedSurfaceInfo.imageFormat, attachedSurfaceInfo.size
+            )
+        }
+        return existingSurfaceFrameRateCeiling
+    }
+
+    private fun findBestSizesAndFps(
+        newUseCaseConfigsSupportedSizeMap: Map<UseCaseConfig<*>, List<Size>>,
+        attachedSurfaces: List<AttachedSurfaceInfo>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        existingSurfaceFrameRateCeiling: Int,
+        useCasesPriorityOrder: List<Int>,
+        targetFrameRateForConfig: Range<Int>?,
+        featureSettings: FeatureSettings
+    ): Pair<List<Size>, Int> {
+        var bestSizes: List<Size>? = null
+        var bestConfigMaxFps = Int.MAX_VALUE
+        val allPossibleSizeArrangements = getAllPossibleSizeArrangements(
+            getSupportedOutputSizesList(
+                newUseCaseConfigsSupportedSizeMap,
+                newUseCaseConfigs,
+                useCasesPriorityOrder
+            )
+        )
         // Transform use cases to SurfaceConfig list and find the first (best) workable combination
         for (possibleSizeList in allPossibleSizeArrangements) {
             // Attach SurfaceConfig of original use cases since it will impact the new use cases
-            val surfaceConfigList: MutableList<SurfaceConfig> = ArrayList()
-            for (sc in existingSurfaces) {
-                surfaceConfigList.add(sc.surfaceConfig)
-            }
-
-            // Attach SurfaceConfig of new use cases
-            for (i in possibleSizeList.indices) {
-                val size = possibleSizeList[i]
-                val newUseCase = newUseCaseConfigs[useCasesPriorityOrder[i]]
-                surfaceConfigList.add(
-                    SurfaceConfig.transformSurfaceConfig(
-                        cameraMode,
-                        newUseCase.inputFormat,
-                        size,
-                        getUpdatedSurfaceSizeDefinitionByFormat(newUseCase.inputFormat)
-                    )
-                )
-            }
-
-            // Check whether the SurfaceConfig combination can be supported
-            if (checkSupported(cameraMode, surfaceConfigList)) {
-                suggestedStreamSpecMap = HashMap()
-                for (useCaseConfig in newUseCaseConfigs) {
-                    suggestedStreamSpecMap.put(
-                        useCaseConfig,
-                        StreamSpec.builder(
-                            possibleSizeList[useCasesPriorityOrder.indexOf(
-                                newUseCaseConfigs.indexOf(useCaseConfig)
-                            )]
-                        ).build()
-                    )
+            val surfaceConfigList = getSurfaceConfigList(
+                featureSettings.cameraMode,
+                attachedSurfaces, possibleSizeList, newUseCaseConfigs,
+                useCasesPriorityOrder
+            )
+            val currentConfigFrameRateCeiling = getCurrentConfigFrameRateCeiling(
+                possibleSizeList, newUseCaseConfigs,
+                useCasesPriorityOrder, existingSurfaceFrameRateCeiling
+            )
+            var isConfigFrameRateAcceptable = true
+            if (targetFrameRateForConfig != null) {
+                if (existingSurfaceFrameRateCeiling > currentConfigFrameRateCeiling &&
+                    currentConfigFrameRateCeiling < targetFrameRateForConfig.lower
+                ) {
+                    // if the max fps before adding new use cases supports our target fps range
+                    // BUT the max fps of the new configuration is below
+                    // our target fps range, we'll want to check the next configuration until we
+                    // get one that supports our target FPS
+                    isConfigFrameRateAcceptable = false
                 }
-                break
+            }
+
+            // only change the saved config if you get another that has a better max fps
+            if (checkSupported(featureSettings, surfaceConfigList)) {
+                // if we have a configuration where the max fps is acceptable for our target, break
+                if (isConfigFrameRateAcceptable) {
+                    bestConfigMaxFps = currentConfigFrameRateCeiling
+                    bestSizes = possibleSizeList
+                    break
+                }
+                // if the config is supported by the device but doesn't meet the target frame rate,
+                // save the config
+                if (bestConfigMaxFps == Int.MAX_VALUE) {
+                    bestConfigMaxFps = currentConfigFrameRateCeiling
+                    bestSizes = possibleSizeList
+                } else if (bestConfigMaxFps < currentConfigFrameRateCeiling) {
+                    // only change the saved config if the max fps is better
+                    bestConfigMaxFps = currentConfigFrameRateCeiling
+                    bestSizes = possibleSizeList
+                }
             }
         }
-        if (suggestedStreamSpecMap == null) {
-            throw java.lang.IllegalArgumentException(
-                "No supported surface combination is found for camera device - Id : " +
-                    cameraId + " and Hardware level: " + hardwareLevel +
-                    ". May be the specified resolution is too large and not supported." +
-                    " Existing surfaces: " + existingSurfaces +
-                    " New configs: " + newUseCaseConfigs
+        require(bestSizes != null) {
+            "No supported surface combination is found for camera device - Id : $cameraId " +
+                "and Hardware level: $hardwareLevel. " +
+                "May be the specified resolution is too large and not supported. " +
+                "Existing surfaces: $attachedSurfaces. New configs: $newUseCaseConfigs."
+        }
+        return Pair(bestSizes, bestConfigMaxFps)
+    }
+
+    private fun generateSuggestedStreamSpecMap(
+        bestSizes: List<Size>,
+        targetFpsRange: Range<Int>?,
+        bestConfigMaxFps: Int,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>,
+        resolvedDynamicRanges: Map<UseCaseConfig<*>, DynamicRange>,
+    ): Map<UseCaseConfig<*>, StreamSpec> {
+        val suggestedStreamSpecMap = mutableMapOf<UseCaseConfig<*>, StreamSpec>()
+        var targetFrameRateForDevice: Range<Int>? = null
+        if (targetFpsRange != null) {
+            targetFrameRateForDevice = getClosestSupportedDeviceFrameRate(
+                targetFpsRange,
+                bestConfigMaxFps
             )
         }
-        return Pair.create(suggestedStreamSpecMap, mapOf<AttachedSurfaceInfo, StreamSpec>())
+        for ((index, useCaseConfig) in newUseCaseConfigs.withIndex()) {
+            val resolutionForUseCase =
+                bestSizes[
+                    useCasesPriorityOrder.indexOf(index)]
+            val streamSpecBuilder = StreamSpec.builder(resolutionForUseCase)
+                .setDynamicRange(
+                    checkNotNull(resolvedDynamicRanges[useCaseConfig])
+                )
+            if (targetFrameRateForDevice != null) {
+                streamSpecBuilder.setExpectedFrameRateRange(targetFrameRateForDevice)
+            }
+            suggestedStreamSpecMap[useCaseConfig] = streamSpecBuilder.build()
+        }
+        return suggestedStreamSpecMap
+    }
+
+    private fun getRequiredMaxBitDepth(
+        resolvedDynamicRanges: Map<UseCaseConfig<*>, DynamicRange>
+    ): Int {
+        for (dynamicRange in resolvedDynamicRanges.values) {
+            if (dynamicRange.bitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+                return DynamicRange.BIT_DEPTH_10_BIT
+            }
+        }
+        return DynamicRange.BIT_DEPTH_8_BIT
+    }
+
+    private fun getSurfaceConfigList(
+        @CameraMode.Mode cameraMode: Int,
+        attachedSurfaces: List<AttachedSurfaceInfo>,
+        possibleSizeList: List<Size>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>,
+    ): List<SurfaceConfig> {
+        val surfaceConfigList: MutableList<SurfaceConfig> = mutableListOf()
+        for (attachedSurfaceInfo in attachedSurfaces) {
+            surfaceConfigList.add(attachedSurfaceInfo.surfaceConfig)
+        }
+
+        // Attach SurfaceConfig of new use cases
+        for ((i, size) in possibleSizeList.withIndex()) {
+            val newUseCase = newUseCaseConfigs[useCasesPriorityOrder[i]]
+            val imageFormat = newUseCase.inputFormat
+            // add new use case/size config to list of surfaces
+            val surfaceConfig = SurfaceConfig.transformSurfaceConfig(
+                cameraMode,
+                imageFormat,
+                size,
+                getUpdatedSurfaceSizeDefinitionByFormat(imageFormat)
+            )
+            surfaceConfigList.add(surfaceConfig)
+        }
+        return surfaceConfigList
+    }
+
+    private fun getCurrentConfigFrameRateCeiling(
+        possibleSizeList: List<Size>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasesPriorityOrder: List<Int>,
+        currentConfigFrameRateCeiling: Int,
+    ): Int {
+        var newConfigFrameRateCeiling: Int = currentConfigFrameRateCeiling
+        // Attach SurfaceConfig of new use cases
+        for ((i, size) in possibleSizeList.withIndex()) {
+            val newUseCase = newUseCaseConfigs[useCasesPriorityOrder[i]]
+            // get the maximum fps of the new surface and update the maximum fps of the
+            // proposed configuration
+            newConfigFrameRateCeiling = getUpdatedMaximumFps(
+                newConfigFrameRateCeiling,
+                newUseCase.inputFormat,
+                size
+            )
+        }
+        return newConfigFrameRateCeiling
+    }
+
+    private fun getMaxFrameRate(
+        imageFormat: Int,
+        size: Size?
+    ): Int {
+        var maxFrameRate = 0
+        try {
+            val minFrameDuration = getStreamConfigurationMapCompat().getOutputMinFrameDuration(
+                imageFormat,
+                size
+            ) ?: return 0
+            maxFrameRate = floor(1_000_000_000.0 / minFrameDuration + 0.05).toInt()
+        } catch (e1: IllegalArgumentException) {
+            // TODO: this try catch is in place for the rare that a surface config has a size
+            //  incompatible for getOutputMinFrameDuration...  put into a Quirk
+        }
+        return maxFrameRate
+    }
+
+    /**
+     *
+     * @param range
+     * @return the length of the range
+     */
+    private fun getRangeLength(range: Range<Int>): Int {
+        return range.upper - range.lower + 1
+    }
+
+    /**
+     * @return the distance between the nearest limits of two non-intersecting ranges
+     */
+    private fun getRangeDistance(firstRange: Range<Int>, secondRange: Range<Int>): Int {
+        require(
+            !firstRange.contains(secondRange.upper) &&
+                !firstRange.contains(secondRange.lower)
+        ) { "Ranges must not intersect" }
+        return if (firstRange.lower > secondRange.upper) {
+            firstRange.lower - secondRange.upper
+        } else {
+            secondRange.lower - firstRange.upper
+        }
+    }
+
+    /**
+     * @param targetFps the target frame rate range used while comparing to device-supported ranges
+     * @param storedRange the device-supported range that is currently saved and intersects with
+     * targetFps
+     * @param newRange a new potential device-supported range that intersects with targetFps
+     * @return the device-supported range that better matches the target fps
+     */
+    private fun compareIntersectingRanges(
+        targetFps: Range<Int>,
+        storedRange: Range<Int>,
+        newRange: Range<Int>
+    ): Range<Int> {
+        // TODO(b/272075984): some ranges may may have a larger intersection but may also have an
+        //  excessively large portion that is non-intersecting. Will want to do further
+        //  investigation to find a more optimized way to decide when a potential range has too
+        //  much non-intersecting value and discard it
+        val storedIntersectionsize =
+            getRangeLength(storedRange.intersect(targetFps)).toDouble()
+        val newIntersectionSize = getRangeLength(newRange.intersect(targetFps)).toDouble()
+        val newRangeRatio = newIntersectionSize / getRangeLength(newRange)
+        val storedRangeRatio = storedIntersectionsize / getRangeLength(storedRange)
+        if (newIntersectionSize > storedIntersectionsize) {
+            // if new, the new range must have at least 50% of its range intersecting, OR has a
+            // larger percentage of intersection than the previous stored range
+            if (newRangeRatio >= .5 || newRangeRatio >= storedRangeRatio) {
+                return newRange
+            }
+        } else if (newIntersectionSize == storedIntersectionsize) {
+            // if intersecting ranges have same length... pick the one that has the higher
+            // intersection ratio
+            if (newRangeRatio > storedRangeRatio) {
+                return newRange
+            } else if (newRangeRatio == storedRangeRatio && newRange.lower > storedRange.lower
+            ) {
+                // if equal intersection size AND ratios pick the higher range
+                return newRange
+            }
+        } else if (storedRangeRatio < .5 && newRangeRatio > storedRangeRatio
+        ) {
+            // if the new one has a smaller range... only change if existing has an intersection
+            // ratio < 50% and the new one has an intersection ratio > than the existing one
+            return newRange
+        }
+        return storedRange
+    }
+
+    /**
+     * Finds a frame rate range supported by the device that is closest to the target frame rate
+     *
+     * @param targetFrameRate the Target Frame Rate resolved from all current existing surfaces
+     * and incoming new use cases
+     * @return a frame rate range supported by the device that is closest to targetFrameRate
+     */
+    private fun getClosestSupportedDeviceFrameRate(
+        targetFrameRate: Range<Int>,
+        maxFps: Int
+    ): Range<Int> {
+        var newTargetFrameRate = targetFrameRate
+        // get all fps ranges supported by device
+        val availableFpsRanges =
+            cameraMetadata[CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES]
+                ?: return StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
+        // if  whole target frame rate range > maxFps of configuration, the target for this
+        // calculation will be [max,max].
+
+        // if the range is partially larger than  maxFps, the target for this calculation will be
+        // [target.lower, max] for the sake of this calculation
+        newTargetFrameRate = Range(
+            min(newTargetFrameRate.lower, maxFps),
+            min(newTargetFrameRate.upper, maxFps)
+        )
+        var bestRange = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
+        var currentIntersectSize = 0
+        for (potentialRange in availableFpsRanges) {
+            // ignore ranges completely larger than configuration's maximum fps
+            if (maxFps < potentialRange.lower) {
+                continue
+            }
+            if (bestRange == StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED) {
+                bestRange = potentialRange
+            }
+            // take if range is a perfect match
+            if (potentialRange == newTargetFrameRate) {
+                bestRange = potentialRange
+                break
+            }
+            try {
+                // bias towards a range that intersects on the upper end
+                val newIntersection = potentialRange.intersect(newTargetFrameRate)
+                val newIntersectSize: Int = getRangeLength(
+                    newIntersection
+                )
+                // if this range intersects our target + no other range was already
+                if (currentIntersectSize == 0) {
+                    bestRange = potentialRange
+                    currentIntersectSize = newIntersectSize
+                } else if (newIntersectSize >= currentIntersectSize) {
+                    // if the currently stored range + new range both intersect, check to see
+                    // which one should be picked over the other
+                    bestRange = compareIntersectingRanges(
+                        newTargetFrameRate, bestRange,
+                        potentialRange
+                    )
+                    currentIntersectSize = getRangeLength(
+                        newTargetFrameRate.intersect(bestRange)
+                    )
+                }
+            } catch (e: IllegalArgumentException) {
+                if (currentIntersectSize != 0) {
+                    continue
+                }
+
+                // if no intersection is present, pick the range that is closer to our target
+                if (getRangeDistance(potentialRange, newTargetFrameRate)
+                    < getRangeDistance(
+                        bestRange, newTargetFrameRate
+                    )
+                ) {
+                    bestRange = potentialRange
+                } else if (getRangeDistance(potentialRange, newTargetFrameRate) ==
+                    getRangeDistance(bestRange, newTargetFrameRate)
+                ) {
+                    if (potentialRange.lower > bestRange.upper) {
+                        // if they both have the same distance, pick the higher range
+                        bestRange = potentialRange
+                    } else if (getRangeLength(potentialRange)
+                        < getRangeLength(bestRange)
+                    ) {
+                        // if one isn't higher than the other, pick the range with the
+                        // shorter length
+                        bestRange = potentialRange
+                    }
+                }
+            }
+        }
+        return bestRange
+    }
+
+    /**
+     * @param newTargetFrameRate    an incoming frame rate range
+     * @param storedTargetFrameRate a stored frame rate range to be modified
+     * @return adjusted target frame rate
+     *
+     * If the two ranges are both nonnull and disjoint of each other, then the range that was
+     * already stored will be used
+     */
+    private fun getUpdatedTargetFrameRate(
+        newTargetFrameRate: Range<Int>?,
+        storedTargetFrameRate: Range<Int>?
+    ): Range<Int>? {
+        var updatedTarget = storedTargetFrameRate
+        if (storedTargetFrameRate == null) {
+            // if stored value was null before, set it to the new value
+            updatedTarget = newTargetFrameRate
+        } else if (newTargetFrameRate != null) {
+            updatedTarget = try {
+                // get intersection of existing target fps
+                storedTargetFrameRate
+                    .intersect(newTargetFrameRate)
+            } catch (e: java.lang.IllegalArgumentException) {
+                // no intersection, keep the previously stored value
+                storedTargetFrameRate
+            }
+        }
+        return updatedTarget
+    }
+
+    /**
+     * @param currentMaxFps the previously stored Max FPS
+     * @param imageFormat   the image format of the incoming surface
+     * @param size          the size of the incoming surface
+     */
+    private fun getUpdatedMaximumFps(currentMaxFps: Int, imageFormat: Int, size: Size): Int {
+        return min(currentMaxFps, getMaxFrameRate(imageFormat, size))
     }
 
     /**
@@ -317,7 +738,7 @@
         imageFormat: Int
     ): List<Size> {
         // Applies TargetAspectRatio workaround
-        var ratio: Rational? =
+        val ratio: Rational? =
             when (targetAspectRatio[cameraMetadata, streamConfigurationMapCompat]) {
                 TargetAspectRatio.RATIO_4_3 ->
                     AspectRatioUtil.ASPECT_RATIO_4_3
@@ -426,6 +847,12 @@
         )
     }
 
+    private fun generate10BitSupportedCombinationList() {
+        surfaceCombinations10Bit.addAll(
+            GuaranteedConfigurationsUtil.get10BitSupportedCombinationList()
+        )
+    }
+
     /**
      * Generation the size definition for VGA, s720p, PREVIEW, s1440p, RECORD, MAXIMUM and
      * ULTRA_MAXIMUM.
@@ -621,42 +1048,14 @@
     }
 
     /**
-     * Retrieves the display which has the max size among all displays.
-     */
-    private fun getMaxSizeDisplay(): Display {
-        val displays: Array<Display> = displayManager.displays
-        if (displays.size == 1) {
-            return displays[0]
-        }
-        var maxDisplay: Display? = null
-        var maxDisplaySize = -1
-        for (display: Display in displays) {
-            if (display.state != Display.STATE_OFF) {
-                val displaySize = Point()
-                display.getRealSize(displaySize)
-                if (displaySize.x * displaySize.y > maxDisplaySize) {
-                    maxDisplaySize = displaySize.x * displaySize.y
-                    maxDisplay = display
-                }
-            }
-        }
-        if (maxDisplay == null) {
-            throw IllegalArgumentException(
-                "No display can be found from the input display manager!"
-            )
-        }
-        return maxDisplay
-    }
-
-    /**
      * Once the stream resource is occupied by one use case, it will impact the other use cases.
      * Therefore, we need to define the priority for stream resource usage. For the use cases
      * with the higher priority, we will try to find the best one for them in priority as
      * possible.
      */
     private fun getUseCasesPriorityOrder(newUseCaseConfigs: List<UseCaseConfig<*>>): List<Int> {
-        val priorityOrder: MutableList<Int> = ArrayList()
-        val priorityValueList: MutableList<Int> = ArrayList()
+        val priorityOrder: MutableList<Int> = mutableListOf()
+        val priorityValueList: MutableList<Int> = mutableListOf()
         for (config in newUseCaseConfigs) {
             val priority = config.getSurfaceOccupancyPriority(0)
             if (!priorityValueList.contains(priority)) {
@@ -711,13 +1110,13 @@
 
         if (Build.VERSION.SDK_INT >= 23 && highResolutionIncluded) {
             val highResolutionOutputSizes = map?.getHighResolutionOutputSizes(imageFormat)
-            if (highResolutionOutputSizes != null && highResolutionOutputSizes.isNotEmpty()) {
+            if (!highResolutionOutputSizes.isNullOrEmpty()) {
                 maxHighResolutionSize =
                     Collections.max(highResolutionOutputSizes.asList(), compareSizesByArea)
             }
         }
 
-        return Collections.max(Arrays.asList(maxSize, maxHighResolutionSize), compareSizesByArea)
+        return Collections.max(listOf(maxSize, maxHighResolutionSize), compareSizesByArea)
     }
 
     /**
@@ -735,11 +1134,11 @@
         // supportedOutputSizes
         // for some use case
         require(totalArrangementsCount != 0) { "Failed to find supported resolutions." }
-        val allPossibleSizeArrangements: MutableList<MutableList<Size>> = ArrayList()
+        val allPossibleSizeArrangements: MutableList<MutableList<Size>> = mutableListOf()
 
         // Initialize allPossibleSizeArrangements for the following operations
         for (i in 0 until totalArrangementsCount) {
-            val sizeList: MutableList<Size> = ArrayList()
+            val sizeList: MutableList<Size> = mutableListOf()
             allPossibleSizeArrangements.add(sizeList)
         }
 
@@ -769,7 +1168,23 @@
         return allPossibleSizeArrangements
     }
 
-    companion object {
-        private const val TAG = "SupportedSurfaceCombination"
-    }
+    /**
+     * A collection of feature settings related to the Camera2 capabilities exposed by
+     * [CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES] and device features exposed
+     * by [PackageManager.hasSystemFeature].
+     *
+     * @param cameraMode The camera mode. This involves the following mapping of mode to features:
+     *           [CameraMode.CONCURRENT_CAMERA] -> [PackageManager.FEATURE_CAMERA_CONCURRENT]
+     *           [CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA] ->
+     *           [CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR]
+     * @param requiredMaxBitDepth The required maximum bit depth for any non-RAW stream attached to
+     *           the camera. A value of [DynamicRange.BIT_DEPTH_10_BIT] corresponds to the camera
+     *           capability
+     *           [CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT].
+     *
+     */
+    data class FeatureSettings(
+        @CameraMode.Mode val cameraMode: Int,
+        val requiredMaxBitDepth: Int
+    )
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt
new file mode 100644
index 0000000..4d1fddd
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompat.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.core.checkApi
+import androidx.camera.core.DynamicRange
+
+/**
+ * Helper for accessing features in DynamicRangeProfiles in a backwards compatible fashion.
+ */
+@RequiresApi(21)
+class DynamicRangeProfilesCompat internal constructor(
+    private val mImpl: DynamicRangeProfilesCompatImpl
+) {
+    /**
+     * Returns a set of supported [DynamicRange] that can be referenced in a single
+     * capture request.
+     *
+     * For example if a particular 10-bit output capable device returns (STANDARD,
+     * HLG10, HDR10) as result from calling [getSupportedDynamicRanges] and
+     * [DynamicRangeProfiles.getProfileCaptureRequestConstraints]
+     * returns (STANDARD, HLG10) when given an argument
+     * of STANDARD. This means that the corresponding camera device will only accept and process
+     * capture requests that reference outputs configured using HDR10 dynamic range or
+     * alternatively some combination of STANDARD and HLG10. However trying to queue capture
+     * requests to outputs that reference both HDR10 and STANDARD/HLG10 will result in
+     * IllegalArgumentException.
+     *
+     * The list will be empty in case there are no constraints for the given dynamic range.
+     *
+     * @param dynamicRange The dynamic range that will be checked for constraints
+     * @return non-modifiable set of dynamic ranges
+     * @throws IllegalArgumentException If the dynamic range argument is not within the set
+     * returned by [getSupportedDynamicRanges].
+     */
+    fun getDynamicRangeCaptureRequestConstraints(
+        dynamicRange: DynamicRange
+    ): Set<DynamicRange> {
+        return mImpl.getDynamicRangeCaptureRequestConstraints(dynamicRange)
+    }
+
+    /**
+     * Returns a set of supported dynamic ranges.
+     *
+     * @return a non-modifiable set of dynamic ranges.
+     */
+    fun getSupportedDynamicRanges(): Set<DynamicRange> {
+        return mImpl.getSupportedDynamicRanges()
+    }
+
+    /**
+     * Checks whether a given dynamic range is suitable for latency sensitive use cases.
+     *
+     * Due to internal lookahead logic, camera outputs configured with some dynamic range
+     * profiles may experience additional latency greater than 3 buffers. Using camera outputs
+     * with such dynamic ranges for latency sensitive use cases such as camera preview is not
+     * recommended. Dynamic ranges that have such extra streaming delay are typically utilized for
+     * scenarios such as offscreen video recording.
+     *
+     * @param dynamicRange The dynamic range to check for extra latency
+     * @return `true` if the given profile is not suitable for latency sensitive use cases,
+     * `false` otherwise.
+     * @throws IllegalArgumentException If the dynamic range argument is not within the set
+     * returned by [getSupportedDynamicRanges].
+     */
+    fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
+        return mImpl.isExtraLatencyPresent(dynamicRange)
+    }
+
+    /**
+     * Returns the underlying framework
+     * [DynamicRangeProfiles].
+     *
+     * @return the underlying [DynamicRangeProfiles] or
+     * `null` if the device doesn't support 10 bit dynamic range.
+     */
+    @RequiresApi(33)
+    fun toDynamicRangeProfiles(): DynamicRangeProfiles? {
+        checkApi(
+            33, "DynamicRangesCompat can only be " +
+                "converted to DynamicRangeProfiles on API 33 or higher."
+        )
+        return mImpl.unwrap()
+    }
+
+    internal interface DynamicRangeProfilesCompatImpl {
+        fun getDynamicRangeCaptureRequestConstraints(
+            dynamicRange: DynamicRange
+        ): Set<DynamicRange>
+
+        fun getSupportedDynamicRanges(): Set<DynamicRange>
+
+        fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean
+        fun unwrap(): DynamicRangeProfiles?
+    }
+
+    companion object {
+        /**
+         * Returns a [DynamicRangeProfilesCompat] using the capabilities derived from the provided
+         * characteristics.
+         *
+         * @param cameraMetadata the metaData used to derive dynamic range information.
+         * @return a [DynamicRangeProfilesCompat] object.
+         */
+        fun fromCameraMetaData(
+            cameraMetadata: CameraMetadata
+        ): DynamicRangeProfilesCompat {
+            var rangesCompat: DynamicRangeProfilesCompat? = null
+            if (Build.VERSION.SDK_INT >= 33) {
+                rangesCompat = toDynamicRangesCompat(
+                    cameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES]
+                )
+            }
+            return rangesCompat ?: DynamicRangeProfilesCompatBaseImpl.COMPAT_INSTANCE
+        }
+
+        /**
+         * Creates an instance from a framework [DynamicRangeProfiles]
+         * object.
+         *
+         * @param dynamicRangeProfiles a [DynamicRangeProfiles].
+         * @return an equivalent [DynamicRangeProfilesCompat] object.
+         */
+        @RequiresApi(33)
+        fun toDynamicRangesCompat(
+            dynamicRangeProfiles: DynamicRangeProfiles?
+        ): DynamicRangeProfilesCompat? {
+            if (dynamicRangeProfiles == null) {
+                return null
+            }
+            checkApi(
+                33, "DynamicRangeProfiles can only " +
+                    "be converted to DynamicRangesCompat on API 33 or higher."
+            )
+            return DynamicRangeProfilesCompat(
+                DynamicRangeProfilesCompatApi33Impl(
+                    dynamicRangeProfiles
+                )
+            )
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt
new file mode 100644
index 0000000..12a5687
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatApi33Impl.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.params.DynamicRangeProfiles
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.internal.DynamicRangeConversions
+import androidx.camera.core.DynamicRange
+import java.util.Collections
+
+@RequiresApi(33)
+internal class DynamicRangeProfilesCompatApi33Impl(
+    private val dynamicRangeProfiles: DynamicRangeProfiles
+) : DynamicRangeProfilesCompat.DynamicRangeProfilesCompatImpl {
+
+    override fun getDynamicRangeCaptureRequestConstraints(
+        dynamicRange: DynamicRange
+    ): Set<DynamicRange> {
+        val dynamicRangeProfile = dynamicRangeToFirstSupportedProfile(dynamicRange)
+        require(dynamicRangeProfile != null) {
+            "DynamicRange is not supported: $dynamicRange"
+        }
+        return profileSetToDynamicRangeSet(
+            dynamicRangeProfiles.getProfileCaptureRequestConstraints(dynamicRangeProfile)
+        )
+    }
+
+    override fun getSupportedDynamicRanges() = profileSetToDynamicRangeSet(
+        dynamicRangeProfiles.supportedProfiles
+    )
+
+    override fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
+        val dynamicRangeProfile = dynamicRangeToFirstSupportedProfile(dynamicRange)
+        require(
+            dynamicRangeProfile != null
+        ) {
+            "DynamicRange is not supported: $dynamicRange"
+        }
+        return dynamicRangeProfiles.isExtraLatencyPresent(dynamicRangeProfile)
+    }
+
+    override fun unwrap() = dynamicRangeProfiles
+
+    private fun dynamicRangeToFirstSupportedProfile(dynamicRange: DynamicRange) =
+        DynamicRangeConversions.dynamicRangeToFirstSupportedProfile(
+            dynamicRange,
+            dynamicRangeProfiles
+        )
+
+    private fun profileToDynamicRange(profile: Long): DynamicRange {
+        val result = DynamicRangeConversions.profileToDynamicRange(
+            profile
+        )
+        require(result != null) {
+            "Dynamic range profile cannot be converted to a DynamicRange object: $profile"
+        }
+        return result
+    }
+
+    private fun profileSetToDynamicRangeSet(profileSet: Set<Long>): Set<DynamicRange> {
+        if (profileSet.isEmpty()) {
+            return emptySet()
+        }
+        val dynamicRangeSet: MutableSet<DynamicRange> = mutableSetOf()
+        for (profile in profileSet) {
+            dynamicRangeSet.add(profileToDynamicRange(profile))
+        }
+        return Collections.unmodifiableSet(dynamicRangeSet)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt
new file mode 100644
index 0000000..b9a0cad
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatBaseImpl.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.params.DynamicRangeProfiles
+import androidx.annotation.RequiresApi
+import androidx.camera.core.DynamicRange
+import androidx.core.util.Preconditions
+
+@RequiresApi(21)
+internal class DynamicRangeProfilesCompatBaseImpl :
+    DynamicRangeProfilesCompat.DynamicRangeProfilesCompatImpl {
+    override fun getDynamicRangeCaptureRequestConstraints(
+        dynamicRange: DynamicRange
+    ): Set<DynamicRange> {
+        Preconditions.checkArgument(
+            DynamicRange.SDR == dynamicRange,
+            "DynamicRange is not supported: $dynamicRange"
+        )
+        return SDR_ONLY
+    }
+
+    override fun getSupportedDynamicRanges(): Set<DynamicRange> {
+        return SDR_ONLY
+    }
+
+    override fun isExtraLatencyPresent(dynamicRange: DynamicRange): Boolean {
+        Preconditions.checkArgument(
+            DynamicRange.SDR == dynamicRange,
+            "DynamicRange is not supported: $dynamicRange"
+        )
+        return false
+    }
+
+    override fun unwrap(): DynamicRangeProfiles? {
+        return null
+    }
+
+    companion object {
+        val COMPAT_INSTANCE: DynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat(DynamicRangeProfilesCompatBaseImpl())
+        private val SDR_ONLY = setOf(DynamicRange.SDR)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt
index 5b963d5..00ebe44 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompat.kt
@@ -136,6 +136,10 @@
         return outputSizes?.clone()
     }
 
+    fun getOutputMinFrameDuration(format: Int, size: Size?): Long? {
+        return impl.getOutputMinFrameDuration(format, size)
+    }
+
     /**
      * Returns the [StreamConfigurationMap] represented by this object.
      */
@@ -147,6 +151,7 @@
         fun getOutputSizes(format: Int): Array<Size>?
         fun <T> getOutputSizes(klass: Class<T>): Array<Size>?
         fun getHighResolutionOutputSizes(format: Int): Array<Size>?
+        fun getOutputMinFrameDuration(format: Int, size: Size?): Long?
 
         /**
          * Returns the underlying [StreamConfigurationMap] instance.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
index b5bfa26..fef2df7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/StreamConfigurationMapCompatBaseImpl.kt
@@ -50,6 +50,10 @@
         return null
     }
 
+    override fun getOutputMinFrameDuration(format: Int, size: Size?): Long? {
+        return streamConfigurationMap?.getOutputMinFrameDuration(format, size)
+    }
+
     override fun unwrap(): StreamConfigurationMap? {
         return streamConfigurationMap
     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 84bd53e..88ff62b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -43,6 +43,7 @@
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
 import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
+import androidx.camera.core.DynamicRange
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.CameraInternal
 import androidx.camera.core.impl.CameraMode
@@ -457,7 +458,12 @@
             )
         }
 
-        return supportedSurfaceCombination.checkSupported(CameraMode.DEFAULT, surfaceConfigs)
+        return supportedSurfaceCombination.checkSupported(
+            SupportedSurfaceCombination.FeatureSettings(
+                CameraMode.DEFAULT,
+                DynamicRange.BIT_DEPTH_8_BIT
+            ), surfaceConfigs
+        )
     }
 
     private fun Collection<UseCase>.surfaceCount(): Int =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeConversions.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeConversions.kt
new file mode 100644
index 0000000..090e587
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeConversions.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.internal
+
+import android.hardware.camera2.params.DynamicRangeProfiles
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.camera.core.DynamicRange
+
+/**
+ * Utilities for converting between [DynamicRange] and profiles from
+ * [DynamicRangeProfiles].
+ */
+@RequiresApi(33)
+internal object DynamicRangeConversions {
+    private val PROFILE_TO_DR_MAP: MutableMap<Long, DynamicRange> = mutableMapOf()
+    private val DR_TO_PROFILE_MAP: MutableMap<DynamicRange?, List<Long>> = mutableMapOf()
+
+    init {
+        // SDR
+        PROFILE_TO_DR_MAP[DynamicRangeProfiles.STANDARD] = DynamicRange.SDR
+        DR_TO_PROFILE_MAP[DynamicRange.SDR] = listOf(DynamicRangeProfiles.STANDARD)
+
+        // HLG
+        PROFILE_TO_DR_MAP[DynamicRangeProfiles.HLG10] = DynamicRange.HLG_10_BIT
+        DR_TO_PROFILE_MAP[PROFILE_TO_DR_MAP[DynamicRangeProfiles.HLG10]] =
+            listOf(DynamicRangeProfiles.HLG10)
+
+        // HDR10
+        PROFILE_TO_DR_MAP[DynamicRangeProfiles.HDR10] = DynamicRange.HDR10_10_BIT
+        DR_TO_PROFILE_MAP[DynamicRange.HDR10_10_BIT] = listOf(DynamicRangeProfiles.HDR10)
+
+        // HDR10+
+        PROFILE_TO_DR_MAP[DynamicRangeProfiles.HDR10_PLUS] = DynamicRange.HDR10_PLUS_10_BIT
+        DR_TO_PROFILE_MAP[DynamicRange.HDR10_PLUS_10_BIT] = listOf(DynamicRangeProfiles.HDR10_PLUS)
+
+        // Dolby Vision 10-bit
+        // A list of the Camera2 10-bit dolby vision profiles ordered by priority. Any API that
+        // takes a DynamicRange with dolby vision encoding will attempt to convert to these
+        // profiles in order, using the first one that is supported. We will need to add a
+        // mechanism for choosing between these
+        val dolbyVision10BitProfilesOrdered = listOf(
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM_PO,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF_PO
+        )
+        for (profile in dolbyVision10BitProfilesOrdered) {
+            PROFILE_TO_DR_MAP[profile] = DynamicRange.DOLBY_VISION_10_BIT
+        }
+        DR_TO_PROFILE_MAP[DynamicRange.DOLBY_VISION_10_BIT] =
+            dolbyVision10BitProfilesOrdered
+
+        // Dolby vision 8-bit
+        val dolbyVision8BitProfilesOrdered = listOf(
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM_PO,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF_PO
+        )
+        for (profile in dolbyVision8BitProfilesOrdered) {
+            PROFILE_TO_DR_MAP[profile] = DynamicRange.DOLBY_VISION_8_BIT
+        }
+        DR_TO_PROFILE_MAP[DynamicRange.DOLBY_VISION_8_BIT] =
+            dolbyVision8BitProfilesOrdered
+    }
+
+    /**
+     * Converts Camera2 dynamic range profile constants to [DynamicRange].
+     */
+    @DoNotInline
+    fun profileToDynamicRange(profile: Long): DynamicRange? {
+        return PROFILE_TO_DR_MAP[profile]
+    }
+
+    /**
+     * Converts a [DynamicRange] to a Camera2 dynamic range profile.
+     *
+     *
+     * For dynamic ranges which can resolve to multiple profiles, the first supported profile
+     * from the passed [android.hardware.camera2.params.DynamicRangeProfiles] will be
+     * returned. The order in which profiles are checked for support is internally defined.
+     *
+     *
+     * This will only return profiles for fully defined dynamic ranges. For instance, if the
+     * format returned by [DynamicRange.getEncoding] is
+     * [DynamicRange.ENCODING_HDR_UNSPECIFIED], this will return `null`.
+     */
+    @DoNotInline
+    fun dynamicRangeToFirstSupportedProfile(
+        dynamicRange: DynamicRange,
+        dynamicRangeProfiles: DynamicRangeProfiles
+    ): Long? {
+        val orderedProfiles = DR_TO_PROFILE_MAP[dynamicRange]
+        if (orderedProfiles != null) {
+            val supportedList = dynamicRangeProfiles.supportedProfiles
+            for (profile in orderedProfiles) {
+                if (supportedList.contains(profile)) {
+                    return profile
+                }
+            }
+        }
+
+        // No profile supported
+        return null
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt
new file mode 100644
index 0000000..947d31c
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeResolver.kt
@@ -0,0 +1,479 @@
+package androidx.camera.camera2.pipe.integration.internal
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.integration.compat.DynamicRangeProfilesCompat
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.impl.AttachedSurfaceInfo
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.core.util.Preconditions
+
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+class DynamicRangeResolver(val cameraMetadata: CameraMetadata) {
+    private val is10BitSupported: Boolean
+    private val dynamicRangesInfo: DynamicRangeProfilesCompat
+
+    init {
+        val availableCapabilities: IntArray? =
+            cameraMetadata[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]
+        is10BitSupported =
+            availableCapabilities?.contains(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
+            )
+                ?: false
+        dynamicRangesInfo = DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+    }
+
+    /**
+     * Returns whether 10-bit dynamic ranges are supported on this device.
+     */
+    fun is10BitDynamicRangeSupported(): Boolean = is10BitSupported
+
+    /**
+     * Returns a set of supported dynamic ranges for the dynamic ranges requested by the list of
+     * attached and new use cases.
+     *
+     *
+     * If a new use case requests a dynamic range that isn't supported, an
+     * IllegalArgumentException will be thrown.
+     */
+    fun resolveAndValidateDynamicRanges(
+        existingSurfaces: List<AttachedSurfaceInfo>,
+        newUseCaseConfigs: List<UseCaseConfig<*>>,
+        useCasePriorityOrder: List<Int>
+    ): Map<UseCaseConfig<*>, DynamicRange> {
+        // Create an ordered set of already-attached surface's dynamic ranges. These are assumed
+        // to be valid since they are already attached.
+        val orderedExistingDynamicRanges = mutableSetOf<DynamicRange>()
+        for (asi in existingSurfaces) {
+            orderedExistingDynamicRanges.add(asi.dynamicRange)
+        }
+
+        // Get the supported dynamic ranges from the device
+        val supportedDynamicRanges = dynamicRangesInfo.getSupportedDynamicRanges()
+
+        // Collect initial dynamic range constraints. This set will potentially shrink as we add
+        // more dynamic ranges. We start with the initial set of supported dynamic ranges to
+        // denote no constraints.
+        val combinedConstraints = supportedDynamicRanges.toMutableSet()
+        for (dynamicRange in orderedExistingDynamicRanges) {
+            updateConstraints(combinedConstraints, dynamicRange, dynamicRangesInfo)
+        }
+
+        // We want to resolve and validate dynamic ranges in the following order:
+        // 1. First validate fully defined dynamic ranges. No resolving is required here.
+        // 2. Resolve and validate partially defined dynamic ranges, such as HDR_UNSPECIFIED or
+        // dynamic ranges with concrete encodings but BIT_DEPTH_UNSPECIFIED. We can now potentially
+        // infer a dynamic range based on constraints of the fully defined dynamic ranges or
+        // the list of supported HDR dynamic ranges.
+        // 3. Finally, resolve and validate UNSPECIFIED dynamic ranges. These will resolve
+        // to dynamic ranges from the first 2 groups, or fall back to SDR if no other dynamic
+        // ranges are defined.
+        //
+        // To accomplish this, we need to partition the use cases into 3 categories.
+        val orderedFullyDefinedUseCaseConfigs: MutableList<UseCaseConfig<*>> = mutableListOf()
+        val orderedPartiallyDefinedUseCaseConfigs: MutableList<UseCaseConfig<*>> = mutableListOf()
+        val orderedUndefinedUseCaseConfigs: MutableList<UseCaseConfig<*>> = mutableListOf()
+        for (priorityIdx in useCasePriorityOrder) {
+            val config = newUseCaseConfigs[priorityIdx]
+            val requestedDynamicRange = config.dynamicRange
+            if (isFullyUnspecified(requestedDynamicRange)
+            ) {
+                orderedUndefinedUseCaseConfigs.add(config)
+            } else if (isPartiallySpecified(requestedDynamicRange)
+            ) {
+                orderedPartiallyDefinedUseCaseConfigs.add(config)
+            } else {
+                orderedFullyDefinedUseCaseConfigs.add(config)
+            }
+        }
+        val resolvedDynamicRanges: MutableMap<UseCaseConfig<*>, DynamicRange> = mutableMapOf()
+        // Keep track of new dynamic ranges for more fine-grained error messages in exceptions.
+        // This allows us to differentiate between dynamic ranges from already-attached use cases
+        // and requested dynamic ranges from newly added use cases.
+        val orderedNewDynamicRanges: MutableSet<DynamicRange> = mutableSetOf()
+        // Now resolve and validate all of the dynamic ranges in order of the 3 partitions form
+        // above.
+        val orderedUseCaseConfigs: MutableList<UseCaseConfig<*>> = mutableListOf()
+        orderedUseCaseConfigs.addAll(orderedFullyDefinedUseCaseConfigs)
+        orderedUseCaseConfigs.addAll(orderedPartiallyDefinedUseCaseConfigs)
+        orderedUseCaseConfigs.addAll(orderedUndefinedUseCaseConfigs)
+        for (config in orderedUseCaseConfigs) {
+            val resolvedDynamicRange: DynamicRange = resolveDynamicRangeAndUpdateConstraints(
+                supportedDynamicRanges, orderedExistingDynamicRanges,
+                orderedNewDynamicRanges, config, combinedConstraints
+            )
+            resolvedDynamicRanges[config] = resolvedDynamicRange
+            if (!orderedExistingDynamicRanges.contains(resolvedDynamicRange)) {
+                orderedNewDynamicRanges.add(resolvedDynamicRange)
+            }
+        }
+        return resolvedDynamicRanges
+    }
+
+    private fun resolveDynamicRangeAndUpdateConstraints(
+        supportedDynamicRanges: Set<DynamicRange?>,
+        orderedExistingDynamicRanges: Set<DynamicRange>,
+        orderedNewDynamicRanges: Set<DynamicRange>,
+        config: UseCaseConfig<*>,
+        outCombinedConstraints: MutableSet<DynamicRange>
+    ): DynamicRange {
+        val requestedDynamicRange = config.dynamicRange
+        val resolvedDynamicRange: DynamicRange? = resolveDynamicRange(
+            requestedDynamicRange,
+            outCombinedConstraints, orderedExistingDynamicRanges, orderedNewDynamicRanges,
+            config.targetName
+        )
+        if (resolvedDynamicRange != null) {
+            updateConstraints(outCombinedConstraints, resolvedDynamicRange, dynamicRangesInfo)
+        } else {
+            throw IllegalArgumentException(
+                "Unable to resolve supported " +
+                    "dynamic range. The dynamic range may not be supported on the device " +
+                    "or may not be allowed concurrently with other attached use cases.\n" +
+                    "Use case:\n" +
+                    "  ${config.targetName}\n" +
+                    "Requested dynamic range:\n" +
+                    "  $requestedDynamicRange\n" +
+                    "Supported dynamic ranges:\n" +
+                    "  $supportedDynamicRanges\n" +
+                    "Constrained set of concurrent dynamic ranges:\n" +
+                    "  $outCombinedConstraints",
+            )
+        }
+        return resolvedDynamicRange
+    }
+
+    /**
+     * Resolves the requested dynamic range into a fully specified dynamic range.
+     *
+     *
+     * This uses existing fully-specified dynamic ranges, new fully-specified dynamic ranges,
+     * dynamic range constraints and the list of supported dynamic ranges to exhaustively search
+     * for a dynamic range if the requested dynamic range is not fully specified, i.e., it has an
+     * UNSPECIFIED encoding or UNSPECIFIED bitrate.
+     *
+     *
+     * Any dynamic range returned will be validated to work according to the constraints and
+     * supported dynamic ranges provided.
+     *
+     *
+     * If no suitable dynamic range can be found, returns `null`.
+     */
+    private fun resolveDynamicRange(
+        requestedDynamicRange: DynamicRange,
+        combinedConstraints: Set<DynamicRange>,
+        orderedExistingDynamicRanges: Set<DynamicRange>,
+        orderedNewDynamicRanges: Set<DynamicRange>,
+        rangeOwnerLabel: String
+    ): DynamicRange? {
+
+        // Dynamic range is already resolved if it is fully specified.
+        if (requestedDynamicRange.isFullySpecified) {
+            return if (combinedConstraints.contains(requestedDynamicRange)) {
+                requestedDynamicRange
+            } else null
+            // Requested dynamic range is full specified but unsupported. No need to continue
+            // trying to resolve.
+        }
+
+        // Explicitly handle the case of SDR with unspecified bit depth.
+        // SDR is only supported as 8-bit.
+        val requestedEncoding = requestedDynamicRange.encoding
+        val requestedBitDepth = requestedDynamicRange.bitDepth
+        if (requestedEncoding == DynamicRange.ENCODING_SDR &&
+            requestedBitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED
+        ) {
+            return if (combinedConstraints.contains(DynamicRange.SDR)) {
+                DynamicRange.SDR
+            } else null
+            // If SDR isn't supported, we can't resolve to any other dynamic range.
+        }
+
+        // First attempt to find another fully specified HDR dynamic range to resolve to from
+        // existing dynamic ranges
+        var resolvedDynamicRange = findSupportedHdrMatch(
+            requestedDynamicRange,
+            orderedExistingDynamicRanges, combinedConstraints
+        )
+        if (resolvedDynamicRange != null) {
+            Log.debug {
+                "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel " +
+                    "from existing attached surface.\n" +
+                    "$requestedDynamicRange\n->\n$resolvedDynamicRange"
+            }
+
+            return resolvedDynamicRange
+        }
+
+        // Attempt to find another fully specified HDR dynamic range to resolve to from
+        // new dynamic ranges
+        resolvedDynamicRange =
+            findSupportedHdrMatch(
+                requestedDynamicRange,
+                orderedNewDynamicRanges, combinedConstraints
+            )
+        if (resolvedDynamicRange != null) {
+            Log.debug {
+                "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel from " +
+                    "concurrently bound use case." +
+                    "\n$requestedDynamicRange\n->\n$resolvedDynamicRange"
+            }
+
+            return resolvedDynamicRange
+        }
+
+        // Now that we have checked existing HDR dynamic ranges, we must resolve fully unspecified
+        // and unspecified 8-bit dynamic ranges to SDR if it is supported. This ensures the
+        // default behavior for most use cases is to choose SDR when an HDR dynamic range isn't
+        // already present or explicitly requested.
+        if (canResolveWithinConstraints(
+                requestedDynamicRange, DynamicRange.SDR,
+                combinedConstraints
+            )
+        ) {
+            Log.debug {
+                "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel to " +
+                    "no compatible HDR dynamic ranges.\n$requestedDynamicRange\n" +
+                    "->\n${DynamicRange.SDR}"
+            }
+            return DynamicRange.SDR
+        }
+
+        // For unspecified HDR encodings (10-bit or unspecified bit depth), we have a
+        // couple options: the device recommended 10-bit encoding or the mandated HLG encoding.
+        if (requestedEncoding == DynamicRange.ENCODING_HDR_UNSPECIFIED &&
+            ((requestedBitDepth == DynamicRange.BIT_DEPTH_10_BIT ||
+                requestedBitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED))
+        ) {
+            val hdrDefaultRanges: MutableSet<DynamicRange> = mutableSetOf()
+
+            // Attempt to use the recommended 10-bit dynamic range
+            var recommendedRange: DynamicRange? = null
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                recommendedRange =
+                    Api33Impl.getRecommended10BitDynamicRange(
+                        cameraMetadata
+                    )
+                if (recommendedRange != null) {
+                    hdrDefaultRanges.add(recommendedRange)
+                }
+            }
+            // Attempt to fall back to HLG since it is a mandated required 10-bit
+            // dynamic range.
+            hdrDefaultRanges.add(DynamicRange.HLG_10_BIT)
+            resolvedDynamicRange =
+                findSupportedHdrMatch(
+                    requestedDynamicRange, hdrDefaultRanges, combinedConstraints
+                )
+            if (resolvedDynamicRange != null) {
+                Log.debug {
+                    "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel" +
+                        "from ${
+                            if ((resolvedDynamicRange == recommendedRange)) "recommended"
+                            else "required"
+                        } 10-bit supported dynamic range.\n" +
+                        "${requestedDynamicRange}\n" +
+                        "->\n" +
+                        "$resolvedDynamicRange"
+                }
+                return resolvedDynamicRange
+            }
+        }
+
+        // Finally, attempt to find an HDR dynamic range for HDR or 10-bit dynamic ranges from
+        // the constraints of the other validated dynamic ranges. If there are no other dynamic
+        // ranges, this should be the full list of supported dynamic ranges.
+        // The constraints are unordered, so it may not produce an "optimal" dynamic range. This
+        // works for 8-bit, 10-bit or partially specified HDR dynamic ranges.
+        for (candidateRange: DynamicRange in combinedConstraints) {
+            check(candidateRange.isFullySpecified) {
+                "Candidate dynamic range must be fully specified."
+            }
+
+            // Only consider HDR constraints
+            if ((candidateRange == DynamicRange.SDR)) {
+                continue
+            }
+            if (canResolveDynamicRange(
+                    requestedDynamicRange,
+                    candidateRange
+                )
+            ) {
+                Log.debug {
+                    "DynamicRangeResolver: Resolved dynamic range for use case $rangeOwnerLabel " +
+                        "from validated dynamic range constraints or supported HDR dynamic " +
+                        "ranges.\n$requestedDynamicRange\n->\n$candidateRange"
+                }
+                return candidateRange
+            }
+        }
+
+        // Unable to resolve dynamic range
+        return null
+    }
+
+    /**
+     * Updates the provided dynamic range constraints by combining them with the new constraints
+     * from the new dynamic range.
+     *
+     * @param combinedConstraints The constraints that will be updated. This set must not be empty.
+     * @param newDynamicRange     The new dynamic range for which we'll apply new constraints
+     * @param dynamicRangesInfo   Information about dynamic ranges to retrieve new constraints.
+     */
+    private fun updateConstraints(
+        combinedConstraints: MutableSet<DynamicRange>,
+        newDynamicRange: DynamicRange,
+        dynamicRangesInfo: DynamicRangeProfilesCompat
+    ) {
+        Preconditions.checkState(
+            combinedConstraints.isNotEmpty(), "Cannot update already-empty constraints."
+        )
+        val newConstraints =
+            dynamicRangesInfo.getDynamicRangeCaptureRequestConstraints(newDynamicRange)
+        if (newConstraints.isNotEmpty()) {
+            // Retain for potential exception message
+            val previousConstraints = combinedConstraints.toSet()
+            // Take the intersection of constraints
+            combinedConstraints.retainAll(newConstraints)
+            // This shouldn't happen if we're diligent about checking that dynamic range
+            // is within the existing constraints before attempting to call
+            // updateConstraints. If it happens, then the dynamic ranges are not mutually
+            // compatible.
+            require(combinedConstraints.isNotEmpty()) {
+                "Constraints of dynamic " +
+                    "range cannot be combined with existing constraints.\n" +
+                    "Dynamic range:\n" +
+                    "  $newDynamicRange\n" +
+                    "Constraints:\n" +
+                    "  $newConstraints\n" +
+                    "Existing constraints:\n" +
+                    "  $previousConstraints"
+            }
+        }
+    }
+
+    private fun findSupportedHdrMatch(
+        rangeToMatch: DynamicRange,
+        fullySpecifiedCandidateRanges: Collection<DynamicRange>,
+        constraints: Set<DynamicRange>
+    ): DynamicRange? {
+        // SDR can never match with HDR
+        if (rangeToMatch.encoding == DynamicRange.ENCODING_SDR) {
+            return null
+        }
+        for (candidateRange in fullySpecifiedCandidateRanges) {
+            val candidateEncoding = candidateRange.encoding
+            check(candidateRange.isFullySpecified) {
+                "Fully specified DynamicRange must have fully defined encoding."
+            }
+            if (candidateEncoding == DynamicRange.ENCODING_SDR) {
+                // Only consider HDR encodings
+                continue
+            }
+            if (canResolveWithinConstraints(
+                    rangeToMatch,
+                    candidateRange,
+                    constraints
+                )
+            ) {
+                return candidateRange
+            }
+        }
+        return null
+    }
+
+    /**
+     * Returns `true` if the dynamic range is ENCODING_UNSPECIFIED and BIT_DEPTH_UNSPECIFIED.
+     */
+    private fun isFullyUnspecified(dynamicRange: DynamicRange): Boolean {
+        return (dynamicRange == DynamicRange.UNSPECIFIED)
+    }
+
+    /**
+     * Returns `true` if the dynamic range has an unspecified HDR encoding, a concrete
+     * encoding with unspecified bit depth, or a concrete bit depth.
+     */
+    private fun isPartiallySpecified(dynamicRange: DynamicRange): Boolean {
+        return dynamicRange.encoding == DynamicRange.ENCODING_HDR_UNSPECIFIED ||
+            (dynamicRange.encoding != DynamicRange.ENCODING_UNSPECIFIED &&
+                dynamicRange.bitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED) ||
+            (dynamicRange.encoding == DynamicRange.ENCODING_UNSPECIFIED &&
+                dynamicRange.bitDepth != DynamicRange.BIT_DEPTH_UNSPECIFIED)
+    }
+
+    /**
+     * Returns `true` if the test dynamic range can resolve to the candidate, fully specified
+     * dynamic range, taking into account constraints.
+     *
+     *
+     * A range can resolve if test fields are unspecified and appropriately match the fields
+     * of the fully specified dynamic range, or the test fields exactly match the fields of
+     * the fully specified dynamic range.
+     */
+    private fun canResolveWithinConstraints(
+        rangeToResolve: DynamicRange,
+        candidateRange: DynamicRange,
+        constraints: Set<DynamicRange>
+    ): Boolean {
+        if (!constraints.contains(candidateRange)) {
+            Log.debug {
+                "DynamicRangeResolver: Candidate Dynamic range is not within constraints.\n" +
+                    "Dynamic range to resolve:\n" +
+                    "  $rangeToResolve\n" +
+                    "Candidate dynamic range:\n" +
+                    "  $candidateRange"
+            }
+            return false
+        }
+        return canResolveDynamicRange(rangeToResolve, candidateRange)
+    }
+
+    /**
+     * Returns `true` if the test dynamic range can resolve to the fully specified dynamic
+     * range.
+     *
+     *
+     * A range can resolve if test fields are unspecified and appropriately match the fields
+     * of the fully specified dynamic range, or the test fields exactly match the fields of
+     * the fully specified dynamic range.
+     */
+    private fun canResolveDynamicRange(
+        testRange: DynamicRange,
+        fullySpecifiedRange: DynamicRange
+    ): Boolean {
+        check(fullySpecifiedRange.isFullySpecified) {
+            "Fully specified range $fullySpecifiedRange not actually fully specified."
+        }
+        if ((testRange.encoding == DynamicRange.ENCODING_HDR_UNSPECIFIED &&
+                fullySpecifiedRange.encoding == DynamicRange.ENCODING_SDR)
+        ) {
+            return false
+        }
+        return if ((testRange.encoding != DynamicRange.ENCODING_HDR_UNSPECIFIED
+                ) && (testRange.encoding != DynamicRange.ENCODING_UNSPECIFIED
+                ) && (testRange.encoding != fullySpecifiedRange.encoding)
+        ) {
+            false
+        } else (testRange.bitDepth == DynamicRange.BIT_DEPTH_UNSPECIFIED ||
+            testRange.bitDepth == fullySpecifiedRange.bitDepth)
+    }
+
+    @RequiresApi(33)
+    internal object Api33Impl {
+        @DoNotInline
+        fun getRecommended10BitDynamicRange(
+            cameraMetadata: CameraMetadata
+        ): DynamicRange? {
+            val recommendedProfile = cameraMetadata[
+                CameraCharacteristics.REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE]
+            return if (recommendedProfile != null) {
+                DynamicRangeConversions.profileToDynamicRange(recommendedProfile)
+            } else null
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 4c76b49..6b4e41a 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -20,8 +20,11 @@
 import android.graphics.ImageFormat
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES
+import android.hardware.camera2.CameraCharacteristics.REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE
 import android.hardware.camera2.CameraManager
 import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.params.DynamicRangeProfiles
 import android.hardware.camera2.params.StreamConfigurationMap
 import android.media.CamcorderProfile.QUALITY_1080P
 import android.media.CamcorderProfile.QUALITY_2160P
@@ -42,6 +45,17 @@
 import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getLimitedSupportedCombinationList
 import androidx.camera.camera2.pipe.integration.adapter.GuaranteedConfigurationsUtil.getRAWSupportedCombinationList
 import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_10B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_8B_SDR_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_8B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_8B_UNCONSTRAINED_HLG10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HDR10_HDR10_PLUS_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HDR10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_SDR_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.LATENCY_NONE
 import androidx.camera.camera2.pipe.testing.FakeCameraBackend
 import androidx.camera.camera2.pipe.testing.FakeCameraDevices
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
@@ -49,6 +63,7 @@
 import androidx.camera.core.CameraSelector.LensFacing
 import androidx.camera.core.CameraX
 import androidx.camera.core.CameraXConfig
+import androidx.camera.core.DynamicRange
 import androidx.camera.core.UseCase
 import androidx.camera.core.concurrent.CameraCoordinator
 import androidx.camera.core.impl.AttachedSurfaceInfo
@@ -57,6 +72,8 @@
 import androidx.camera.core.impl.EncoderProfilesProxy
 import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
 import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.ImageInputConfig
+import androidx.camera.core.impl.StreamSpec
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
@@ -85,6 +102,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 import org.robolectric.RobolectricTestRunner
@@ -197,7 +215,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -214,7 +235,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -231,7 +255,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -248,7 +275,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -265,7 +295,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -282,7 +315,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -299,7 +335,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -316,7 +355,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -333,7 +375,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isFalse()
         }
@@ -353,7 +398,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -373,7 +421,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -393,7 +444,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -413,7 +467,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -430,7 +487,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.DEFAULT, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -450,7 +510,10 @@
         for (combination in combinationList) {
             val isSupported =
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.CONCURRENT_CAMERA, combination.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.CONCURRENT_CAMERA,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), combination.surfaceConfigList
                 )
             assertThat(isSupported).isTrue()
         }
@@ -474,7 +537,10 @@
         GuaranteedConfigurationsUtil.getUltraHighResolutionSupportedCombinationList().forEach {
             assertThat(
                 supportedSurfaceCombination.checkSupported(
-                    CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA, it.surfaceConfigList
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA,
+                        DynamicRange.BIT_DEPTH_8_BIT
+                    ), it.surfaceConfigList
                 )
             ).isTrue()
         }
@@ -1460,9 +1526,19 @@
         attachedSurfaceInfoList: List<AttachedSurfaceInfo> = emptyList(),
         hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
         capabilities: IntArray? = null,
-        compareWithAtMost: Boolean = false
+        compareWithAtMost: Boolean = false,
+        compareExpectedFps: Range<Int>? = null,
+        cameraMode: Int = CameraMode.DEFAULT,
+        useCasesExpectedDynamicRangeMap: Map<UseCase, DynamicRange> = emptyMap(),
+        dynamicRangeProfiles: DynamicRangeProfiles? = null,
+        default10BitProfile: Long? = null,
     ) {
-        setupCamera(hardwareLevel = hardwareLevel, capabilities = capabilities)
+        setupCamera(
+            hardwareLevel = hardwareLevel,
+            capabilities = capabilities,
+            dynamicRangeProfiles = dynamicRangeProfiles,
+            default10BitProfile = default10BitProfile
+        )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, fakeCameraMetadata,
             mockEncoderProfilesAdapter
@@ -1472,7 +1548,7 @@
         val useCaseConfigToOutputSizesMap =
             getUseCaseConfigToOutputSizesMap(useCaseConfigMap.values.toList())
         val suggestedStreamSpecs = supportedSurfaceCombination.getSuggestedStreamSpecifications(
-            CameraMode.DEFAULT,
+            cameraMode,
             attachedSurfaceInfoList,
             useCaseConfigToOutputSizesMap
         ).first
@@ -1485,6 +1561,19 @@
             } else {
                 assertThat(sizeIsAtMost(resultSize, expectedSize)).isTrue()
             }
+
+            compareExpectedFps?.let { _ ->
+                assertThat(
+                    suggestedStreamSpecs[useCaseConfigMap[it]]!!.expectedFrameRateRange
+                ).isEqualTo(compareExpectedFps)
+            }
+        }
+
+        useCasesExpectedDynamicRangeMap.keys.forEach {
+            val resultDynamicRange = suggestedStreamSpecs[useCaseConfigMap[it]]!!.dynamicRange
+            val expectedDynamicRange = useCasesExpectedDynamicRangeMap[it]
+
+            assertThat(resultDynamicRange).isEqualTo(expectedDynamicRange)
         }
     }
 
@@ -1519,6 +1608,1165 @@
 
     // //////////////////////////////////////////////////////////////////////////////////////////
     //
+    // StreamSpec selection tests for DynamicRange
+    //
+    // //////////////////////////////////////////////////////////////////////////////////////////
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun check10BitDynamicRangeCombinationsSupported() {
+        setupCamera(
+            capabilities = intArrayOf(
+                CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
+            )
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, fakeCameraMetadata,
+            mockEncoderProfilesAdapter
+        )
+
+        GuaranteedConfigurationsUtil.get10BitSupportedCombinationList().forEach {
+            assertThat(
+                supportedSurfaceCombination.checkSupported(
+                    SupportedSurfaceCombination.FeatureSettings(
+                        CameraMode.DEFAULT,
+                        DynamicRange.BIT_DEPTH_10_BIT
+                    ),
+                    it.surfaceConfigList
+                )
+            ).isTrue()
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun getSupportedStreamSpecThrows_whenUsingUnsupportedDynamicRange() {
+        val useCase =
+            createUseCase(
+                UseCaseConfigFactory.CaptureType.PREVIEW,
+                dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+            )
+        val useCaseExpectedResultMap = mapOf(
+            useCase to Size(0, 0) // Should throw before verifying size
+        )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedResultMap,
+                capabilities =
+                intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun getSupportedStreamSpecThrows_whenUsingConcurrentCameraAndSupported10BitRange() {
+        Shadows.shadowOf(context.packageManager).setSystemFeature(
+            PackageManager.FEATURE_CAMERA_CONCURRENT, true
+        )
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to Size(0, 0) // Should throw before verifying size
+        )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                cameraMode = CameraMode.CONCURRENT_CAMERA,
+                dynamicRangeProfiles = HLG10_CONSTRAINED
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun getSupportedStreamSpecThrows_whenUsingUltraHighResolutionAndSupported10BitRange() {
+        Shadows.shadowOf(context.packageManager).setSystemFeature(
+            PackageManager.FEATURE_CAMERA_CONCURRENT, true
+        )
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to Size(0, 0) // Should throw before verifying size
+        )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                cameraMode = CameraMode.ULTRA_HIGH_RESOLUTION_CAMERA,
+                dynamicRangeProfiles = HLG10_CONSTRAINED
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsHlg_dueToMandatory10Bit() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.HLG_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HLG10_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsHdr10_dueToRecommended10BitDynamicRange() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.HDR10_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HDR10_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            default10BitProfile = DynamicRangeProfiles.HDR10
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision8_dueToSupportedDynamicRanges() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_HDR_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_8_BIT
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_UNCONSTRAINED,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision8_fromUnspecifiedBitDepth() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_DOLBY_VISION,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_UNCONSTRAINED,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision10_fromUnspecifiedBitDepth() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_DOLBY_VISION,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_10B_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision8_fromUnspecifiedHdrWithUnspecifiedBitDepth() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_HDR_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_UNCONSTRAINED,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision10_fromUnspecifiedHdrWithUnspecifiedBitDepth() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_HDR_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.DOLBY_VISION_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            default10BitProfile = DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision8_withUndefinedBitDepth_andFullyDefinedHlg10() {
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_DOLBY_VISION,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            videoUseCase to DynamicRange.HLG_10_BIT,
+            previewUseCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_UNCONSTRAINED_HLG10_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_returnsDolbyVision10_dueToDynamicRangeConstraints() {
+        // VideoCapture partially defined dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        // Preview fully defined dynamic range
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.DOLBY_VISION_8_BIT,
+
+            )
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            videoUseCase to DynamicRange.DOLBY_VISION_10_BIT,
+            previewUseCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = DOLBY_VISION_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesUnspecifiedDynamicRange_afterPartiallySpecifiedDynamicRange() {
+        // VideoCapture partially defined dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HDR_UNSPECIFIED_10_BIT
+        )
+        // Preview unspecified dynamic range
+        val previewUseCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            previewUseCase to DynamicRange.HLG_10_BIT,
+            videoUseCase to DynamicRange.HLG_10_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HLG10_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesUnspecifiedDynamicRangeToSdr() {
+        // Preview unspecified dynamic range
+        val useCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.SDR
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HLG10_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Test
+    fun dynamicRangeResolver_resolvesToSdr_when10BitNotSupported() {
+        // Preview unspecified dynamic range
+        val useCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.SDR
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Test
+    fun dynamicRangeResolver_resolvesToSdr8Bit_whenSdrWithUnspecifiedBitDepthProvided() {
+        // Preview unspecified dynamic range
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_SDR,
+                DynamicRange.BIT_DEPTH_UNSPECIFIED
+            )
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            useCase to maximumSize
+        )
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.SDR
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesUnspecified8Bit_usingConstraintsFrom10BitDynamicRange() {
+        // VideoCapture has 10-bit HDR range with constraint for 8-bit non-SDR range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.DOLBY_VISION_10_BIT
+        )
+        // Preview unspecified encoding but 8-bit bit depth
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_8_BIT
+            )
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            videoUseCase to DynamicRange.DOLBY_VISION_10_BIT,
+            previewUseCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            dynamicRangeProfiles = DOLBY_VISION_CONSTRAINED
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesToSdr_forUnspecified8Bit_whenNoOtherDynamicRangesPresent() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange(
+                DynamicRange.ENCODING_UNSPECIFIED,
+                DynamicRange.BIT_DEPTH_8_BIT
+            )
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            useCase to maximumSize
+        )
+
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            useCase to DynamicRange.SDR
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_SDR_UNCONSTRAINED
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeResolver_resolvesUnspecified8BitToDolbyVision8Bit_whenAlreadyPresent() {
+        // VideoCapture fully resolved Dolby Vision 8-bit
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.DOLBY_VISION_8_BIT
+        )
+        // Preview unspecified encoding / 8-bit
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.UNSPECIFIED
+        )
+
+        // Since there are no 10-bit dynamic ranges, the 10-bit resolution table isn't used.
+        // Instead, this will use the camera default LIMITED table which is limited to preview
+        // size for 2 PRIV use cases.
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to previewSize,
+            previewUseCase to previewSize
+        )
+
+        val useCaseExpectedDynamicRangeMap = mapOf(
+            videoUseCase to DynamicRange.DOLBY_VISION_8_BIT,
+            previewUseCase to DynamicRange.DOLBY_VISION_8_BIT
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            useCasesExpectedDynamicRangeMap = useCaseExpectedDynamicRangeMap,
+            dynamicRangeProfiles = DOLBY_VISION_8B_SDR_UNCONSTRAINED
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun tenBitTable_isUsed_whenAttaching10BitUseCaseToAlreadyAttachedSdrUseCases() {
+        // JPEG use case can't be attached with an existing PRIV + YUV in the 10-bit tables
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            // Size would be valid for LIMITED table
+            useCase to recordSize
+        )
+        // existing surfaces (Preview + ImageAnalysis)
+        val attachedPreview = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.PRIVATE,
+            previewSize,
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+        val attachedAnalysis = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.YUV,
+                SurfaceConfig.ConfigSize.RECORD
+            ),
+            ImageFormat.YUV_420_888,
+            recordSize,
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                attachedSurfaceInfoList = listOf(attachedPreview, attachedAnalysis),
+                // LIMITED allows this combination, but 10-bit table does not
+                hardwareLevel = CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+                dynamicRangeProfiles = HLG10_SDR_CONSTRAINED,
+                capabilities =
+                intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun dynamicRangeConstraints_causeAutoResolutionToThrow() {
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            // Size would be valid for 10-bit table within constraints
+            useCase to recordSize
+        )
+        // existing surfaces (PRIV + PRIV)
+        val attachedPriv1 = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.PRIVATE,
+            previewSize,
+            DynamicRange.HDR10_10_BIT,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+        val attachedPriv2 = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.RECORD
+            ),
+            ImageFormat.YUV_420_888,
+            recordSize,
+            DynamicRange.HDR10_PLUS_10_BIT,
+            listOf(UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+
+        // These constraints say HDR10 and HDR10_PLUS can be combined, but not HLG
+        val constraintsTable =
+            DynamicRangeProfiles(
+                longArrayOf(
+                    DynamicRangeProfiles.HLG10,
+                    DynamicRangeProfiles.HLG10,
+                    LATENCY_NONE,
+                    DynamicRangeProfiles.HDR10,
+                    DynamicRangeProfiles.HDR10 or DynamicRangeProfiles.HDR10_PLUS,
+                    LATENCY_NONE,
+                    DynamicRangeProfiles.HDR10_PLUS,
+                    DynamicRangeProfiles.HDR10_PLUS or DynamicRangeProfiles.HDR10,
+                    LATENCY_NONE
+                )
+            )
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                attachedSurfaceInfoList = listOf(attachedPriv1, attachedPriv2),
+                dynamicRangeProfiles = constraintsTable,
+                capabilities =
+                intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canAttachHlgDynamicRange_toExistingSdrStreams() {
+        // JPEG use case can be attached with an existing PRIV + PRIV in the 10-bit tables
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        val useCaseExpectedSizeMap = mapOf(
+            // Size is valid for 10-bit table within constraints
+            useCase to recordSize
+        )
+        // existing surfaces (PRIV + PRIV)
+        val attachedPriv1 = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.PRIVATE,
+            previewSize,
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+        val attachedPriv2 = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.PRIV,
+                SurfaceConfig.ConfigSize.RECORD
+            ),
+            ImageFormat.YUV_420_888,
+            recordSize,
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS),
+            useCase.currentConfig,
+            /*targetFrameRate=*/null
+        )
+
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            attachedSurfaceInfoList = listOf(attachedPriv1, attachedPriv2),
+            dynamicRangeProfiles = HLG10_SDR_CONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT)
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun requiredSdrDynamicRangeThrows_whenCombinedWithConstrainedHlg() {
+        // VideoCapture HLG dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        // Preview SDR dynamic range
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.SDR
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+
+        // Fails because HLG10 is constrained to only HLG10
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSuggestedSpecsAndVerify(
+                useCaseExpectedSizeMap,
+                dynamicRangeProfiles = HLG10_CONSTRAINED,
+                capabilities =
+                intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+            )
+        }
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun requiredSdrDynamicRange_canBeCombinedWithUnconstrainedHlg() {
+        // VideoCapture HLG dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HLG_10_BIT
+        )
+        // Preview SDR dynamic range
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.SDR
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+
+        // Should succeed due to HLG10 being unconstrained
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HLG10_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+        )
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun multiple10BitUnconstrainedDynamicRanges_canBeCombined() {
+        // VideoCapture HDR10 dynamic range
+        val videoUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE,
+            dynamicRange = DynamicRange.HDR10_10_BIT
+        )
+        // Preview HDR10_PLUS dynamic range
+        val previewUseCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            dynamicRange = DynamicRange.HDR10_PLUS_10_BIT
+        )
+
+        val useCaseExpectedSizeMap = mutableMapOf(
+            videoUseCase to recordSize,
+            previewUseCase to previewSize
+        )
+
+        // Succeeds because both HDR10 and HDR10_PLUS are unconstrained
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedSizeMap,
+            dynamicRangeProfiles = HDR10_HDR10_PLUS_UNCONSTRAINED,
+            capabilities =
+            intArrayOf(CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT),
+        )
+    }
+
+    // //////////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Resolution selection tests for FPS settings
+    //
+    // //////////////////////////////////////////////////////////////////////////////////////////
+
+    @Test
+    fun getSupportedOutputSizes_single_valid_targetFPS() {
+        // a valid target means the device is capable of that fps
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(25, 30)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase, Size(3840, 2160))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_single_invalid_targetFPS() {
+        // an invalid target means the device would neve be able to reach that fps
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(65, 70)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase, Size(800, 450))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_targetFPS_first_is_larger() {
+        // a valid target means the device is capable of that fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCase2 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(15, 25)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // both selected size should be no larger than 1920 x 1445
+            put(useCase1, Size(1920, 1445))
+            put(useCase2, Size(1920, 1445))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_targetFPS_first_is_smaller() {
+        // a valid target means the device is capable of that fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCase2 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(45, 50)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // both selected size should be no larger than 1920 x 1440
+            put(useCase1, Size(1920, 1440))
+            put(useCase2, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_targetFPS_intersect() {
+        // first and second new use cases have target fps that intersect each other
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 40)
+        )
+        val useCase2 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(35, 45)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // effective target fps becomes 35-40
+            // both selected size should be no larger than 1920 x 1080
+            put(useCase1, Size(1920, 1080))
+            put(useCase2, Size(1920, 1080))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_cases_first_has_targetFPS() {
+        // first new use case has a target fps, second new use case does not
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCase2 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // both selected size should be no larger than 1920 x 1440
+            put(useCase1, Size(1920, 1440))
+            put(useCase2, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_multiple_cases_second_has_targetFPS() {
+        // second new use case does not have a target fps, first new use case does not
+        val useCase1 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+        val useCase2 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // both selected size should be no larger than 1920 x 1440
+            put(useCase1, Size(1920, 1440))
+            put(useCase2, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_attached_with_targetFPS_no_new_targetFPS() {
+        // existing surface with target fps + new use case without a target fps
+        val useCase = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // size should be no larger than 1280 x 960
+            put(useCase, Size(1280, 960))
+        }
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.JPEG,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.JPEG,
+            Size(1280, 720),
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            Range(40, 50)
+        )
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_attached_with_targetFPS_and_new_targetFPS_no_intersect() {
+        // existing surface with target fps + new use case with target fps that does not intersect
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 35)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // size of new surface should be no larger than 1280 x 960
+            put(useCase, Size(1280, 960))
+        }
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.JPEG,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.JPEG,
+            Size(1280, 720),
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            Range(40, 50)
+        )
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_attached_with_targetFPS_and_new_targetFPS_with_intersect() {
+        // existing surface with target fps + new use case with target fps that intersect each other
+        val useCase = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(45, 50)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            // size of new surface should be no larger than 1280 x 720
+            put(useCase, Size(1280, 720))
+        }
+        // existing surface w/ target fps
+        val attachedSurfaceInfo = AttachedSurfaceInfo.create(
+            SurfaceConfig.create(
+                SurfaceConfig.ConfigType.JPEG,
+                SurfaceConfig.ConfigSize.PREVIEW
+            ),
+            ImageFormat.JPEG,
+            Size(1280, 720),
+            DynamicRange.SDR,
+            listOf(UseCaseConfigFactory.CaptureType.PREVIEW),
+            useCase.currentConfig,
+            Range(40, 50)
+        )
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            attachedSurfaceInfoList = listOf(attachedSurfaceInfo),
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            compareWithAtMost = true
+        )
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_device_supported_expectedFrameRateRange() {
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(15, 25)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(4032, 3024))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(10, 22)
+        )
+        // expected fps 10,22 because it has the largest intersection
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_exact_device_supported_expectedFrameRateRange() {
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(30, 40)
+        )
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(30, 30)
+        )
+        // expected fps 30,30 because the fps ceiling is 30
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_no_device_supported_expectedFrameRateRange() {
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(65, 65)
+        )
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(800, 450))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(60, 60)
+        )
+        // expected fps 60,60 because it is the closest range available
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_multiple_device_supported_expectedFrameRateRange() {
+
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(36, 45)
+        )
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(1280, 960))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(30, 40)
+        )
+        // expected size will give a maximum of 40 fps
+        // expected range 30,40. another range with the same intersection size was 30,50, but 30,40
+        // was selected instead because its range has a larger ratio of intersecting value vs
+        // non-intersecting
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_no_device_intersection_expectedFrameRateRange() {
+        // target fps is between ranges, but within device capability (for some reason lol)
+
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(26, 27)
+        )
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(30, 30)
+        )
+        // 30,30 was expected because it is the closest and shortest range to our target fps
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_no_device_intersection_equidistant_expectedFrameRateRange() {
+
+        // use case with target fps
+        val useCase1 = createUseCase(
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            targetFrameRate = Range<Int>(26, 26)
+        )
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(1920, 1440))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareWithAtMost = true,
+            compareExpectedFps = Range(30, 30)
+        )
+        // 30,30 selected because although there are other ranges that  have the same distance to
+        // the target, 30,30 is the shortest range that also happens to be on the upper side of the
+        // target range
+    }
+
+    @Test
+    fun getSuggestedStreamSpec_has_no_expectedFrameRateRange() {
+        // a valid target means the device is capable of that fps
+
+        // use case with no target fps
+        val useCase1 = createUseCase(UseCaseConfigFactory.CaptureType.PREVIEW)
+
+        val useCaseExpectedResultMap = mutableMapOf<UseCase, Size>().apply {
+            put(useCase1, Size(4032, 3024))
+        }
+        getSuggestedSpecsAndVerify(
+            useCaseExpectedResultMap,
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            compareExpectedFps = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED
+        )
+        // since no target fps present, no specific device fps will be selected, and is set to
+        // unspecified: (0,0)
+    }
+
+    // //////////////////////////////////////////////////////////////////////////////////////////
+    //
     // Other tests
     //
     // //////////////////////////////////////////////////////////////////////////////////////////
@@ -1726,6 +2974,8 @@
         highResolutionSupportedSizes: Array<Size>? = null,
         maximumResolutionSupportedSizes: Array<Size>? = null,
         maximumResolutionHighResolutionSupportedSizes: Array<Size>? = null,
+        dynamicRangeProfiles: DynamicRangeProfiles? = null,
+        default10BitProfile: Long? = null,
         capabilities: IntArray? = null,
         cameraId: CameraId = CameraId.fromCamera1Id(0)
     ) {
@@ -1759,6 +3009,17 @@
                 null
             }
 
+        val deviceFPSRanges: Array<Range<Int>?> = arrayOf(
+            Range(10, 22),
+            Range(22, 22),
+            Range(30, 30),
+            Range(30, 50),
+            Range(30, 40),
+            Range(30, 60),
+            Range(50, 60),
+            Range(60, 60)
+        )
+
         val characteristicsMap = mutableMapOf(
             CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
             CameraCharacteristics.SENSOR_ORIENTATION to sensorOrientation,
@@ -1766,17 +3027,27 @@
             CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK,
             CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES to capabilities,
             CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to mockMap,
+            CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES to deviceFPSRanges
         ).also { characteristicsMap ->
             mockMaximumResolutionMap?.let {
                 if (Build.VERSION.SDK_INT >= 31) {
                     characteristicsMap[
                         CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP_MAXIMUM_RESOLUTION
                     ] =
-                    mockMaximumResolutionMap
+                        mockMaximumResolutionMap
                 }
             }
         }
 
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            dynamicRangeProfiles?.let {
+                characteristicsMap[REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES] = it
+            }
+            default10BitProfile?.let {
+                characteristicsMap[REQUEST_RECOMMENDED_TEN_BIT_DYNAMIC_RANGE_PROFILE] = it
+            }
+        }
+
         // set up FakeCafakeCameraMetadatameraMetadata
         fakeCameraMetadata = FakeCameraMetadata(
             cameraId = cameraId,
@@ -1809,6 +3080,81 @@
                     .thenReturn(it)
             }
         }
+
+        // setup to return different minimum frame durations depending on resolution
+        // minimum frame durations were designated only for the purpose of testing
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(4032, 3024))
+            )
+        )
+            .thenReturn(50000000L) // 20 fps, size maximum
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(3840, 2160))
+            )
+        )
+            .thenReturn(40000000L) // 25, size record
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(1920, 1440))
+            )
+        )
+            .thenReturn(33333333L) // 30
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(1920, 1080))
+            )
+        )
+            .thenReturn(28571428L) // 35
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(1280, 960))
+            )
+        )
+            .thenReturn(25000000L) // 40
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(1280, 720))
+            )
+        )
+            .thenReturn(22222222L) // 45, size preview/display
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(960, 544))
+            )
+        )
+            .thenReturn(20000000L) // 50
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(800, 450))
+            )
+        )
+            .thenReturn(16666666L) // 60fps
+
+        Mockito.`when`(
+            mockMap.getOutputMinFrameDuration(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.eq(Size(640, 480))
+            )
+        )
+            .thenReturn(16666666L) // 60fps
+
         shadowCharacteristics.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
         mockMaximumResolutionMap?.let {
             whenever(mockMaximumResolutionMap.getOutputSizes(ArgumentMatchers.anyInt()))
@@ -1891,7 +3237,8 @@
 
     private fun createUseCase(
         captureType: UseCaseConfigFactory.CaptureType,
-        targetFrameRate: Range<Int>? = null
+        targetFrameRate: Range<Int>? = null,
+        dynamicRange: DynamicRange? = DynamicRange.UNSPECIFIED
     ): UseCase {
         val builder = FakeUseCaseConfig.Builder(
             captureType, when (captureType) {
@@ -1903,6 +3250,10 @@
         targetFrameRate?.let {
             builder.mutableConfig.insertOption(UseCaseConfig.OPTION_TARGET_FRAME_RATE, it)
         }
+        builder.mutableConfig.insertOption(
+            ImageInputConfig.OPTION_INPUT_DYNAMIC_RANGE,
+            dynamicRange
+        )
         return builder.build()
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt
new file mode 100644
index 0000000..b06ab92
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/DynamicRangeProfilesCompatTest.kt
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.compat
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_10B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_10B_UNCONSTRAINED_SLOW
+import androidx.camera.camera2.pipe.integration.internal.DOLBY_VISION_8B_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HDR10_PLUS_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HDR10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_HDR10_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_SDR_CONSTRAINED
+import androidx.camera.camera2.pipe.integration.internal.HLG10_UNCONSTRAINED
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.core.DynamicRange
+import com.google.common.truth.Truth
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowCameraCharacteristics
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DynamicRangeProfilesCompatTest {
+
+    private val cameraId = CameraId.fromCamera1Id(0)
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canWrapAndUnwrapDynamicRangeProfiles() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+
+        Truth.assertThat(dynamicRangeProfilesCompat).isNotNull()
+        Truth.assertThat(dynamicRangeProfilesCompat?.toDynamicRangeProfiles())
+            .isEqualTo(HLG10_UNCONSTRAINED)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHlg10Profile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.HLG_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHdr10Profile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HDR10_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.HDR10_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromHdr10PlusProfile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HDR10_PLUS_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.HDR10_PLUS_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromDolbyVision10bProfile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(DOLBY_VISION_10B_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.DOLBY_VISION_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canSupportDynamicRangeFromDolbyVision8bProfile() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(DOLBY_VISION_8B_UNCONSTRAINED)
+        Truth.assertThat(dynamicRangeProfilesCompat?.getSupportedDynamicRanges())
+            .contains(DynamicRange.DOLBY_VISION_8_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceConcurrentDynamicRangeConstraints() {
+        val hlg10ConstrainedWrapped =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_CONSTRAINED)
+        Truth.assertThat(
+            hlg10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        ).containsExactly(DynamicRange.SDR)
+        Truth.assertThat(
+            hlg10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DynamicRange.HLG_10_BIT
+            )
+        ).containsExactly(DynamicRange.HLG_10_BIT)
+
+        val hlg10SdrConstrainedWrapped =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_SDR_CONSTRAINED)
+        Truth.assertThat(
+            hlg10SdrConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        ).containsExactly(DynamicRange.SDR, DynamicRange.HLG_10_BIT)
+        Truth.assertThat(
+            hlg10SdrConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DynamicRange.HLG_10_BIT
+            )
+        ).containsExactly(DynamicRange.HLG_10_BIT, DynamicRange.SDR)
+
+        val hlg10Hdr10ConstrainedWrapped =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_HDR10_CONSTRAINED)
+        Truth.assertThat(
+            hlg10Hdr10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        ).containsExactly(DynamicRange.SDR)
+        Truth.assertThat(
+            hlg10Hdr10ConstrainedWrapped?.getDynamicRangeCaptureRequestConstraints(
+                DynamicRange.HLG_10_BIT
+            )
+        ).containsExactly(DynamicRange.HLG_10_BIT, DynamicRange.HDR10_10_BIT)
+        Truth.assertThat(
+            hlg10Hdr10ConstrainedWrapped
+                ?.getDynamicRangeCaptureRequestConstraints(DynamicRange.HDR10_10_BIT)
+        ).containsExactly(DynamicRange.HDR10_10_BIT, DynamicRange.HLG_10_BIT)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun producesDynamicRangeWithCorrectLatency() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(DOLBY_VISION_10B_UNCONSTRAINED_SLOW)
+        Truth.assertThat(dynamicRangeProfilesCompat?.isExtraLatencyPresent(DynamicRange.SDR))
+            .isFalse()
+        Truth.assertThat(
+            dynamicRangeProfilesCompat?.isExtraLatencyPresent(DynamicRange.DOLBY_VISION_10_BIT)
+        ).isTrue()
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceDynamicRangeWithoutConstraints() {
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.toDynamicRangesCompat(HLG10_UNCONSTRAINED)
+        Truth.assertThat(
+            dynamicRangeProfilesCompat?.getDynamicRangeCaptureRequestConstraints(
+                DynamicRange.HLG_10_BIT
+            )
+        ).isEmpty()
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun producesNullDynamicRangeProfilesFromNullCharacteristics() {
+        val cameraMetadata = FakeCameraMetadata(cameraId = cameraId)
+
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(dynamicRangeProfilesCompat.toDynamicRangeProfiles()).isNull()
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+    @Test
+    fun canProduceDynamicRangesCompatFromCharacteristics() {
+        val cameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId, characteristics = mutableMapOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES to HLG10_CONSTRAINED
+            )
+        )
+
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(dynamicRangeProfilesCompat.toDynamicRangeProfiles())
+            .isEqualTo(HLG10_CONSTRAINED)
+    }
+
+    @Test
+    fun alwaysSupportsOnlySdrWithoutDynamicRangeProfilesInCharacteristics() {
+        val cameraMetadata = FakeCameraMetadata(cameraId = cameraId)
+
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+            .containsExactly(DynamicRange.SDR)
+        Truth.assertThat(
+            dynamicRangeProfilesCompat.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        ).containsExactly(DynamicRange.SDR)
+    }
+
+    @Test
+    fun unsupportedDynamicRangeAlwaysThrowsException() {
+        val characteristics = mutableMapOf<CameraCharacteristics.Key<*>, Any?>()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            characteristics[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES] =
+                DOLBY_VISION_8B_UNCONSTRAINED
+        }
+        val cameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId, characteristics = characteristics
+        )
+
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+                .containsExactly(DynamicRange.SDR)
+        } else {
+            Truth.assertThat(dynamicRangeProfilesCompat.getSupportedDynamicRanges())
+                .containsExactly(
+                    DynamicRange.SDR, DynamicRange.DOLBY_VISION_8_BIT
+                )
+        }
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            dynamicRangeProfilesCompat
+                .getDynamicRangeCaptureRequestConstraints(DynamicRange.DOLBY_VISION_10_BIT)
+        }
+
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            dynamicRangeProfilesCompat.isExtraLatencyPresent(DynamicRange.DOLBY_VISION_10_BIT)
+        }
+    }
+
+    @Test
+    fun sdrHasNoExtraLatency() {
+        val characteristics = mutableMapOf<CameraCharacteristics.Key<*>, Any?>()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            characteristics[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES] =
+                HLG10_CONSTRAINED
+        }
+        val cameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId, characteristics = characteristics
+        )
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(dynamicRangeProfilesCompat.isExtraLatencyPresent(DynamicRange.SDR))
+            .isFalse()
+    }
+
+    @Test
+    fun sdrHasSdrConstraint_whenConcurrentDynamicRangesNotSupported() {
+        val characteristics = mutableMapOf<CameraCharacteristics.Key<*>, Any?>()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            characteristics[CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES] =
+                HLG10_CONSTRAINED
+        }
+        val cameraMetadata = FakeCameraMetadata(
+            cameraId = cameraId, characteristics = characteristics
+        )
+        val dynamicRangeProfilesCompat =
+            DynamicRangeProfilesCompat.fromCameraMetaData(cameraMetadata)
+
+        Truth.assertThat(
+            dynamicRangeProfilesCompat.getDynamicRangeCaptureRequestConstraints(DynamicRange.SDR)
+        )
+            .containsExactly(DynamicRange.SDR)
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+fun ShadowCameraCharacteristics.addDynamicRangeProfiles(
+    dynamicRangeProfiles: DynamicRangeProfiles
+) {
+    set(
+        CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES,
+        dynamicRangeProfiles
+    )
+}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt
new file mode 100644
index 0000000..bcaae5a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/internal/DynamicRangeTestCases.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:RequiresApi(33)
+
+package androidx.camera.camera2.pipe.integration.internal
+
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM
+import android.hardware.camera2.params.DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM
+import android.hardware.camera2.params.DynamicRangeProfiles.HDR10
+import android.hardware.camera2.params.DynamicRangeProfiles.HDR10_PLUS
+import android.hardware.camera2.params.DynamicRangeProfiles.HLG10
+import android.hardware.camera2.params.DynamicRangeProfiles.STANDARD
+import androidx.annotation.RequiresApi
+
+val HLG10_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(longArrayOf(HLG10, 0, 0))
+}
+
+val HLG10_CONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, HLG10, LATENCY_NONE
+        )
+    )
+}
+
+val HLG10_SDR_CONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, HLG10 or STANDARD, LATENCY_NONE
+        )
+    )
+}
+
+val HLG10_HDR10_CONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, HLG10 or HDR10, LATENCY_NONE,
+            HDR10, HDR10 or HLG10, LATENCY_NONE
+        )
+    )
+}
+
+val HDR10_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            HDR10, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val HDR10_PLUS_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            HDR10_PLUS, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val HDR10_HDR10_PLUS_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            HDR10, CONSTRAINTS_NONE, LATENCY_NONE,
+            HDR10_PLUS, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val DOLBY_VISION_10B_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            DOLBY_VISION_10B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val DOLBY_VISION_10B_UNCONSTRAINED_SLOW by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE, // HLG is mandated
+            DOLBY_VISION_10B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NON_ZERO
+        )
+    )
+}
+
+val DOLBY_VISION_8B_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            DOLBY_VISION_8B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NONE
+        )
+    )
+}
+
+val DOLBY_VISION_8B_SDR_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            DOLBY_VISION_8B_HDR_OEM, DOLBY_VISION_8B_HDR_OEM or STANDARD, LATENCY_NONE
+        )
+    )
+}
+
+val DOLBY_VISION_8B_UNCONSTRAINED_HLG10_UNCONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, CONSTRAINTS_NONE, LATENCY_NONE,
+            DOLBY_VISION_8B_HDR_OEM, CONSTRAINTS_NONE, LATENCY_NONE,
+        )
+    )
+}
+
+val DOLBY_VISION_CONSTRAINED by lazy {
+    DynamicRangeProfiles(
+        longArrayOf(
+            HLG10, HLG10, LATENCY_NONE, // HLG is mandated
+            DOLBY_VISION_10B_HDR_OEM, DOLBY_VISION_10B_HDR_OEM or DOLBY_VISION_8B_HDR_OEM,
+            LATENCY_NONE,
+            DOLBY_VISION_8B_HDR_OEM, DOLBY_VISION_8B_HDR_OEM or DOLBY_VISION_10B_HDR_OEM,
+            LATENCY_NONE
+        )
+    )
+}
+
+const val LATENCY_NONE = 0L
+private const val LATENCY_NON_ZERO = 3L
+private const val CONSTRAINTS_NONE = 0L
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
index 401c134..420f747 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
@@ -178,6 +178,14 @@
             override fun getSupportedDynamicRanges(): MutableSet<DynamicRange> {
                 throw NotImplementedError("Not used in testing")
             }
+
+            override fun isPreviewStabilizationSupported(): Boolean {
+                throw NotImplementedError("Not used in testing")
+            }
+
+            override fun isVideoStabilizationSupported(): Boolean {
+                throw NotImplementedError("Not used in testing")
+            }
         }
         Camera2CameraInfo.from(wrongCameraInfo)
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
index ec0be0e9..7274b3b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
@@ -36,8 +36,7 @@
  * This allows all fields to be accessed and return reasonable values on all OS versions.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class Camera2CameraMetadata
-constructor(
+internal class Camera2CameraMetadata(
     override val camera: CameraId,
     override val isRedacted: Boolean,
     private val characteristics: CameraCharacteristics,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
index e7187fe..ba3953d7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
@@ -17,7 +17,9 @@
 package androidx.camera.camera2.pipe.compat
 
 import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraManager
+import android.os.Build
 import android.util.ArrayMap
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
@@ -105,7 +107,13 @@
 
                 // Merge the camera specific and global cache blocklists together.
                 // this will prevent these values from being cached after first access.
-                val cameraBlocklist = cameraMetadataConfig.cameraCacheBlocklist[cameraId]
+                val cameraBlocklist =
+                    if (shouldBlockSensorOrientationCache(characteristics)) {
+                        (cameraMetadataConfig.cameraCacheBlocklist[cameraId] ?: emptySet()) +
+                            CameraCharacteristics.SENSOR_ORIENTATION
+                    } else {
+                        cameraMetadataConfig.cameraCacheBlocklist[cameraId]
+                    }
                 val cacheBlocklist =
                     if (cameraBlocklist == null) {
                         cameraMetadataConfig.cacheBlocklist
@@ -146,4 +154,9 @@
     }
 
     private fun isMetadataRedacted(): Boolean = !permissions.hasCameraPermission
+
+    private fun shouldBlockSensorOrientationCache(characteristics: CameraCharacteristics): Boolean {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 &&
+            characteristics[CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP] != null
+    }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index e6da980..edcf484 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -55,6 +55,7 @@
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.Logger;
+import androidx.camera.core.PreviewCapabilities;
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraInfoInternal;
@@ -507,6 +508,12 @@
         }
     }
 
+    @NonNull
+    @Override
+    public PreviewCapabilities getPreviewCapabilities() {
+        return Camera2PreviewCapabilities.from(this);
+    }
+
     @Override
     public boolean isVideoStabilizationSupported() {
         int[] availableVideoStabilizationModes =
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java
new file mode 100644
index 0000000..aa25edd
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PreviewCapabilities.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.PreviewCapabilities;
+import androidx.camera.core.impl.CameraInfoInternal;
+
+/**
+ * Camera2 implementation of {@link PreviewCapabilities}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class Camera2PreviewCapabilities implements PreviewCapabilities {
+
+    private boolean mIsStabilizationSupported = false;
+
+    Camera2PreviewCapabilities(@NonNull CameraInfoInternal cameraInfoInternal) {
+        mIsStabilizationSupported = cameraInfoInternal.isPreviewStabilizationSupported();
+    }
+
+
+    @NonNull
+    static Camera2PreviewCapabilities from(@NonNull CameraInfo cameraInfo) {
+        return new Camera2PreviewCapabilities((CameraInfoInternal) cameraInfo);
+    }
+
+
+    @Override
+    public boolean isStabilizationSupported() {
+        return mIsStabilizationSupported;
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 5908b96..8a876ca 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -687,7 +687,7 @@
         init(/* hasAvailableCapabilities = */ false);
 
         // Camera0
-        CameraInfo cameraInfo0 = new Camera2CameraInfoImpl(CAMERA0_ID,
+        Camera2CameraInfoImpl cameraInfo0 = new Camera2CameraInfoImpl(CAMERA0_ID,
                 mCameraManagerCompat);
 
 
@@ -699,14 +699,14 @@
         assertThat(cameraInfo0.isVideoStabilizationSupported()).isTrue();
 
         // Camera1
-        CameraInfo cameraInfo1 = new Camera2CameraInfoImpl(CAMERA1_ID,
+        Camera2CameraInfoImpl cameraInfo1 = new Camera2CameraInfoImpl(CAMERA1_ID,
                 mCameraManagerCompat);
 
         assertThat(cameraInfo1.isPreviewStabilizationSupported()).isFalse();
         assertThat(cameraInfo0.isVideoStabilizationSupported()).isTrue();
 
         // Camera2
-        CameraInfo cameraInfo2 = new Camera2CameraInfoImpl(CAMERA2_ID,
+        Camera2CameraInfoImpl cameraInfo2 = new Camera2CameraInfoImpl(CAMERA2_ID,
                 mCameraManagerCompat);
         assertThat(cameraInfo2.isPreviewStabilizationSupported()).isFalse();
         assertThat(cameraInfo2.isVideoStabilizationSupported()).isFalse();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 8c0750e..4d7519d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -298,23 +298,14 @@
     }
 
     /**
-     * Returns if video stabilization is supported on the device.
+     * Returns {@link PreviewCapabilities} to query preview stream related device capability.
      *
-     * @return true if supported, otherwise false.
+     * @return {@link PreviewCapabilities}
      */
+    @NonNull
     @RestrictTo(Scope.LIBRARY_GROUP)
-    default boolean isVideoStabilizationSupported() {
-        return false;
-    }
-
-    /**
-     * Returns if preview stabilization is supported on the device.
-     *
-     * @return true if supported, otherwise false.
-     */
-    @RestrictTo(Scope.LIBRARY_GROUP)
-    default boolean isPreviewStabilizationSupported() {
-        return false;
+    default PreviewCapabilities getPreviewCapabilities() {
+        return PreviewCapabilities.EMPTY;
     }
 
     /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java b/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java
new file mode 100644
index 0000000..283b013
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/PreviewCapabilities.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * PreviewCapabilities is used to query preview stream capabilities on the device.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface PreviewCapabilities {
+
+    /**
+     * Returns if preview stabilization is supported on the device.
+     *
+     * @return true if
+     * {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION} is supported,
+     * otherwise false.
+     *
+     * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+     */
+    boolean isStabilizationSupported();
+
+
+    /** An empty implementation. */
+    @NonNull
+    PreviewCapabilities EMPTY = new PreviewCapabilities() {
+        @Override
+        public boolean isStabilizationSupported() {
+            return false;
+        }
+    };
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index e6a1e9b..d78c73a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -18,6 +18,7 @@
 
 import android.graphics.ImageFormat;
 import android.graphics.PixelFormat;
+import android.hardware.camera2.CaptureRequest;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
@@ -106,6 +107,27 @@
     Set<DynamicRange> getSupportedDynamicRanges();
 
     /**
+     * Returns if preview stabilization is supported on the device.
+     *
+     * @return true if
+     * {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION} is supported,
+     * otherwise false.
+     *
+     * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+     */
+    boolean isPreviewStabilizationSupported();
+
+    /**
+     * Returns if video stabilization is supported on the device.
+     *
+     * @return true if {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_ON} is supported,
+     * otherwise false.
+     *
+     * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+     */
+    boolean isVideoStabilizationSupported();
+
+    /**
      * Gets the underlying implementation instance which could be cast into an implementation
      * specific class for further use in implementation module. Returns <code>this</code> if this
      * instance is the implementation instance.
@@ -115,7 +137,6 @@
         return this;
     }
 
-
     /** {@inheritDoc} */
     @NonNull
     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
index ab1314e..be3b3b8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Config.java
@@ -46,8 +46,8 @@
      * Returns whether this configuration contains the supplied option.
      *
      * @param id The {@link Option} to search for in this configuration.
-     * @return <code>true</code> if this configuration contains the supplied option; <code>false
-     * </code> otherwise.
+     * @return {@code true} if this configuration contains the supplied option; {@code false}
+     * otherwise.
      */
     boolean containsOption(@NonNull Option<?> id);
 
@@ -77,7 +77,7 @@
      * @param valueIfMissing The value to return if the specified {@link Option} does not exist in
      *                       this configuration.
      * @param <ValueT>       The type for the value associated with the supplied {@link Option}.
-     * @return The value stored in this configuration, or <code>valueIfMissing</code> if it does
+     * @return The value stored in this configuration, or {@code valueIfMissing} if it does
      * not exist.
      */
     @Nullable
@@ -116,12 +116,10 @@
      *                       option such as \"<code>
      *                       camerax.core.example</code>\".
      * @param matcher        A callback used to receive results of the search. Results will be
-     *                       sent to
-     *                       {@link OptionMatcher#onOptionMatched(Option)} in the order in which
-     *                       they are found inside
-     *                       this configuration. Subsequent results will continue to be sent as
-     *                       long as {@link
-     *                       OptionMatcher#onOptionMatched(Option)} returns <code>true</code>.
+     *                       sent to {@link OptionMatcher#onOptionMatched(Option)} in the order
+     *                       in which they are found inside this configuration. Subsequent
+     *                       results will continue to be sent as long as {@link
+     *                       OptionMatcher#onOptionMatched(Option)} returns {@code true}.
      */
     void findOptions(@NonNull String idSearchString, @NonNull OptionMatcher matcher);
 
@@ -199,8 +197,7 @@
          * @param valueClass The class of the value stored by this option.
          * @param <T>        The type of the value stored by this option.
          * @param token      An optional, type-erased object for storing more context for this
-         *                   specific
-         *                   option. Generally this object should have static scope and be
+         *                   specific option. Generally this object should have static scope and be
          *                   immutable.
          * @return An {@link Option} object which can be used to store/retrieve values from a {@link
          * Config}.
@@ -250,11 +247,14 @@
      */
     enum OptionPriority {
         /**
-         * Should only be used externally by apps. It takes precedence over any other option
-         * values at the risk of causing unexpected behavior.
+         * It takes precedence over any other option values at the risk of causing unexpected
+         * behavior.
          *
-         * <p>This should not used internally in CameraX. It conflicts when merging different
-         * values set to ALWAY_OVERRIDE.
+         * <p>If the same option is already set, the option with this priority will overwrite the
+         * value.
+         *
+         * <p>This priority should only be used to explicitly specify an option, such as used by
+         * {@code Camera2Interop} or {@code Camera2CameraControl}, and should be used with caution.
          */
         ALWAYS_OVERRIDE,
 
@@ -262,15 +262,17 @@
          * It's a required option value in order to achieve expected CameraX behavior. It takes
          * precedence over {@link #OPTIONAL} option values.
          *
-         * <p>If apps set ALWAYS_OVERRIDE options, it'll override REQUIRED option values and can
-         * potentially cause unexpected behaviors. It conflicts when merging different values set
-         * to REQUIRED.
+         * <p>If two values are set to the same option, the value with {@link #ALWAYS_OVERRIDE}
+         * priority will overwrite this priority and can potentially cause unexpected behaviors.
+         *
+         * <p>If two values are set to the same option with this priority, it might indicate a
+         * programming error internally and an exception will be thrown when merging the configs.
          */
         REQUIRED,
 
         /**
          * The lowest priority, it can be overridden by any other option value. When two option
-         * values are set as OPTIONAL, the newer value takes precedence over the old one.
+         * values are set with this priority, the newer value takes precedence over the old one.
          */
         OPTIONAL
     }
@@ -278,35 +280,25 @@
     /**
      * Returns if values with these {@link OptionPriority} conflict or not.
      *
-     * Currently it is not allowed to have different values with same ALWAYS_OVERRIDE
-     * priority or to have different values with same REQUIRED priority.
+     * <p>Currently it is not allowed the same option to have different values with priority
+     * {@link OptionPriority#REQUIRED}.
      */
     static boolean hasConflict(@NonNull OptionPriority priority1,
             @NonNull OptionPriority priority2) {
-        if (priority1 == OptionPriority.ALWAYS_OVERRIDE
-                && priority2 == OptionPriority.ALWAYS_OVERRIDE) {
-            return true;
-        }
-
-        if (priority1 == OptionPriority.REQUIRED
-                && priority2 == OptionPriority.REQUIRED) {
-            return true;
-        }
-
-        return false;
+        return priority1 == OptionPriority.REQUIRED
+                && priority2 == OptionPriority.REQUIRED;
     }
 
     /**
-     * Merges two configs
+     * Merges two configs.
      *
      * @param extendedConfig the extended config. The options in the extendedConfig will be applied
      *                       on top of the baseConfig based on the option priorities.
-     * @param baseConfig the base config
-     * @return a {@link MutableOptionsBundle} of the merged config
+     * @param baseConfig the base config.
+     * @return a {@link MutableOptionsBundle} of the merged config.
      */
     @NonNull
-    static Config mergeConfigs(@Nullable Config extendedConfig,
-            @Nullable Config baseConfig) {
+    static Config mergeConfigs(@Nullable Config extendedConfig, @Nullable Config baseConfig) {
         if (extendedConfig == null && baseConfig == null) {
             return OptionsBundle.emptyBundle();
         }
@@ -333,12 +325,12 @@
     /**
      * Merges a specific option value from two configs.
      *
-     * @param mergedConfig   the final output config
+     * @param mergedConfig   the final output config.
      * @param baseConfig     the base config contains the option value which might be overridden by
      *                       the corresponding option value in the extend config.
      * @param extendedConfig the extended config contains the option value which might override
      *                       the corresponding option value in the base config.
-     * @param opt            the option to merge
+     * @param opt            the option to merge.
      */
     static void mergeOptionValue(@NonNull MutableOptionsBundle mergedConfig,
             @NonNull Config baseConfig,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index c389a1d..0bd54ec 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -27,6 +27,7 @@
 import androidx.camera.core.ExperimentalZeroShutterLag;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.PreviewCapabilities;
 import androidx.camera.core.ZoomState;
 import androidx.lifecycle.LiveData;
 
@@ -192,4 +193,20 @@
     public CameraSelector getCameraSelector() {
         return mCameraInfoInternal.getCameraSelector();
     }
+
+    @Override
+    public boolean isPreviewStabilizationSupported() {
+        return mCameraInfoInternal.isPreviewStabilizationSupported();
+    }
+
+    @Override
+    public boolean isVideoStabilizationSupported() {
+        return mCameraInfoInternal.isVideoStabilizationSupported();
+    }
+
+    @NonNull
+    @Override
+    public PreviewCapabilities getPreviewCapabilities() {
+        return mCameraInfoInternal.getPreviewCapabilities();
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index e998934..1c0a7c4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -61,6 +61,7 @@
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CameraMode;
 import androidx.camera.core.impl.Config;
+import androidx.camera.core.impl.PreviewConfig;
 import androidx.camera.core.impl.RestrictedCameraControl;
 import androidx.camera.core.impl.RestrictedCameraControl.CameraOperation;
 import androidx.camera.core.impl.RestrictedCameraInfo;
@@ -70,6 +71,7 @@
 import androidx.camera.core.impl.SurfaceConfig;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.streamsharing.StreamSharing;
 import androidx.core.util.Preconditions;
@@ -688,8 +690,11 @@
                         supportedOutputSizesSorter.getSortedSupportedOutputSizes(
                                 combinedUseCaseConfig));
 
-                // TODO(kailianc): extract the use case's video stabilization settings  and set
-                //  to isPreviewStabilizationOn
+                if (useCase.getCurrentConfig() instanceof PreviewConfig) {
+                    isPreviewStabilizationOn =
+                            ((PreviewConfig) useCase.getCurrentConfig())
+                                    .getPreviewStabilizationMode() == StabilizationMode.ON;
+                }
             }
 
             // Get suggested stream specifications and update the use case session configuration
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
index c6ad529..471095e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LargeJpegImageQuirk.java
@@ -28,16 +28,16 @@
 
 /**
  * <p>QuirkSummary
- *     Bug Id: 288828159
+ *     Bug Id: 288828159, 299069235
  *     Description: Quirk required to check whether the captured JPEG image contains redundant
  *                  0's padding data. For example, Samsung A5 (2017) series devices have the
  *                  problem and result in the output JPEG image to be extremely large (about 32 MB).
- *     Device(s): Samsung Galaxy A5 (2017), A52, A70, A72 and S7 series devices
+ *     Device(s): Samsung Galaxy A5 (2017), A52, A70, A72, S7 series devices and Vivo S16 device
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public final class LargeJpegImageQuirk implements Quirk {
 
-    private static final Set<String> DEVICE_MODELS = new HashSet<>(Arrays.asList(
+    private static final Set<String> SAMSUNG_DEVICE_MODELS = new HashSet<>(Arrays.asList(
             // Samsung Galaxy A5 series devices
             "SM-A520F",
             "SM-A520L",
@@ -70,7 +70,22 @@
             "SM-S906B"
     ));
 
+    private static final Set<String> VIVO_DEVICE_MODELS = new HashSet<>(Arrays.asList(
+            // Vivo S16
+            "V2244A"
+    ));
+
     static boolean load() {
-        return DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+        return isSamsungProblematicDevice() || isVivoProblematicDevice();
+    }
+
+    private static boolean isSamsungProblematicDevice() {
+        return "Samsung".equalsIgnoreCase(Build.BRAND) && SAMSUNG_DEVICE_MODELS.contains(
+                Build.MODEL.toUpperCase(Locale.US));
+    }
+
+    private static boolean isVivoProblematicDevice() {
+        return "Vivo".equalsIgnoreCase(Build.BRAND) && VIVO_DEVICE_MODELS.contains(
+                Build.MODEL.toUpperCase(Locale.US));
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
index a9b122ab..4a0d527 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParser.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.core.internal.compat.quirk.LargeJpegImageQuirk;
 
@@ -41,12 +42,23 @@
             return bytes.length;
         }
 
+        int jfifEoiMarkEndPosition = getJfifEoiMarkEndPosition(bytes);
+
+        return jfifEoiMarkEndPosition != -1 ? jfifEoiMarkEndPosition : bytes.length;
+    }
+
+    /**
+     * Returns the end position of JFIF EOI mark. Returns -1 while JFIF EOI mark can't be found
+     * in the provided byte array.
+     */
+    @VisibleForTesting
+    public static int getJfifEoiMarkEndPosition(@NonNull byte[] bytes) {
         // Parses the JFIF segments from the start of the JPEG image data
         int markPosition = 0x2;
         while (true) {
             // Breaks the while-loop and return null if the mark byte can't be correctly found.
             if (markPosition + 4 > bytes.length || bytes[markPosition] != ((byte) 0xff)) {
-                return bytes.length;
+                return -1;
             }
 
             int segmentLength =
@@ -65,7 +77,7 @@
         while (true) {
             // Breaks the while-loop and return null if EOI mark can't be found
             if (eoiPosition + 2 > bytes.length) {
-                return bytes.length;
+                return -1;
             }
 
             if (bytes[eoiPosition] == ((byte) 0xff) && bytes[eoiPosition + 1] == ((byte) 0xd9)) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
index 96b086a..e78d689f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/ConfigTest.java
@@ -89,8 +89,8 @@
     }
 
     @Test
-    public void hasConflict_whenTwoValueAreALWAYSOVERRIDE() {
-        assertThat(Config.hasConflict(ALWAYS_OVERRIDE, ALWAYS_OVERRIDE)).isTrue();
+    public void noConflict_whenTwoValueAreALWAYSOVERRIDE() {
+        assertThat(Config.hasConflict(ALWAYS_OVERRIDE, ALWAYS_OVERRIDE)).isFalse();
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
index 1db4883..be0e91c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/MutableOptionsBundleTest.java
@@ -108,13 +108,18 @@
         assertThat(config2.retrieveOptionWithPriority(OPTION_2, OPTIONAL)).isEqualTo(VALUE_1);
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test
     public void insertOption_ALWAYSOVERRIDE_ALWAYSOVERRIDE() {
         MutableOptionsBundle mutOpts = MutableOptionsBundle.create();
 
         mutOpts.insertOption(OPTION_1, ALWAYS_OVERRIDE, VALUE_1);
-        // should throw an Error
         mutOpts.insertOption(OPTION_1, ALWAYS_OVERRIDE, VALUE_2);
+
+        assertThat(mutOpts.retrieveOption(OPTION_1)).isEqualTo(VALUE_2);
+        Config.OptionPriority highestPriority = Collections.min(mutOpts.getPriorities(OPTION_1));
+        assertThat(highestPriority).isEqualTo(ALWAYS_OVERRIDE);
+        assertThat(mutOpts.retrieveOptionWithPriority(OPTION_1, highestPriority))
+                .isEqualTo(VALUE_2);
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
index cdd2576..127b136 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/InvalidJpegDataParserTest.kt
@@ -73,31 +73,34 @@
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class InvalidJpegDataParserTest(
+    private val brand: String,
     private val model: String,
     private val data: ByteArray,
     private val validDataLength: Int,
-    ) {
+) {
 
     companion object {
         @JvmStatic
-        @ParameterizedRobolectricTestRunner.Parameters(name = "model={0}, data={1}, length={2}")
+        @ParameterizedRobolectricTestRunner.Parameters(
+            name = "brand={0}, model={1}, data={2}, length={3}")
         fun data() = mutableListOf<Array<Any?>>().apply {
-            add(arrayOf("SM-A520F", problematicJpegByteArray, 18))
-            add(arrayOf("SM-A520F", problematicJpegByteArray2, 18))
-            add(arrayOf("SM-A520F", correctJpegByteArray1, 18))
-            add(arrayOf("SM-A520F", correctJpegByteArray2, 18))
-            add(arrayOf("SM-A520F", invalidVeryShortData, 2))
-            add(arrayOf("SM-A520F", invalidNoSosData, 28))
-            add(arrayOf("SM-A520F", invalidNoEoiData, 28))
-            add(arrayOf("fake-model", problematicJpegByteArray, 42))
-            add(arrayOf("fake-model", problematicJpegByteArray2, 64))
-            add(arrayOf("fake-model", correctJpegByteArray1, 28))
-            add(arrayOf("fake-model", correctJpegByteArray2, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", problematicJpegByteArray, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", problematicJpegByteArray2, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", correctJpegByteArray1, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", correctJpegByteArray2, 18))
+            add(arrayOf("SAMSUNG", "SM-A520F", invalidVeryShortData, 2))
+            add(arrayOf("SAMSUNG", "SM-A520F", invalidNoSosData, 28))
+            add(arrayOf("SAMSUNG", "SM-A520F", invalidNoEoiData, 28))
+            add(arrayOf("fake-brand", "fake-model", problematicJpegByteArray, 42))
+            add(arrayOf("fake-brand", "fake-model", problematicJpegByteArray2, 64))
+            add(arrayOf("fake-brand", "fake-model", correctJpegByteArray1, 28))
+            add(arrayOf("fake-brand", "fake-model", correctJpegByteArray2, 18))
         }
     }
 
     @Test
     fun canGetValidJpegDataLength() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", brand)
         ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
         assertThat(InvalidJpegDataParser().getValidDataLength(data)).isEqualTo(validDataLength)
     }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
index abd3bda..9bc767e 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/compat/workaround/OnEnableDisableSessionDurationCheckTest.kt
@@ -17,6 +17,7 @@
 package androidx.camera.extensions.internal.compat.workaround
 
 import androidx.camera.extensions.internal.compat.workaround.OnEnableDisableSessionDurationCheck.MIN_DURATION_FOR_ENABLE_DISABLE_SESSION
+import androidx.camera.testing.impl.AndroidUtil
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
@@ -24,6 +25,8 @@
 import kotlin.system.measureTimeMillis
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
+import org.junit.Assume.assumeFalse
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,6 +38,11 @@
         const val TOLERANCE = 60L
     }
 
+    @Before
+    fun setUp() {
+        assumeFalse(AndroidUtil.isEmulatorAndAPI21())
+    }
+
     @Test
     fun enabled_ensureMinimalDuration() = runBlocking {
         // Arrange
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 7a4b53a..8b28e6c 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -299,6 +299,16 @@
         return mIntrinsicZoomRatio;
     }
 
+    @Override
+    public boolean isPreviewStabilizationSupported() {
+        return false;
+    }
+
+    @Override
+    public boolean isVideoStabilizationSupported() {
+        return false;
+    }
+
     /** Adds a quirk to the list of this camera's quirks. */
     @SuppressWarnings("unused")
     public void addCameraQuirk(@NonNull final Quirk quirk) {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt
similarity index 60%
rename from camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt
rename to camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt
index e454f702..b482047 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/E2ETestUtil.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/FileUtil.kt
@@ -18,10 +18,12 @@
 
 import android.content.ContentResolver
 import android.content.ContentValues
+import android.net.Uri
 import android.os.Environment.DIRECTORY_DOCUMENTS
 import android.os.Environment.DIRECTORY_MOVIES
 import android.os.Environment.getExternalStoragePublicDirectory
 import android.provider.MediaStore
+import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.camera.core.Logger
 import androidx.camera.video.FileOutputOptions
@@ -32,12 +34,12 @@
 import java.io.FileOutputStream
 import java.io.OutputStreamWriter
 
-private const val TAG = "E2ETestUtil"
+private const val TAG = "FileUtil"
 private const val EXTENSION_MP4 = "mp4"
 private const val EXTENSION_TEXT = "txt"
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-object E2ETestUtil {
+object FileUtil {
 
     /**
      * Write the given text to the external storage.
@@ -101,7 +103,7 @@
     ): FileOutputOptions {
         val fileNameWithExtension = "$fileName.$extension"
         val folder = getExternalStoragePublicDirectory(DIRECTORY_MOVIES)
-        if (!folder.exists() && !folder.mkdirs()) {
+        if (!createFolder(folder)) {
             Logger.e(TAG, "Failed to create directory: $folder")
         }
         return FileOutputOptions.Builder(File(folder, fileNameWithExtension)).build()
@@ -119,14 +121,10 @@
     fun generateVideoMediaStoreOptions(
         contentResolver: ContentResolver,
         fileName: String
-    ): MediaStoreOutputOptions {
-        val contentValues = generateVideoContentValues(fileName)
-
-        return MediaStoreOutputOptions.Builder(
-            contentResolver,
-            MediaStore.Video.Media.EXTERNAL_CONTENT_URI
-        ).setContentValues(contentValues).build()
-    }
+    ): MediaStoreOutputOptions = MediaStoreOutputOptions.Builder(
+        contentResolver,
+        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+    ).setContentValues(generateVideoContentValues(fileName)).build()
 
     /**
      * Check if the given file name string is valid.
@@ -145,14 +143,79 @@
         return !fileName.isNullOrBlank()
     }
 
-    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())
+    /**
+     * Creates parent folder for the input file path.
+     *
+     * @param filePath the input file path to create its parent folder.
+     * @return `true` if the parent folder already exists or is created successfully.
+     * `false` if the existing parent folder path is not a folder or failed to create.
+     */
+    @JvmStatic
+    fun createParentFolder(filePath: String): Boolean {
+        return createParentFolder(File(filePath))
+    }
 
-        return res
+    /**
+     * Creates parent folder for the input file.
+     *
+     * @param file the input file to create its parent folder
+     * @return `true` if the parent folder already exists or is created successfully.
+     * `false` if the existing parent folder path is not a folder or failed to create.
+     */
+    @JvmStatic
+    fun createParentFolder(file: File): Boolean = file.parentFile?.let {
+        createFolder(it)
+    } ?: false
+
+    /**
+     * Creates folder for the input file.
+     *
+     * @param file the input file to create folder
+     * @return `true` if the folder already exists or is created successfully.
+     * `false` if the existing folder path is not a folder or failed to create.
+     */
+    @JvmStatic
+    fun createFolder(file: File): Boolean = if (file.exists()) {
+        file.isDirectory
+    } else {
+        file.mkdirs()
+    }
+
+    /**
+     * Gets the absolute path from a Uri.
+     *
+     * @param resolver   the content resolver.
+     * @param contentUri the content uri.
+     * @return the file path of the content uri or null if not found.
+     */
+    @JvmStatic
+    fun getAbsolutePathFromUri(resolver: ContentResolver, contentUri: Uri): String? {
+        // MediaStore.Video.Media.DATA was deprecated in API level 29.
+        val column = MediaStore.Video.Media.DATA
+        try {
+            resolver.query(contentUri, arrayOf(column), null, null, null)!!.use { cursor ->
+                val columnIndex = cursor.getColumnIndexOrThrow(column)
+                cursor.moveToFirst()
+                return cursor.getString(columnIndex)
+            }
+        } catch (e: RuntimeException) {
+            Log.e(
+                TAG,
+                String.format(
+                    "Failed in getting absolute path for Uri %s with Exception %s",
+                    contentUri, e
+                ), e
+            )
+            return null
+        }
+    }
+
+    private fun generateVideoContentValues(fileName: String) = ContentValues().apply {
+        put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+        put(MediaStore.Video.Media.TITLE, fileName)
+        put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
+        val currentTimeMs = System.currentTimeMillis()
+        put(MediaStore.Video.Media.DATE_ADDED, currentTimeMs / 1000)
+        put(MediaStore.Video.Media.DATE_TAKEN, currentTimeMs)
     }
 }
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index b9cc77a..7f1314e 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -2,10 +2,12 @@
 package androidx.camera.video {
 
   @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class AudioStats {
+    method public double getAudioAmplitude();
     method public abstract int getAudioState();
     method public abstract Throwable? getErrorCause();
     method public boolean hasAudio();
     method public boolean hasError();
+    field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
     field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
     field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
     field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
@@ -14,6 +16,9 @@
     field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
   }
 
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index b9cc77a..7f1314e 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -2,10 +2,12 @@
 package androidx.camera.video {
 
   @RequiresApi(21) @com.google.auto.value.AutoValue public abstract class AudioStats {
+    method public double getAudioAmplitude();
     method public abstract int getAudioState();
     method public abstract Throwable? getErrorCause();
     method public boolean hasAudio();
     method public boolean hasError();
+    field public static final double AUDIO_AMPLITUDE_NONE = 0.0;
     field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
     field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
     field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
@@ -14,6 +16,9 @@
     field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
   }
 
+  @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAudioApi {
+  }
+
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalPersistentRecording {
   }
 
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
index 42f8ae0..3e39ad6 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoCaptureDeviceTest.kt
@@ -570,6 +570,10 @@
                 val profiles = createFakeEncoderProfilesProxy(size.width, size.height)
                 return VideoValidatedEncoderProfilesProxy.from(profiles)
             }
+
+            override fun isStabilizationSupported(): Boolean {
+                return false
+            }
         }
     }
 
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
index 7f171f5..dde69be 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
@@ -104,7 +104,6 @@
      * Should audio recording be disabled, any attempts to retrieve the amplitude will
      * return this value.
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
     public static final double AUDIO_AMPLITUDE_NONE = 0;
 
     @IntDef({AUDIO_STATE_ACTIVE, AUDIO_STATE_DISABLED, AUDIO_STATE_SOURCE_SILENCED,
@@ -168,8 +167,8 @@
     public abstract Throwable getErrorCause();
 
     /**
-     * Returns the maximum absolute amplitude of the audio most recently sampled. Returns
-     * {@link #AUDIO_AMPLITUDE_NONE} if audio is disabled.
+     * Returns the maximum absolute amplitude of the audio most recently sampled in the past 2
+     * nanoseconds
      *
      * <p>The amplitude is the maximum absolute value over all channels which the audio was
      * most recently sampled from.
@@ -177,10 +176,11 @@
      * <p>Amplitude is a relative measure of the maximum sound pressure/voltage range of the device
      * microphone.
      *
+     * <p>Returns {@link #AUDIO_AMPLITUDE_NONE} if audio is disabled.
      * <p>The amplitude value returned will be a double between {@code 0} and {@code 1}.
+     *
      */
     @OptIn(markerClass = ExperimentalAudioApi.class)
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
     public double getAudioAmplitude() {
         if (getAudioState() == AUDIO_STATE_DISABLED) {
             return AUDIO_AMPLITUDE_NONE;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java b/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
index bf07a6e..99de642 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/ExperimentalAudioApi.java
@@ -19,16 +19,14 @@
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
 import androidx.annotation.RequiresOptIn;
-import androidx.annotation.RestrictTo;
 
 import java.lang.annotation.Retention;
 
 /**
- * Denotes that the methods on retrieving audio amplitude data are experimental and may
+ * Denotes that the annotated element relates to an experimental audio feature and may
  * change in a future release.
  */
 @Retention(CLASS)
 @RequiresOptIn
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public @interface ExperimentalAudioApi {
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
index 7b78e91..b9e560e 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java
@@ -76,6 +76,8 @@
 
     private final EncoderProfilesProvider mProfilesProvider;
 
+    private boolean mIsStabilizationSupported = false;
+
     // Mappings of DynamicRange to recording capability information. The mappings are divided
     // into two collections based on the key's (DynamicRange) category, one for specified
     // DynamicRange and one for others. Specified DynamicRange means that its bit depth and
@@ -130,6 +132,9 @@
                 mCapabilitiesMapForFullySpecifiedDynamicRange.put(dynamicRange, capabilities);
             }
         }
+
+        // Video stabilization
+        mIsStabilizationSupported = cameraInfoInternal.isVideoStabilizationSupported();
     }
 
     /**
@@ -166,6 +171,11 @@
         return capabilities != null && capabilities.isQualitySupported(quality);
     }
 
+    @Override
+    public boolean isStabilizationSupported() {
+        return mIsStabilizationSupported;
+    }
+
     @Nullable
     @Override
     public VideoValidatedEncoderProfilesProxy getProfiles(@NonNull Quality quality,
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
index 02b857e..8f9fd80 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.video;
 
+import android.hardware.camera2.CaptureRequest;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
@@ -114,6 +115,17 @@
     boolean isQualitySupported(@NonNull Quality quality, @NonNull DynamicRange dynamicRange);
 
     /**
+     * Returns if video stabilization is supported on the device.
+     *
+     * @return true if {@link CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE_ON} is supported,
+     * otherwise false.
+     *
+     * @see CaptureRequest#CONTROL_VIDEO_STABILIZATION_MODE
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    boolean isStabilizationSupported();
+
+    /**
      * Gets the corresponding {@link VideoValidatedEncoderProfilesProxy} of the input quality and
      * dynamic range.
      *
@@ -193,5 +205,10 @@
                 @NonNull DynamicRange dynamicRange) {
             return false;
         }
+
+        @Override
+        public boolean isStabilizationSupported() {
+            return false;
+        }
     };
 }
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
index 9fef206..78c7b9a 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/QualitySelectorTest.kt
@@ -438,6 +438,10 @@
             override fun isQualitySupported(quality: Quality, dynamicRange: DynamicRange): Boolean {
                 throw UnsupportedOperationException("Not supported.")
             }
+
+            override fun isStabilizationSupported(): Boolean {
+                return false
+            }
         }
     }
 }
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 141fe23..734deea 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -1716,6 +1716,10 @@
                     return videoCapabilitiesMap[dynamicRange]?.isQualitySupported(quality) ?: false
                 }
 
+                override fun isStabilizationSupported(): Boolean {
+                    return false
+                }
+
                 override fun getProfiles(
                     quality: Quality,
                     dynamicRange: DynamicRange
diff --git a/camera/integration-tests/coretestapp/lint-baseline.xml b/camera/integration-tests/coretestapp/lint-baseline.xml
index 4cbca68..9e9f16f 100644
--- a/camera/integration-tests/coretestapp/lint-baseline.xml
+++ b/camera/integration-tests/coretestapp/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.2.0-alpha15" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-alpha15)" variant="all" version="8.2.0-alpha15">
+<issues format="6" by="lint 8.2.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.0-beta01)" variant="all" version="8.2.0-beta01">
 
     <issue
         id="BanThreadSleep"
@@ -12,108 +12,90 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="CameraInfoInternal can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                if (cameraInfo instanceof CameraInfoInternal) {"
-        errorLine2="                                          ~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    if (!canDeviceWriteToMediaStore()) {"
+        errorLine2="                         ~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="DeviceQuirks.getAll can only be called from within the same library (androidx.camera:camera-video)"
-        errorLine1="                    Quirks deviceQuirks = DeviceQuirks.getAll();"
-        errorLine2="                                                       ~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="CameraInfoInternal can only be accessed from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();"
-        errorLine2="                                            ~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                                                     ~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="CameraInfoInternal.getCameraQuirks can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();"
-        errorLine2="                                                                            ~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                                                               ~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)"
-        errorLine2="                                     ~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)"
-        errorLine2="                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                            || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)"
-        errorLine2="                                            ~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                                                                     ~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                            || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        videoFilePath = getAbsolutePathFromUri("
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                            || cameraQuirks.contains(ImageCaptureFlashNotFireQuirk.class)) {"
-        errorLine2="                                            ~~~~~~~~">
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                getApplicationContext().getContentResolver(),"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="Quirks.contains can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                            || cameraQuirks.contains(ImageCaptureFlashNotFireQuirk.class)) {"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="DeviceQuirks.get can only be called from within the same library (androidx.camera:camera-video)"
-        errorLine1="                    if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {"
-        errorLine2="                                     ~~~">
-        <location
-            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
-    </issue>
-
-    <issue
-        id="RestrictedApiAndroidX"
-        message="DeviceQuirks.get can only be called from within the same library (androidx.camera:camera-video)"
-        errorLine1="                    if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {"
-        errorLine2="                                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                uri"
+        errorLine2="                                ~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
     </issue>
@@ -183,6 +165,69 @@
 
     <issue
         id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (createParentFolder(pictureFolder)) {"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (createParentFolder(pictureFolder)) {"
+        errorLine2="                               ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                getAbsolutePathFromUri(getApplicationContext().getContentResolver(),"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                getAbsolutePathFromUri(getApplicationContext().getContentResolver(),"
+        errorLine2="                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI);"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+        errorLine2="                                                         ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXActivity.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
         message="CameraXExecutors.mainThreadExecutor can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                            CameraXExecutors.mainThreadExecutor());"
         errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
@@ -192,6 +237,159 @@
 
     <issue
         id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (!createParentFolder(pictureFolder)) {"
+        errorLine2="             ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (!createParentFolder(pictureFolder)) {"
+        errorLine2="                                ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="            if (!canDeviceWriteToMediaStore()) {"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                                             ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        this, generateVideoFileOutputOptions(fileName, extension));"
+        errorLine2="                                                                       ~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                                       ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        generateVideoMediaStoreOptions(getContentResolver(), fileName));"
+        errorLine2="                                                                             ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        String videoFilePath = getAbsolutePathFromUri(getContentResolver(),"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        String videoFilePath = getAbsolutePathFromUri(getContentResolver(),"
+        errorLine2="                                                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                MediaStore.Video.Media.EXTERNAL_CONTENT_URI);"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+        errorLine2="                                      ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.createParentFolder can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (videoFilePath == null || !createParentFolder(videoFilePath)) {"
+        errorLine2="                                                         ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                        videoFilePath = getAbsolutePathFromUri("
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                getApplicationContext().getContentResolver(),"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="FileUtil.getAbsolutePathFromUri can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                                uri"
+        errorLine2="                                ~~~">
+        <location
+            file="src/main/java/androidx/camera/integration/core/CameraXService.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
         message="TransformationInfo.hasCameraTransform can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                                    mHasCameraTransform = transformationInfo.hasCameraTransform();"
         errorLine2="                                                                             ~~~~~~~~~~~~~~~~~~">
@@ -264,34 +462,34 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (E2ETestUtil.canDeviceWriteToMediaStore()) {"
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (FileUtil.canDeviceWriteToMediaStore()) {"
+        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
-        errorLine2="                                                               ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),"
+        errorLine2="                                                            ~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                            videoFileName));"
         errorLine2="                            ~~~~~~~~~~~~~">
         <location
@@ -300,52 +498,52 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
+        errorLine2="                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
-        errorLine2="                                                               ~~~~~~~~~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
+        errorLine2="                                                            ~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="                    E2ETestUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
-        errorLine2="                                                                              ~~~~~">
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="                    FileUtil.generateVideoFileOutputOptions(videoFileName, &quot;mp4&quot;));"
+        errorLine2="                                                                           ~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        E2ETestUtil.writeTextToExternalFile(information,"
-        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~">
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        FileUtil.writeTextToExternalFile(information,"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        E2ETestUtil.writeTextToExternalFile(information,"
-        errorLine2="                                            ~~~~~~~~~~~">
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        FileUtil.writeTextToExternalFile(information,"
+        errorLine2="                                         ~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateFileName(INFO_FILE_PREFIX, false), &quot;txt&quot;);"
         errorLine2="                ~~~~~~~~~~~~~~~~">
         <location
@@ -354,7 +552,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateFileName(INFO_FILE_PREFIX, false), &quot;txt&quot;);"
         errorLine2="                                                           ~~~~~">
         <location
@@ -363,36 +561,36 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (!isUnique &amp;&amp; !E2ETestUtil.isFileNameValid(prefix)) {"
-        errorLine2="                                      ~~~~~~~~~~~~~~~">
+        message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (!isUnique &amp;&amp; !FileUtil.isFileNameValid(prefix)) {"
+        errorLine2="                                   ~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (!isUnique &amp;&amp; !E2ETestUtil.isFileNameValid(prefix)) {"
-        errorLine2="                                                      ~~~~~~">
+        message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (!isUnique &amp;&amp; !FileUtil.isFileNameValid(prefix)) {"
+        errorLine2="                                                   ~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (E2ETestUtil.isFileNameValid(prefix)) {"
-        errorLine2="                        ~~~~~~~~~~~~~~~">
+        message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (FileUtil.isFileNameValid(prefix)) {"
+        errorLine2="                     ~~~~~~~~~~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
-        errorLine1="        if (E2ETestUtil.isFileNameValid(prefix)) {"
-        errorLine2="                                        ~~~~~~">
+        message="FileUtil.isFileNameValid can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        errorLine1="        if (FileUtil.isFileNameValid(prefix)) {"
+        errorLine2="                                     ~~~~~~">
         <location
             file="src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java"/>
     </issue>
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt
index 0810339..5ce4624 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXServiceTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core
 
+import android.Manifest
 import android.app.ActivityManager
 import android.app.Service
 import android.content.ComponentName
@@ -32,6 +33,9 @@
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.UseCase
 import androidx.camera.integration.core.CameraXService.ACTION_BIND_USE_CASES
+import androidx.camera.integration.core.CameraXService.ACTION_START_RECORDING
+import androidx.camera.integration.core.CameraXService.ACTION_STOP_RECORDING
+import androidx.camera.integration.core.CameraXService.ACTION_TAKE_PICTURE
 import androidx.camera.integration.core.CameraXService.EXTRA_IMAGE_ANALYSIS_ENABLED
 import androidx.camera.integration.core.CameraXService.EXTRA_IMAGE_CAPTURE_ENABLED
 import androidx.camera.integration.core.CameraXService.EXTRA_VIDEO_CAPTURE_ENABLED
@@ -45,10 +49,12 @@
 import androidx.lifecycle.Lifecycle
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
+import androidx.test.rule.GrantPermissionRule
 import androidx.testutils.LifecycleOwnerUtils
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withTimeout
 import org.junit.After
@@ -72,6 +78,13 @@
     )
 
     @get:Rule
+    val permissionRule: GrantPermissionRule =
+        GrantPermissionRule.grant(
+            Manifest.permission.WRITE_EXTERNAL_STORAGE,
+            Manifest.permission.RECORD_AUDIO,
+        )
+
+    @get:Rule
     val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
         active = implName == CameraPipeConfig::class.simpleName,
     )
@@ -105,6 +118,7 @@
     @After
     fun tearDown() {
         if (this::service.isInitialized) {
+            service.deleteSavedMediaFiles()
             context.unbindService(serviceConnection)
             context.stopService(createServiceIntent())
 
@@ -152,7 +166,7 @@
     }
 
     @Test
-    fun canReceiveAnalysisFrame() = runBlocking {
+    fun canReceiveAnalysisFrame() {
         // Arrange.
         context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
             putExtra(EXTRA_IMAGE_ANALYSIS_ENABLED, true)
@@ -165,6 +179,40 @@
         assertThat(latch.await(15, TimeUnit.SECONDS)).isTrue()
     }
 
+    @Test
+    fun canTakePicture() {
+        // Arrange.
+        context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
+            putExtra(EXTRA_IMAGE_CAPTURE_ENABLED, true)
+        })
+
+        // Act.
+        val latch = service.acquireTakePictureCountDownLatch()
+        context.startService(createServiceIntent(ACTION_TAKE_PICTURE))
+
+        // Assert.
+        assertThat(latch.await(15, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun canRecordVideo() = runBlocking {
+        // Arrange.
+        context.startService(createServiceIntent(ACTION_BIND_USE_CASES).apply {
+            putExtra(EXTRA_VIDEO_CAPTURE_ENABLED, true)
+        })
+
+        // Act.
+        val latch = service.acquireRecordVideoCountDownLatch()
+        context.startService(createServiceIntent(ACTION_START_RECORDING))
+
+        delay(3000L)
+
+        context.startService(createServiceIntent(ACTION_STOP_RECORDING))
+
+        // Assert.
+        assertThat(latch.await(15, TimeUnit.SECONDS)).isTrue()
+    }
+
     private fun createServiceIntent(action: String? = null) =
         Intent(context, CameraXService::class.java).apply {
             action?.let { setAction(it) }
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 abb5eaed..e10ff38 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
@@ -64,6 +64,7 @@
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.camera.core.impl.utils.Exif
 import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability
+import androidx.camera.core.internal.compat.workaround.InvalidJpegDataParser
 import androidx.camera.core.resolutionselector.AspectRatioStrategy
 import androidx.camera.core.resolutionselector.ResolutionFilter
 import androidx.camera.core.resolutionselector.ResolutionSelector
@@ -1802,6 +1803,89 @@
         assertThat(imageProperties.size).isEqualTo(maxHighResolutionOutputSize)
     }
 
+    /**
+     * See b/288828159 for the detailed info of the issue
+     */
+    @Test
+    fun jpegImageZeroPaddingDataDetectionTest(): Unit = runBlocking {
+        val imageCapture = ImageCapture.Builder().build()
+
+        withContext(Dispatchers.Main) {
+            cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, imageCapture)
+        }
+
+        val latch = CountdownDeferred(1)
+        var errors: Exception? = null
+
+        val callback = object : ImageCapture.OnImageCapturedCallback() {
+            override fun onCaptureSuccess(image: ImageProxy) {
+                val planes = image.planes
+                val buffer = planes[0].buffer
+                val data = ByteArray(buffer.capacity())
+                buffer.rewind()
+                buffer[data]
+
+                image.close()
+
+                val invalidJpegDataParser = InvalidJpegDataParser()
+
+                // Only checks the unnecessary zero padding data when the device is not included in
+                // the LargeJpegImageQuirk device list. InvalidJpegDataParser#getValidDataLength()
+                // should have returned the valid data length to avoid the extremely large JPEG
+                // file issue.
+                if (invalidJpegDataParser.getValidDataLength(data) == data.size &&
+                    containsZeroPaddingDataAfterEoi(data)
+                ) {
+                    errors = Exception("UNNECESSARY_JPEG_ZERO_PADDING_DATA_DETECTED!")
+                }
+
+                latch.countDown()
+            }
+
+            override fun onError(exception: ImageCaptureException) {
+                errors = exception
+                latch.countDown()
+            }
+        }
+
+        imageCapture.takePicture(mainExecutor, callback)
+
+        // Wait for the signal that the image has been captured.
+        assertThat(withTimeoutOrNull(CAPTURE_TIMEOUT) {
+            latch.await()
+        }).isNotNull()
+        assertThat(errors).isNull()
+    }
+
+    /**
+     * This util function is only used to detect the unnecessary zero padding data after EOI. It
+     * will directly return false when it fails to parse the JPEG byte array data.
+     */
+    private fun containsZeroPaddingDataAfterEoi(bytes: ByteArray): Boolean {
+        val jfifEoiMarkEndPosition = InvalidJpegDataParser.getJfifEoiMarkEndPosition(bytes)
+
+        // Directly returns false when EOI mark can't be found.
+        if (jfifEoiMarkEndPosition == -1) {
+            return false
+        }
+
+        // Will check 1mb data to know whether unnecessary zero padding data exists or not.
+        // Directly returns false when the data length is long enough
+        val dataLengthToDetect = 1_000_000
+        if (jfifEoiMarkEndPosition + dataLengthToDetect > bytes.size) {
+            return false
+        }
+
+        // Checks that there are at least continuous 1mb of unnecessary zero padding data after EOI
+        for (position in jfifEoiMarkEndPosition..jfifEoiMarkEndPosition + dataLengthToDetect) {
+            if (bytes[position] != 0x00.toByte()) {
+                return false
+            }
+        }
+
+        return true
+    }
+
     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)
diff --git a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
index 4954bfb..5ee37e2 100644
--- a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
@@ -79,10 +79,13 @@
         <service
             android:name=".CameraXService"
             android:exported="true"
-            android:foregroundServiceType="camera"
+            android:foregroundServiceType="camera|microphone"
             android:label="CameraX Service">
             <intent-filter>
                 <action android:name="androidx.camera.integration.core.intent.action.BIND_USE_CASES" />
+                <action android:name="androidx.camera.integration.core.intent.action.TAKE_PICTURE" />
+                <action android:name="androidx.camera.integration.core.intent.action.START_RECORDING" />
+                <action android:name="androidx.camera.integration.core.intent.action.STOP_RECORDING" />
             </intent-filter>
         </service>
     </application>
@@ -96,4 +99,5 @@
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
 </manifest>
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index f89839e..e20ec12 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -25,21 +25,24 @@
 import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
 import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
+import static androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore;
+import static androidx.camera.testing.impl.FileUtil.createParentFolder;
+import static androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions;
+import static androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions;
+import static androidx.camera.testing.impl.FileUtil.getAbsolutePathFromUri;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE;
+import static java.util.Objects.requireNonNull;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
-import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
-import android.database.Cursor;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.display.DisplayManager;
 import android.media.MediaScannerConnection;
@@ -126,8 +129,6 @@
 import androidx.camera.video.VideoCapabilities;
 import androidx.camera.video.VideoCapture;
 import androidx.camera.video.VideoRecordEvent;
-import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite;
 import androidx.core.content.ContextCompat;
 import androidx.core.math.MathUtils;
 import androidx.core.util.Consumer;
@@ -320,6 +321,8 @@
     private Button mZoomIn2XToggle;
     private Button mZoomResetToggle;
     private Toast mEvToast = null;
+    private Toast mPSToast = null;
+    private ToggleButton mPreviewStabilizationToggle;
 
     private OpenGLRenderer mPreviewRenderer;
     private DisplayManager.DisplayListener mDisplayListener;
@@ -328,6 +331,7 @@
     private DynamicRange mDynamicRange = DynamicRange.SDR;
     private final Set<DynamicRange> mSelectableDynamicRanges = new HashSet<>();
     private int mVideoMirrorMode = MIRROR_MODE_ON_FRONT_ONLY;
+    private boolean mIsPreviewStabilizationOn = false;
 
     SessionMediaUriSet mSessionImagesUriSet = new SessionMediaUriSet();
     SessionMediaUriSet mSessionVideosUriSet = new SessionMediaUriSet();
@@ -357,6 +361,9 @@
 
         @Override
         public void onSuccess(@Nullable Integer result) {
+            if (result == null) {
+                return;
+            }
             CameraInfo cameraInfo = getCameraInfo();
             if (cameraInfo != null) {
                 ExposureState exposureState = cameraInfo.getExposureState();
@@ -478,7 +485,7 @@
     @VisibleForTesting
     public void resetViewIdlingResource() {
         mPreviewFrameCount.set(0);
-        // Make the view idling resource non-idle, until required framecount achieved.
+        // Make the view idling resource non-idle, until required frame count achieved.
         if (mViewIdlingResource.isIdleNow()) {
             mViewIdlingResource.increment();
         }
@@ -596,14 +603,17 @@
                 case IDLE:
                     createDefaultVideoFolderIfNotExist();
                     final PendingRecording pendingRecording;
-                    if (DeviceQuirks.get(MediaStoreVideoCannotWrite.class) != null) {
-                        // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
-                        pendingRecording = getVideoCapture().getOutput().prepareRecording(
-                                this, getNewVideoFileOutputOptions());
-                    } else {
+                    String fileName = "video_" + System.currentTimeMillis();
+                    String extension = "mp4";
+                    if (canDeviceWriteToMediaStore()) {
                         // Use MediaStoreOutputOptions for public share media storage.
                         pendingRecording = getVideoCapture().getOutput().prepareRecording(
-                                this, getNewVideoOutputMediaStoreOptions());
+                                this,
+                                generateVideoMediaStoreOptions(getContentResolver(), fileName));
+                    } else {
+                        // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
+                        pendingRecording = getVideoCapture().getOutput().prepareRecording(
+                                this, generateVideoFileOutputOptions(fileName, extension));
                     }
 
                     resetVideoSavedIdlingResource();
@@ -806,32 +816,6 @@
         }
     }
 
-    @NonNull
-    private MediaStoreOutputOptions getNewVideoOutputMediaStoreOptions() {
-        String videoFileName = "video_" + System.currentTimeMillis();
-        ContentValues contentValues = new ContentValues();
-        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
-        contentValues.put(MediaStore.Video.Media.TITLE, videoFileName);
-        contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
-        contentValues.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
-        contentValues.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
-        return new MediaStoreOutputOptions.Builder(getContentResolver(),
-                MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
-                .setContentValues(contentValues)
-                .build();
-    }
-
-    @NonNull
-    private FileOutputOptions getNewVideoFileOutputOptions() {
-        String videoFileName = "video_" + System.currentTimeMillis() + ".mp4";
-        File videoFolder = Environment.getExternalStoragePublicDirectory(
-                Environment.DIRECTORY_MOVIES);
-        if (!videoFolder.exists() && !videoFolder.mkdirs()) {
-            Log.e(TAG, "Failed to create directory: " + videoFolder);
-        }
-        return new FileOutputOptions.Builder(new File(videoFolder, videoFileName)).build();
-    }
-
     private void updateRecordingStats(@NonNull RecordingStats stats) {
         double durationMs = TimeUnit.NANOSECONDS.toMillis(stats.getRecordedDurationNanos());
         // Show megabytes in International System of Units (SI)
@@ -890,7 +874,7 @@
                                                 Toast.LENGTH_SHORT).show());
                                         if (mSessionImagesUriSet != null) {
                                             mSessionImagesUriSet.add(
-                                                    Objects.requireNonNull(
+                                                    requireNonNull(
                                                             outputFileResults.getSavedUri()));
                                         }
                                     }
@@ -956,7 +940,7 @@
                     Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
                 }
             } catch (IllegalArgumentException e) {
-                Toast.makeText(this, "Failed to swich Camera. Error:" + e.getMessage(),
+                Toast.makeText(this, "Failed to switch Camera. Error:" + e.getMessage(),
                         Toast.LENGTH_SHORT).show();
             }
         });
@@ -1008,8 +992,8 @@
 
     private void setUpTorchButton() {
         mTorchButton.setOnClickListener(v -> {
-            Objects.requireNonNull(getCameraInfo());
-            Objects.requireNonNull(getCameraControl());
+            requireNonNull(getCameraInfo());
+            requireNonNull(getCameraControl());
             Integer torchState = getCameraInfo().getTorchState().getValue();
             boolean toggledState = !Objects.equals(torchState, TorchState.ON);
             Log.d(TAG, "Set camera torch: " + toggledState);
@@ -1029,8 +1013,8 @@
 
     private void setUpEVButton() {
         mPlusEV.setOnClickListener(v -> {
-            Objects.requireNonNull(getCameraInfo());
-            Objects.requireNonNull(getCameraControl());
+            requireNonNull(getCameraInfo());
+            requireNonNull(getCameraControl());
 
             ExposureState exposureState = getCameraInfo().getExposureState();
             Range<Integer> range = exposureState.getExposureCompensationRange();
@@ -1048,8 +1032,8 @@
         });
 
         mDecEV.setOnClickListener(v -> {
-            Objects.requireNonNull(getCameraInfo());
-            Objects.requireNonNull(getCameraControl());
+            requireNonNull(getCameraInfo());
+            requireNonNull(getCameraControl());
 
             ExposureState exposureState = getCameraInfo().getExposureState();
             Range<Integer> range = exposureState.getExposureCompensationRange();
@@ -1075,6 +1059,14 @@
         mEvToast.show();
     }
 
+    void showPreviewStabilizationToast(String message) {
+        if (mPSToast != null) {
+            mPSToast.cancel();
+        }
+        mPSToast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT);
+        mPSToast.show();
+    }
+
     private void updateAppUIForE2ETest(@NonNull String testCase) {
         if (getSupportActionBar() != null) {
             getSupportActionBar().hide();
@@ -1120,19 +1112,19 @@
                 ViewGroup.LayoutParams lp = viewFinderStub.getLayoutParams();
                 if (orientation == Configuration.ORIENTATION_PORTRAIT) {
                     lp.width = displayMetrics.widthPixels;
-                    lp.height = (int) (displayMetrics.widthPixels / ratio.getDenominator()
-                            * ratio.getNumerator());
+                    lp.height = displayMetrics.widthPixels / ratio.getDenominator()
+                            * ratio.getNumerator();
                 } else {
                     lp.height = displayMetrics.heightPixels;
-                    lp.width = (int) (displayMetrics.heightPixels / ratio.getDenominator()
-                            * ratio.getNumerator());
+                    lp.width = displayMetrics.heightPixels / ratio.getDenominator()
+                            * ratio.getNumerator();
                 }
                 viewFinderStub.setLayoutParams(lp);
             }
         }
     }
 
-    @SuppressLint("NullAnnotationGroup")
+    @SuppressLint({"NullAnnotationGroup", "RestrictedApiAndroidX"})
     @OptIn(markerClass = androidx.camera.core.ExperimentalZeroShutterLag.class)
     private void updateButtonsUi() {
         mRecordUi.setEnabled(mVideoToggle.isChecked());
@@ -1142,6 +1134,8 @@
                 && getCameraInfo().isZslSupported() ? View.VISIBLE : View.GONE);
         mZslToggle.setEnabled(mPhotoToggle.isChecked());
         mCameraDirectionButton.setEnabled(getCameraInfo() != null);
+        mPreviewStabilizationToggle.setEnabled(mCamera != null
+                && mCamera.getCameraInfo().getPreviewCapabilities().isStabilizationSupported());
         mTorchButton.setEnabled(isFlashAvailable());
         // Flash button
         mFlashButton.setEnabled(mPhotoToggle.isChecked() && isFlashAvailable());
@@ -1184,6 +1178,7 @@
         setUpTorchButton();
         setUpEVButton();
         setUpZoomButton();
+        setUpPreviewStabilizationButton();
         mCaptureQualityToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
         mZslToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
     }
@@ -1279,6 +1274,7 @@
         mPlusEV = findViewById(R.id.plus_ev_toggle);
         mDecEV = findViewById(R.id.dec_ev_toggle);
         mZslToggle = findViewById(R.id.zsl_toggle);
+        mPreviewStabilizationToggle = findViewById(R.id.preview_stabilization);
         mZoomSeekBar = findViewById(R.id.seekBar);
         mZoomRatioLabel = findViewById(R.id.zoomRatio);
         mZoomIn2XToggle = findViewById(R.id.zoom_in_2x_toggle);
@@ -1329,7 +1325,7 @@
         };
 
         DisplayManager dpyMgr =
-                Objects.requireNonNull((DisplayManager) getSystemService(Context.DISPLAY_SERVICE));
+                requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class));
         dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper()));
 
         StrictMode.VmPolicy vmPolicy =
@@ -1446,7 +1442,7 @@
     public void onDestroy() {
         super.onDestroy();
         DisplayManager dpyMgr =
-                Objects.requireNonNull((DisplayManager) getSystemService(Context.DISPLAY_SERVICE));
+                requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class));
         dpyMgr.unregisterDisplayListener(mDisplayListener);
         mPreviewRenderer.shutdown();
         mImageCaptureExecutorService.shutdown();
@@ -1577,23 +1573,22 @@
         // Remove ImageAnalysis to check whether the new use cases combination can be supported.
         if (mAnalysisToggle.isChecked()) {
             mAnalysisToggle.setChecked(false);
-            if (isCheckedUseCasesCombinationSupported()) {
-                return;
-            }
+            // No need to do further use case combination check since Preview + ImageCapture
+            // should be always supported.
         }
-
-        // Preview + ImageCapture should be always supported.
     }
 
     /**
      * Builds all use cases based on current settings and return as an array.
      */
+    @SuppressLint("RestrictedApiAndroidX")
     private List<UseCase> buildUseCases() {
         List<UseCase> useCases = new ArrayList<>();
         if (mPreviewToggle.isChecked()) {
             Preview preview = new Preview.Builder()
                     .setTargetName("Preview")
                     .setTargetAspectRatio(mTargetAspectRatio)
+                    .setPreviewStabilizationEnabled(mIsPreviewStabilizationOn)
                     .build();
             resetViewIdlingResource();
             // Use the listener of the future to make sure the Preview setup the new surface.
@@ -1687,7 +1682,7 @@
                             new ActivityResultContracts.RequestMultiplePermissions(),
                             result -> {
                                 for (String permission : REQUIRED_PERMISSIONS) {
-                                    if (!Objects.requireNonNull(result.get(permission))) {
+                                    if (!requireNonNull(result.get(permission))) {
                                         Toast.makeText(getApplicationContext(),
                                                         "Camera permission denied.",
                                                         Toast.LENGTH_SHORT)
@@ -1720,10 +1715,8 @@
     void createDefaultPictureFolderIfNotExist() {
         File pictureFolder = Environment.getExternalStoragePublicDirectory(
                 Environment.DIRECTORY_PICTURES);
-        if (!pictureFolder.exists()) {
-            if (!pictureFolder.mkdir()) {
-                Log.e(TAG, "Failed to create directory: " + pictureFolder);
-            }
+        if (createParentFolder(pictureFolder)) {
+            Log.e(TAG, "Failed to create directory: " + pictureFolder);
         }
     }
 
@@ -1732,17 +1725,8 @@
         String videoFilePath =
                 getAbsolutePathFromUri(getApplicationContext().getContentResolver(),
                         MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
-
-        // If cannot get the video path, just skip checking and create folder.
-        if (videoFilePath == null) {
-            return;
-        }
-        File videoFile = new File(videoFilePath);
-
-        if (videoFile.getParentFile() != null && !videoFile.getParentFile().exists()) {
-            if (!videoFile.getParentFile().mkdir()) {
-                Log.e(TAG, "Failed to create directory: " + videoFile);
-            }
+        if (videoFilePath == null || !createParentFolder(videoFilePath)) {
+            Log.e(TAG, "Failed to create parent directory for: " + videoFilePath);
         }
     }
 
@@ -1778,13 +1762,14 @@
     ScaleGestureDetector.SimpleOnScaleGestureListener mScaleGestureListener =
             new ScaleGestureDetector.SimpleOnScaleGestureListener() {
                 @Override
-                public boolean onScale(ScaleGestureDetector detector) {
+                public boolean onScale(@NonNull ScaleGestureDetector detector) {
                     if (mCamera == null) {
                         return true;
                     }
 
                     CameraInfo cameraInfo = mCamera.getCameraInfo();
-                    float newZoom = cameraInfo.getZoomState().getValue().getZoomRatio()
+                    float newZoom =
+                            requireNonNull(cameraInfo.getZoomState().getValue()).getZoomRatio()
                             * detector.getScaleFactor();
                     setZoomRatio(newZoom);
                     return true;
@@ -1794,7 +1779,7 @@
     GestureDetector.OnGestureListener onTapGestureListener =
             new GestureDetector.SimpleOnGestureListener() {
                 @Override
-                public boolean onSingleTapUp(MotionEvent e) {
+                public boolean onSingleTapUp(@NonNull MotionEvent e) {
                     if (mCamera == null) {
                         return false;
                     }
@@ -1834,7 +1819,8 @@
 
         mZoomSeekBar.setMax(MAX_SEEKBAR_VALUE);
         mZoomSeekBar.setProgress(
-                (int) (cameraInfo.getZoomState().getValue().getLinearZoom() * MAX_SEEKBAR_VALUE));
+                (int) (requireNonNull(cameraInfo.getZoomState().getValue()).getLinearZoom()
+                        * MAX_SEEKBAR_VALUE));
         mZoomSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
             @Override
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
@@ -1884,7 +1870,7 @@
     private boolean is2XZoomSupported() {
         CameraInfo cameraInfo = getCameraInfo();
         return cameraInfo != null
-                && cameraInfo.getZoomState().getValue().getMaxZoomRatio() >= 2.0f;
+                && requireNonNull(cameraInfo.getZoomState().getValue()).getMaxZoomRatio() >= 2.0f;
     }
 
     private void setUpZoomButton() {
@@ -1893,6 +1879,16 @@
         mZoomResetToggle.setOnClickListener(v -> setZoomRatio(1.0f));
     }
 
+    private void setUpPreviewStabilizationButton() {
+        mPreviewStabilizationToggle.setOnClickListener(v -> {
+            mIsPreviewStabilizationOn = !mIsPreviewStabilizationOn;
+            if (mIsPreviewStabilizationOn) {
+                showPreviewStabilizationToast("Preview Stabilization On, FOV changes");
+            }
+            tryBindUseCases();
+        });
+    }
+
     void setZoomRatio(float newZoom) {
         if (mCamera == null) {
             return;
@@ -1901,7 +1897,7 @@
         CameraInfo cameraInfo = mCamera.getCameraInfo();
         CameraControl cameraControl = mCamera.getCameraControl();
         float clampedNewZoom = MathUtils.clamp(newZoom,
-                cameraInfo.getZoomState().getValue().getMinZoomRatio(),
+                requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio(),
                 cameraInfo.getZoomState().getValue().getMaxZoomRatio());
 
         Log.d(TAG, "setZoomRatio ratio: " + clampedNewZoom);
@@ -1932,34 +1928,6 @@
         });
     }
 
-    /** Gets the absolute path from a Uri. */
-    @Nullable
-    public String getAbsolutePathFromUri(@NonNull ContentResolver resolver,
-            @NonNull Uri contentUri) {
-        Cursor cursor = null;
-        try {
-            // We should include in any Media collections.
-            String[] proj;
-            int columnIndex;
-            // MediaStore.Video.Media.DATA was deprecated in API level 29.
-            proj = new String[]{MediaStore.Video.Media.DATA};
-            cursor = resolver.query(contentUri, proj, null, null, null);
-            columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
-
-            cursor.moveToFirst();
-            return cursor.getString(columnIndex);
-        } catch (RuntimeException e) {
-            Log.e(TAG, String.format(
-                    "Failed in getting absolute path for Uri %s with Exception %s",
-                    contentUri, e));
-            return "";
-        } finally {
-            if (cursor != null) {
-                cursor.close();
-            }
-        }
-    }
-
     private class SessionMediaUriSet {
         private final Set<Uri> mSessionMediaUris;
 
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java
index 45f7201..ac8e9cf 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXService.java
@@ -17,15 +17,31 @@
 package androidx.camera.integration.core;
 
 import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+import static androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore;
+import static androidx.camera.testing.impl.FileUtil.createParentFolder;
+import static androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions;
+import static androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions;
+import static androidx.camera.testing.impl.FileUtil.getAbsolutePathFromUri;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
+import android.content.ContentValues;
 import android.content.Intent;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
+import android.os.Environment;
 import android.os.IBinder;
+import android.os.SystemClock;
+import android.provider.MediaStore;
 import android.util.Log;
 
 import androidx.annotation.DoNotInline;
@@ -35,11 +51,18 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.ImageAnalysis;
 import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.UseCaseGroup;
 import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.video.FileOutputOptions;
+import androidx.camera.video.MediaStoreOutputOptions;
+import androidx.camera.video.OutputOptions;
+import androidx.camera.video.PendingRecording;
 import androidx.camera.video.Recorder;
+import androidx.camera.video.Recording;
 import androidx.camera.video.VideoCapture;
+import androidx.camera.video.VideoRecordEvent;
 import androidx.core.app.NotificationCompat;
 import androidx.core.content.ContextCompat;
 import androidx.core.util.Consumer;
@@ -47,9 +70,18 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.io.File;
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 
@@ -64,6 +96,12 @@
     // Actions
     public static final String ACTION_BIND_USE_CASES =
             "androidx.camera.integration.core.intent.action.BIND_USE_CASES";
+    public static final String ACTION_TAKE_PICTURE =
+            "androidx.camera.integration.core.intent.action.TAKE_PICTURE";
+    public static final String ACTION_START_RECORDING =
+            "androidx.camera.integration.core.intent.action.START_RECORDING";
+    public static final String ACTION_STOP_RECORDING =
+            "androidx.camera.integration.core.intent.action.STOP_RECORDING";
 
     // Extras
     public static final String EXTRA_VIDEO_CAPTURE_ENABLED = "EXTRA_VIDEO_CAPTURE_ENABLED";
@@ -73,12 +111,26 @@
     private final IBinder mBinder = new CameraXServiceBinder();
 
     ////////////////////////////////////////////////////////////////////////////////////////////////
+    //                          Members only accessed on main thread                              //
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    private final Map<Class<?>, UseCase> mBoundUseCases = new HashMap<>();
+    @Nullable
+    private Recording mActiveRecording;
+    //--------------------------------------------------------------------------------------------//
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
     //                                   Members for testing                                      //
     ////////////////////////////////////////////////////////////////////////////////////////////////
+    private final Set<Uri> mSavedMediaUri = new HashSet<>();
+
     @Nullable
     private Consumer<Collection<UseCase>> mOnUseCaseBoundCallback;
     @Nullable
     private CountDownLatch mAnalysisFrameLatch;
+    @Nullable
+    private CountDownLatch mTakePictureLatch;
+    @Nullable
+    private CountDownLatch mRecordVideoLatch;
     //--------------------------------------------------------------------------------------------//
 
     @Override
@@ -101,6 +153,12 @@
             Log.d(TAG, "onStartCommand: action = " + action + ", extras = " + intent.getExtras());
             if (ACTION_BIND_USE_CASES.equals(action)) {
                 bindToLifecycle(intent);
+            } else if (ACTION_TAKE_PICTURE.equals(action)) {
+                takePicture();
+            } else if (ACTION_START_RECORDING.equals(action)) {
+                startRecording();
+            } else if (ACTION_STOP_RECORDING.equals(action)) {
+                stopRecording();
             }
         }
         return super.onStartCommand(intent, flags, startId);
@@ -125,6 +183,7 @@
             throw new IllegalStateException(e);
         }
         cameraProvider.unbindAll();
+        mBoundUseCases.clear();
         UseCaseGroup useCaseGroup = resolveUseCaseGroup(intent);
         List<UseCase> boundUseCases = Collections.emptyList();
         if (useCaseGroup != null) {
@@ -136,6 +195,9 @@
             }
         }
         Log.d(TAG, "Bound UseCases: " + boundUseCases);
+        for (UseCase boundUseCase : boundUseCases) {
+            mBoundUseCases.put(boundUseCase.getClass(), boundUseCase);
+        }
         if (mOnUseCaseBoundCallback != null) {
             mOnUseCaseBoundCallback.accept(boundUseCases);
         }
@@ -183,6 +245,113 @@
         return checkNotNull(ContextCompat.getSystemService(this, NotificationManager.class));
     }
 
+    @Nullable
+    private ImageCapture getImageCapture() {
+        return (ImageCapture) mBoundUseCases.get(ImageCapture.class);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    private VideoCapture<Recorder> getVideoCapture() {
+        return (VideoCapture<Recorder>) mBoundUseCases.get(VideoCapture.class);
+    }
+
+    private void takePicture() {
+        ImageCapture imageCapture = getImageCapture();
+        if (imageCapture == null) {
+            Log.w(TAG, "ImageCapture is not bound.");
+            return;
+        }
+        createDefaultPictureFolderIfNotExist();
+        Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US);
+        String fileName = "ServiceTestApp-" + formatter.format(Calendar.getInstance().getTime())
+                + ".jpg";
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
+        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
+        ImageCapture.OutputFileOptions outputFileOptions =
+                new ImageCapture.OutputFileOptions.Builder(
+                        getContentResolver(),
+                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                        contentValues).build();
+        long startTimeMs = SystemClock.elapsedRealtime();
+        imageCapture.takePicture(outputFileOptions,
+                ContextCompat.getMainExecutor(this),
+                new ImageCapture.OnImageSavedCallback() {
+                    @Override
+                    public void onImageSaved(
+                            @NonNull ImageCapture.OutputFileResults outputFileResults) {
+                        long durationMs = SystemClock.elapsedRealtime() - startTimeMs;
+                        Log.d(TAG, "Saved image " + outputFileResults.getSavedUri()
+                                + "  (" + durationMs + " ms)");
+                        mSavedMediaUri.add(outputFileResults.getSavedUri());
+                        if (mTakePictureLatch != null) {
+                            mTakePictureLatch.countDown();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull ImageCaptureException exception) {
+                        Log.e(TAG, "Failed to save image by " + exception.getImageCaptureError(),
+                                exception);
+                    }
+                });
+    }
+
+    private void createDefaultPictureFolderIfNotExist() {
+        File pictureFolder = Environment.getExternalStoragePublicDirectory(
+                Environment.DIRECTORY_PICTURES);
+        if (!createParentFolder(pictureFolder)) {
+            Log.e(TAG, "Failed to create directory: " + pictureFolder);
+        }
+    }
+
+    private void startRecording() {
+        VideoCapture<Recorder> videoCapture = getVideoCapture();
+        if (videoCapture == null) {
+            Log.w(TAG, "VideoCapture is not bound.");
+            return;
+        }
+
+        createDefaultVideoFolderIfNotExist();
+        if (mActiveRecording == null) {
+            PendingRecording pendingRecording;
+            String fileName = "video_" + System.currentTimeMillis();
+            String extension = "mp4";
+            if (canDeviceWriteToMediaStore()) {
+                // Use MediaStoreOutputOptions for public share media storage.
+                pendingRecording = getVideoCapture().getOutput().prepareRecording(
+                        this,
+                        generateVideoMediaStoreOptions(getContentResolver(), fileName));
+            } else {
+                // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
+                pendingRecording = getVideoCapture().getOutput().prepareRecording(
+                        this, generateVideoFileOutputOptions(fileName, extension));
+            }
+            //noinspection MissingPermission
+            mActiveRecording = pendingRecording
+                    .withAudioEnabled()
+                    .start(ContextCompat.getMainExecutor(this), mRecordingListener);
+        } else {
+            Log.e(TAG, "It should stop the active recording before start a new one.");
+        }
+    }
+
+    private void stopRecording() {
+        if (mActiveRecording != null) {
+            mActiveRecording.stop();
+            mActiveRecording = null;
+        }
+    }
+
+    private void createDefaultVideoFolderIfNotExist() {
+        String videoFilePath = getAbsolutePathFromUri(getContentResolver(),
+                MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
+        if (videoFilePath == null || !createParentFolder(videoFilePath)) {
+            Log.e(TAG, "Failed to create parent directory for: " + videoFilePath);
+        }
+    }
+
     private final ImageAnalysis.Analyzer mAnalyzer = image -> {
         if (mAnalysisFrameLatch != null) {
             mAnalysisFrameLatch.countDown();
@@ -190,6 +359,58 @@
         image.close();
     };
 
+    private final Consumer<VideoRecordEvent> mRecordingListener = event -> {
+        if (event instanceof VideoRecordEvent.Finalize) {
+            VideoRecordEvent.Finalize finalize = (VideoRecordEvent.Finalize) event;
+
+            switch (finalize.getError()) {
+                case ERROR_NONE:
+                case ERROR_FILE_SIZE_LIMIT_REACHED:
+                case ERROR_DURATION_LIMIT_REACHED:
+                case ERROR_INSUFFICIENT_STORAGE:
+                case ERROR_SOURCE_INACTIVE:
+                    Uri uri = finalize.getOutputResults().getOutputUri();
+                    OutputOptions outputOptions = finalize.getOutputOptions();
+                    String msg;
+                    String videoFilePath;
+                    if (outputOptions instanceof MediaStoreOutputOptions) {
+                        msg = "Saved video " + uri;
+                        videoFilePath = getAbsolutePathFromUri(
+                                getApplicationContext().getContentResolver(),
+                                uri
+                        );
+                    } else if (outputOptions instanceof FileOutputOptions) {
+                        videoFilePath = ((FileOutputOptions) outputOptions).getFile().getPath();
+                        MediaScannerConnection.scanFile(this,
+                                new String[]{videoFilePath}, null,
+                                (path, uri1) -> Log.i(TAG, "Scanned " + path + " -> uri= " + uri1));
+                        msg = "Saved video " + videoFilePath;
+                    } else {
+                        throw new AssertionError("Unknown or unsupported OutputOptions type: "
+                                + outputOptions.getClass().getSimpleName());
+                    }
+                    // The video file path is used in tracing e2e test log. Don't remove it.
+                    Log.d(TAG, "Saved video file: " + videoFilePath);
+
+                    if (finalize.getError() != ERROR_NONE) {
+                        msg += " with code (" + finalize.getError() + ")";
+                    }
+                    Log.d(TAG, msg, finalize.getCause());
+
+                    mSavedMediaUri.add(uri);
+                    if (mRecordVideoLatch != null) {
+                        mRecordVideoLatch.countDown();
+                    }
+                    break;
+                default:
+                    String errMsg = "Video capture failed by (" + finalize.getError() + "): "
+                            + finalize.getCause();
+                    Log.e(TAG, errMsg, finalize.getCause());
+            }
+            mActiveRecording = null;
+        }
+    };
+
     @RequiresApi(26)
     static class Api26Impl {
 
@@ -217,11 +438,41 @@
     }
 
     @VisibleForTesting
+    @NonNull
     CountDownLatch acquireAnalysisFrameCountDownLatch() {
         mAnalysisFrameLatch = new CountDownLatch(3);
         return mAnalysisFrameLatch;
     }
 
+    @VisibleForTesting
+    @NonNull
+    CountDownLatch acquireTakePictureCountDownLatch() {
+        mTakePictureLatch = new CountDownLatch(1);
+        return mTakePictureLatch;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    CountDownLatch acquireRecordVideoCountDownLatch() {
+        mRecordVideoLatch = new CountDownLatch(1);
+        return mRecordVideoLatch;
+    }
+
+    @VisibleForTesting
+    void deleteSavedMediaFiles() {
+        deleteUriSet(mSavedMediaUri);
+    }
+
+    private void deleteUriSet(@NonNull Set<Uri> uriSet) {
+        for (Uri uri : uriSet) {
+            try {
+                getContentResolver().delete(uri, null, null);
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Unable to delete uri: " + uri, e);
+            }
+        }
+    }
+
     class CameraXServiceBinder extends Binder {
         @NonNull
         CameraXService getService() {
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
index 61a1f09..620f7e0 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
@@ -107,7 +107,7 @@
     @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
     @MainThread
     public static boolean isCameraProviderUnInitializedOrSameAsParameter(
-            @NonNull String cameraImplementation) {
+            @Nullable String cameraImplementation) {
 
         if (sConfiguredCameraXCameraImplementation == null) {
             return true;
@@ -116,11 +116,7 @@
                 sConfiguredCameraXCameraImplementation);
         cameraImplementation = getCameraProviderName(cameraImplementation);
 
-        if (currentCameraProvider.equals(cameraImplementation)) {
-            return true;
-        }
-
-        return false;
+        return currentCameraProvider.equals(cameraImplementation);
     }
 
     /**
@@ -129,7 +125,7 @@
      */
     @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
     @MainThread
-    private static String getCameraProviderName(String mCameraProvider) {
+    private static String getCameraProviderName(@Nullable String mCameraProvider) {
         if (mCameraProvider == null) {
             mCameraProvider = CAMERA2_IMPLEMENTATION_OPTION;
         }
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
index c775b3d..976b5f3 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoCameraSwitchingActivity.java
@@ -33,7 +33,7 @@
 import androidx.camera.core.Logger;
 import androidx.camera.core.Preview;
 import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.camera.testing.impl.E2ETestUtil;
+import androidx.camera.testing.impl.FileUtil;
 import androidx.camera.video.ExperimentalPersistentRecording;
 import androidx.camera.video.PendingRecording;
 import androidx.camera.video.Recorder;
@@ -232,14 +232,14 @@
 
         final String videoFileName = generateFileName(VIDEO_FILE_PREFIX, true);
         final PendingRecording pendingRecording;
-        if (E2ETestUtil.canDeviceWriteToMediaStore()) {
+        if (FileUtil.canDeviceWriteToMediaStore()) {
             // Use MediaStoreOutputOptions for public share media storage.
             pendingRecording = mVideoCapture.getOutput().prepareRecording(this,
-                    E2ETestUtil.generateVideoMediaStoreOptions(this.getContentResolver(),
+                    FileUtil.generateVideoMediaStoreOptions(this.getContentResolver(),
                             videoFileName));
         } else {
             pendingRecording = mVideoCapture.getOutput().prepareRecording(this,
-                    E2ETestUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));
+                    FileUtil.generateVideoFileOutputOptions(videoFileName, "mp4"));
         }
         mRecording = pendingRecording
                 .asPersistentRecording() // Perform the recording as a persistent recording.
@@ -274,17 +274,17 @@
 
     private void exportTestInformation() {
         String information = KEY_DEVICE_ORIENTATION + ": " + mDeviceOrientation;
-        E2ETestUtil.writeTextToExternalFile(information,
+        FileUtil.writeTextToExternalFile(information,
                 generateFileName(INFO_FILE_PREFIX, false), "txt");
     }
 
     @NonNull
     private String generateFileName(@Nullable String prefix, boolean isUnique) {
-        if (!isUnique && !E2ETestUtil.isFileNameValid(prefix)) {
+        if (!isUnique && !FileUtil.isFileNameValid(prefix)) {
             throw new IllegalArgumentException("Invalid arguments for generating file name.");
         }
         StringBuilder fileName = new StringBuilder();
-        if (E2ETestUtil.isFileNameValid(prefix)) {
+        if (FileUtil.isFileNameValid(prefix)) {
             fileName.append(prefix);
             if (isUnique) {
                 fileName.append("_");
diff --git a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
index a1e8f19..53613a2 100644
--- a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
@@ -308,6 +308,20 @@
         app:layout_constraintLeft_toRightOf="@id/plus_ev_toggle"
         app:layout_constraintTop_toBottomOf="@id/VideoToggle" />
 
+    <ToggleButton
+        android:id="@+id/preview_stabilization"
+        android:layout_width="46dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="5dp"
+        android:layout_marginTop="1dp"
+        android:background="@android:drawable/btn_default"
+        android:textOff="@string/toggle_preview_stabilization_off"
+        android:textOn="@string/toggle_preview_stabilization_on"
+        android:textSize="11dp"
+        android:translationZ="1dp"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/direction_toggle" />
+
     <Button
         android:id="@+id/video_quality"
         android:layout_width="46dp"
diff --git a/camera/integration-tests/coretestapp/src/main/res/values/donottranslate-strings.xml b/camera/integration-tests/coretestapp/src/main/res/values/donottranslate-strings.xml
index de7293e..65335ef 100644
--- a/camera/integration-tests/coretestapp/src/main/res/values/donottranslate-strings.xml
+++ b/camera/integration-tests/coretestapp/src/main/res/values/donottranslate-strings.xml
@@ -30,6 +30,8 @@
     <string name="toggle_capture_quality_off">MIN</string>
     <string name="toggle_capture_zsl_on">ZSL ON</string>
     <string name="toggle_capture_zsl_off">ZSL OFF</string>
+    <string name="toggle_preview_stabilization_on">PS ON</string>
+    <string name="toggle_preview_stabilization_off">PS OFF</string>
     <string name="toggle_plus_ev">+EV</string>
     <string name="toggle_dec_ev">-EV</string>
     <string name="toggle_zoom_in_2x">2X</string>
diff --git a/camera/integration-tests/viewtestapp/lint-baseline.xml b/camera/integration-tests/viewtestapp/lint-baseline.xml
index acea7110..cfacbf5 100644
--- a/camera/integration-tests/viewtestapp/lint-baseline.xml
+++ b/camera/integration-tests/viewtestapp/lint-baseline.xml
@@ -264,7 +264,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.canDeviceWriteToMediaStore can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        return if (canDeviceWriteToMediaStore()) {"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -273,7 +273,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateVideoMediaStoreOptions(context.contentResolver, fileName)"
         errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -282,7 +282,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateVideoMediaStoreOptions(context.contentResolver, fileName)"
         errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -291,7 +291,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoMediaStoreOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="                generateVideoMediaStoreOptions(context.contentResolver, fileName)"
         errorLine2="                                                                        ~~~~~~~~">
         <location
@@ -300,7 +300,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="            recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName))"
         errorLine2="                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -309,7 +309,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.generateVideoFileOutputOptions can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="            recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName))"
         errorLine2="                                                                              ~~~~~~~~">
         <location
@@ -318,7 +318,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        writeTextToExternalFile(information, fileName)"
         errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -327,7 +327,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        writeTextToExternalFile(information, fileName)"
         errorLine2="                                ~~~~~~~~~~~">
         <location
@@ -336,7 +336,7 @@
 
     <issue
         id="RestrictedApiAndroidX"
-        message="E2ETestUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
+        message="FileUtil.writeTextToExternalFile can only be called from within the same library group (referenced groupId=`androidx.camera` from groupId=`androidx.camera.integration-tests`)"
         errorLine1="        writeTextToExternalFile(information, fileName)"
         errorLine2="                                             ~~~~~~~~">
         <location
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
index a593ec0..6577ce6 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/StreamSharingActivity.kt
@@ -33,10 +33,10 @@
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.testing.impl.E2ETestUtil.canDeviceWriteToMediaStore
-import androidx.camera.testing.impl.E2ETestUtil.generateVideoFileOutputOptions
-import androidx.camera.testing.impl.E2ETestUtil.generateVideoMediaStoreOptions
-import androidx.camera.testing.impl.E2ETestUtil.writeTextToExternalFile
+import androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore
+import androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions
+import androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions
+import androidx.camera.testing.impl.FileUtil.writeTextToExternalFile
 import androidx.camera.video.PendingRecording
 import androidx.camera.video.Recorder
 import androidx.camera.video.Recording
diff --git a/car/app/app-samples/navigation/automotive/build.gradle b/car/app/app-samples/navigation/automotive/build.gradle
index 531198c..9984ce7 100644
--- a/car/app/app-samples/navigation/automotive/build.gradle
+++ b/car/app/app-samples/navigation/automotive/build.gradle
@@ -26,6 +26,7 @@
     defaultConfig {
         applicationId "androidx.car.app.sample.navigation"
         minSdkVersion 29
+        targetSdkVersion 33
         versionCode 1
         versionName "1.0"
     }
diff --git a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
index 47c9404..ee2be84 100644
--- a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -37,6 +37,12 @@
   <uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
   <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+  <!-- For the Microphone Recording demos. -->
+  <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
   <!-- Various required feature settings for an automotive app. -->
   <uses-feature
       android:name="android.hardware.type.automotive"
diff --git a/car/app/app-samples/navigation/mobile/build.gradle b/car/app/app-samples/navigation/mobile/build.gradle
index 5e80268..ece98f3 100644
--- a/car/app/app-samples/navigation/mobile/build.gradle
+++ b/car/app/app-samples/navigation/mobile/build.gradle
@@ -25,6 +25,7 @@
     defaultConfig {
         applicationId "androidx.car.app.sample.navigation"
         minSdkVersion 23
+        targetSdkVersion 33
         versionCode 1
         versionName "1.0"
     }
diff --git a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
index e2ec81f..d9473ac 100644
--- a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -39,6 +39,8 @@
 
     <!-- For Microphone Recording -->
     <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+
+    <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
     <application
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
index 83fb3b2..28758d6 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
@@ -24,6 +24,9 @@
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
   <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
   <!-- For PlaceListMapTemplate -->
   <uses-permission android:name="androidx.car.app.MAP_TEMPLATES"/>
 
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
index 75945b7..96ad8b9 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -40,8 +40,12 @@
   <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
   <uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
 
+  <!-- For the Microphone Recording demos. -->
   <uses-permission android:name="android.permission.RECORD_AUDIO"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
   <!-- For Access to Car Hardware. -->
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
   <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-as/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-as/strings.xml
index bbdd5df..2ce2fd1 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-as/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-as/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"সূচীৰ টেব"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"টেবৰ দীঘলীয়া শিৰোনামৰ সৈতে গ্ৰিডৰ টেব"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"সন্ধানৰ টেব"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"টেব ল’ড কৰি থকা হৈছে"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"টেবৰ টেম্পলে’টৰ ল’ড হৈ থকা ডেম’"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"কোনো টেব নথকা টেবৰ টেম্পলে’টৰ ডেম’"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"অজ্ঞাত হ’ষ্টৰ বাবে প্ৰতিচ্ছবি দেখুৱাব নোৱাৰি"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-be/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-be/strings.xml
index 343df8c..2cd921b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-be/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-be/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Укладка \"Спіс\""</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Укладка з доўгай назвай і змесцівам у выглядзе сеткі"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Укладка \"Пошук\""</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ідзе загрузка ўкладкі"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Дэмаверсія загрузкі шаблона ўкладкі"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Дэмаверсія шаблона без укладак"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Відарысы нельга адлюстроўваць для невядомага хоста"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-bn/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-bn/strings.xml
index a9343bd..5111661 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-bn/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-bn/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"তালিকা ট্যাব"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ট্যাবের দীর্ঘ শিরোনাম সহ গ্রিড ট্যাব"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"সার্চ ট্যাব"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"\'ট্যাব\' লোড করা হচ্ছে"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"টেম্পলেট লোড করার সম্পর্কিত ডেমো ট্যাব"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ট্যাব ছাড়াই ট্যাব টেমপ্লেটের ডেমো"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"অজানা হোস্টের জন্য ছবি দেখানো যায় না"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
index c6da65a..e7115bc 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ca/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Pestanya de llista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Pestanya de quadrícula amb títol llarg"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Pestanya de cerca"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"S\'està carregant la pestanya"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demostració de càrrega d\'una plantilla de pestanya"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demostració d\'una plantilla de pestanya sense pestanyes"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"No es poden mostrar les imatges per a un amfitrió desconegut"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-da/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-da/strings.xml
index 4f89f7e..dc1614f 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-da/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-da/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listefane"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Gitterfane med lang fanetitel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Søgefane"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Indlæser fanen"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstration af indlæsning af faneskabelon"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstration af faneskabelon uden faner"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Billeder kan ikke vises for en ukendt host"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-de/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-de/strings.xml
index 50c6aaf..8ebf3ba 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-de/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-de/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Tab „Liste“"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Tab „Raster“ mit langem Tab-Titel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Tab „Suche“"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Tab wird geladen"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Tab-Vorlage – Demo wird geladen"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Tab-Vorlage – Demo „Keine Tabs“"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Für einen unbekannten Host können keine Bilder angezeigt werden"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-es/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-es/strings.xml
index f5543de..80cd6ee 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-es/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-es/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Pestaña de lista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Pestaña de cuadrícula con título largo"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Pestaña de búsqueda"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Cargando pestaña"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demo de carga de plantilla de pestañas"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo de plantilla de pestañas sin pestañas"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Las imágenes no se pueden mostrar en un host desconocido"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-eu/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-eu/strings.xml
index bdd159e..1c790f3 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-eu/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-eu/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Zerrendaren fitxa"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Saretaren fitxa izenburu luzearekin"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Bilaketen fitxa"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Fitxa kargatzen"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Fitxen txantiloi bat kargatzearen demo-bertsioa"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Fitxen txantiloien fitxarik gabeko demo-bertsioa"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Ostalari ezezagunei ezin zaizkie bistaratu irudiak"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
index 6ece42c..2b1282c 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"برگه فهرست"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"برگه جدول با عنوان برگه طولانی"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"برگه جستجو"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"درحال بار کردن «برگه»"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"نمونه بارگیری الگوی برگه"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"نمونه عدم وجود برگه الگوی برگه"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"به‌دلیل نامشخص بودن میزبان، تصویر نمایش داده نمی‌شود"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fi/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fi/strings.xml
index cd5b960..d623ed5 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fi/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fi/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Luettelovälilehti"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Ruudukkovälilehti ja pitkä välilehden nimi"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Hakuvälilehti"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ladataan välilehteä"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Välilehtimallin lataamisen esittely"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Välilehtimallin (ei välilehtiä) esittely"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Kuvia ei voida näyttää tuntemattomalle isännälle"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fr/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fr/strings.xml
index f488f44..5b45a10 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fr/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fr/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Onglet de liste"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Onglet de grille avec titre d\'onglet long"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Onglet de recherche"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Chargement de l\'onglet"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Démonstration de chargement du modèle d\'onglet"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Démonstrations du modèle d\'onglet sans onglets"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Les images d\'un hôte inconnu ne peuvent pas être affichées"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-gl/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-gl/strings.xml
index c53b92f..5d559ff 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-gl/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-gl/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Pestana de lista"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Pestana de grade con título de pestana longo"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Pestana de busca"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Cargando pestana"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demostración de carga do modelo de pestanas"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demostración do modelo de pestanas sen pestanas"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"As imaxes non se poden mostrar nun host descoñecido"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-gu/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-gu/strings.xml
index d156d06..cb90252 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-gu/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-gu/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"સૂચિ ટૅબ"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ટૅબનું લાંબું શીર્ષક ધરાવતું ગ્રીડ ટૅબ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"શોધ ટૅબ"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"ટૅબ લોડ થઈ રહ્યું છે"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ટૅબનો નમૂનો લોડ થવાનો ડેમો"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ટૅબના નમૂનાનો, ટૅબ વિનાનો ડેમો"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"અજાણ્યા હોસ્ટ માટે છબીઓ બતાવી શકાતી નથી"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
index 0a01056..109e8e6 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-hi/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"सूची टैब"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"लंबे टैब टाइटल वाले ग्रिड टैब"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"खोज टैब"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"टैब लोड हो रहा है"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"टैब टेंप्लेट लोड करने का डेमो"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"बिना किसी टैब वाले टैब टेंप्लेट का डेमो"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"अनजान होस्ट के लिए इमेज नहीं दिखाई जा सकतीं"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-hy/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-hy/strings.xml
index e4dcad5..e114756 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-hy/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-hy/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Ցանկերի ներդիր"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Ցանցերի ներդիր՝ երկար վերնագրով"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Որոնումների ներդիր"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ներդիրի բեռնում"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Ներդիրների ձևանմուշի բեռնման դեմո"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Առանց ներդիրների ձևանմուշի դեմո"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Պատկերները չեն կարող ցուցադրվել անհայտ խնամորդի համար"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-is/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-is/strings.xml
index 6de4b46..dba3f3a 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-is/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-is/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listaflipi"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Töfluflipi með löngum flipatitli"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Leitarflipi"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Hleður flipa"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Prufuútgáfa hleðslu flipasniðmáts"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Prufuútgáfa flipasniðmáts með engum flipum"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Ekki er hægt að birta myndir fyrir óþekktan hýsil"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-iw/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-iw/strings.xml
index 93da01d..ac062b5 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-iw/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-iw/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"כרטיסייה של רשימה"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"כרטיסיית רשת עם שם כרטיסייה ארוך"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"כרטיסיית חיפוש"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"הכרטיסייה נטענת"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"הדגמה של העלאת תבנית הכרטיסייה"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"הדגמה של תבנית הכרטיסייה כשאין כרטיסיות"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"לא ניתן להציג תמונות למארח לא ידוע"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-kk/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-kk/strings.xml
index 7299eb7..e3b064d 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-kk/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-kk/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Тізім қойындысы"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Қойынды атауы ұзын тор қойындысы"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Іздеу қойындысы"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Қойынды жүктеліп жатыр"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Қойынды үлгісінің демо нұсқасын жүктеу"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Қойынды үлгісі – қойындылардың демо нұсқасы жоқ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Суреттер белгісіз хост үшін көрсетілмейді."</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-kn/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-kn/strings.xml
index 4a4a70e6a..42a4a75 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-kn/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-kn/strings.xml
@@ -162,7 +162,7 @@
     <string name="go_straight" msgid="2301747728609198718">"ನೇರವಾಗಿ ಹೋಗಿ"</string>
     <string name="turn_right" msgid="4710562732720109969">"ಬಲಕ್ಕೆ ತಿರುಗಿ"</string>
     <string name="take_520" msgid="3804796387195842741">"520 ಗೆ ಹೋಗಿ"</string>
-    <string name="gas_station" msgid="1203313937444666161">"ಗ್ಯಾಸ್ ಸ್ಟೇಶನ್"</string>
+    <string name="gas_station" msgid="1203313937444666161">"ಪೆಟ್ರೋಲ್ ಬಂಕ್"</string>
     <string name="short_route" msgid="4831864276538141265">"ಕಡಿಮೆ ದೂರದ ಮಾರ್ಗ"</string>
     <string name="less_busy" msgid="310625272281710983">"ಕಡಿಮೆ ಟ್ರಾಫಿಕ್"</string>
     <string name="hov_friendly" msgid="6956152104754594971">"HOV ಸ್ನೇಹಿ"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ko/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ko/strings.xml
index a23af53..d3c96b0 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ko/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ko/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"탭 목록"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"긴 탭 제목이 있는 그리드 탭"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"검색 탭"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"탭 로드 중"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"탭 템플릿 로딩 데모"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"탭 없음 탭 템플릿 데모"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"알려지지 않은 호스트의 이미지를 표시할 수 없음"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
index 8feac44..074c234 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ky/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Тизме өтмөгү"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Өтмөктүн аталышы узун болгон торчо түрүндөгү өтмөк"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Издөө өтмөгү"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Өтмөк жүктөлүүдө"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Өтмөктүн үлгүсүн жүктөөнүн демо версиясы"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Өтмөктүн үлгүсү, өтмөктөр жок демо версиясы"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Сүрөттөрдү белгисиз башкы түйүн үчүн көрсөтүүгө болбойт"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-lo/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-lo/strings.xml
index c0b11ad..6afe958 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-lo/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-lo/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ແຖບລາຍຊື່"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ແຖບຕາໜ່າງພ້ອມຊື່ແຖບຍາວ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ແຖບຊອກຫາ"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"ກຳລັງໂຫຼດແຖບ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ເດໂມແມ່ແບບແຖບການໂຫຼດ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ເດໂມແມ່ແບບແຖບທີ່ບໍ່ມີແຖບ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ບໍ່ສາມາດສະແດງຮູບສຳລັບໂຮສທີ່ບໍ່ຮູ້ຈັກໄດ້"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-lv/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-lv/strings.xml
index 3ad011a..4963476 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-lv/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-lv/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Saraksta skata cilne"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Režģa skata cilne ar garu cilnes nosaukumu"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Cilne meklēšanai"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Notiek cilnes ielāde…"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Cilnes veidnes ielādes demonstrācija"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstrācija cilnes veidnei, kurā nav ciļņu"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Nevar rādīt attēlus nezināmam saimniekdatoram"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-mr/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-mr/strings.xml
index ca74eef..b1c63b2f 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-mr/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-mr/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"सूची टॅब"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"लांब टॅब शीर्षक असलेला ग्रिड टॅब"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"शोध टॅब"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"टॅब लोड करत आहे"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"टॅब टेंप्लेट लोड होत आहे डेमो"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"टॅब टेंप्लेट टॅब नाही डेमो"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"अज्ञात होस्टला इमेज दाखवल्या जाऊ शकत नाहीत"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-nb/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-nb/strings.xml
index 584f12b..53ec16b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-nb/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-nb/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listefane"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Rutenettfane med lang fanetittel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Søkefane"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Laster inn fanen"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Laster inn demo av fanemal"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demo av fanemal uten faner"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Bilder kan ikke vises for en ukjent vert"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ne/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ne/strings.xml
index 67bfcef..79b778d 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ne/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ne/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"लिस्ट ट्याब"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ट्याबको लामो शीर्षक भएको ग्रिड ट्याब"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"सर्च ट्याब"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"ट्याब लोड गरिँदै छ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ट्याब टेम्प्लेट लोड गर्ने डेमो"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"कुनै पनि ट्याब नभएको ट्याब टेम्प्लेटको डेमो"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"अज्ञात होस्टका हकमा फोटोहरू देखाउन मिल्दैन"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
index 429cae1..42cf151 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ଲିଷ୍ଟ ଟାବ"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ଲମ୍ବା ଟାବ ଟାଇଟେଲ ସହ ଗ୍ରୀଡ ଟାବ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ସର୍ଚ୍ଚ ଟାବ"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"ଟାବ ଲୋଡ ହେଉଛି"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ଟାବ ଟେମ୍ପଲେଟର ଲୋଡିଂ ଡେମୋ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ଟାବ ଟେମ୍ପଲେଟ କୌଣସି ଟାବ ବିନା ଡେମୋ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ଏକ ଅଜଣା ହୋଷ୍ଟ ପାଇଁ ଇମେଜଗୁଡ଼ିକୁ ଡିସପ୍ଲେ କରାଯାଇପାରିବ ନାହିଁ"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-pa/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-pa/strings.xml
index 821bb56..0d162a5 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-pa/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-pa/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ਸੂਚੀ ਟੈਬ"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"ਲੰਮੇ ਟੈਬ ਸਿਰਲੇਖ ਵਾਲੇ ਗਰਿੱਡ ਟੈਬ"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"ਖੋਜੋ ਟੈਬ"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"ਟੈਬ ਨੂੰ ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ਟੈਬ ਟੈਮਪਲੇਟ ਲੋਡ ਕਰਨ ਦੀ ਪ੍ਰਕਿਰਿਆ ਦਾ ਡੈਮੋ"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ਬਿਨਾ ਕਿਸੇ ਟੈਬ ਵਾਲੇ ਟੈਬ ਟੈਮਪਲੇਟ ਦਾ ਡੈਮੋ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"ਕਿਸੇ ਅਗਿਆਤ ਹੋਸਟ ਲਈ ਚਿੱਤਰ ਨਹੀਂ ਦਿਖਾਏ ਜਾ ਸਕਦੇ"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-pl/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-pl/strings.xml
index fddf150..9649d73 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-pl/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-pl/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Karta listy"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Wersja demonstracyjna szablonu kart korzystająca z długich kart"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Karta wyszukiwania"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Ładowanie karty"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Ładowanie wersji demonstracyjnej szablonu kart"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Wersja demonstracyjna szablonu kart bez kart"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Nie można wyświetlać grafiki z nieznanego serwera"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ro/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ro/strings.xml
index 4cd8d32..e5f8a3c 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ro/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ro/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Filă cu listă"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Filă cu grilă și titlu lung"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Filă cu căutare"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Se încarcă fila"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstrație cu încărcarea șablonului de file"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstrație fără file pentru șablon"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Nu se pot afișa imagini pentru o gazdă necunoscută"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ru/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ru/strings.xml
index ba76ba6..0c20c56 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ru/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ru/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Вкладка списка"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Вкладка сетки с длинным названием"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Вкладка поиска"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Загрузка вкладки…"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Демонстрация загрузки шаблона вкладок"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Демонстрация шаблона без вкладок"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Нельзя показывать изображения для неизвестного хоста."</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-si/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-si/strings.xml
index 918242d..146ca3e 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-si/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-si/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"ලැයිස්තු පටිත්ත"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"දිගු පටිති මාතෘකාව සහිත ජාල පටිත්ත"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"සෙවීම් පටිත්ත"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"පටිත්ත පූරණය වේ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"පටිති අච්චු පූරණය වීමේ ආදර්ශනය"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"පටිති අච්චු පටිති නැති ආදර්ශනය"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"නොදන්නා සංග්‍රාහකයෙක් සඳහා රූප සංදර්ශනය කළ නොහැකිය"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sk/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sk/strings.xml
index 601c7d9..d494703 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sk/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sk/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Karta zoznamu"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Karta mriežky s dlhým názvom karty"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Karta vyhľadávania"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Načítava sa karta"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Ukážka načítavania šablóny karty"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Ukážka šablón karty bez kariet"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Obrázky nie je možné zobraziť pre neznámeho hostiteľa"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sq/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sq/strings.xml
index 2621c98..23a7b4c 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sq/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sq/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Skeda e listës"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Skeda e rrjetës me titull të gjatë skede"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Skeda e \"Kërko\""</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Skeda po ngarkohet"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Demonstrimi i ngarkimit të shabllonit të skedës"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Demonstrimi i shabllonit të skedës pa skeda"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Imazhet nuk mund të shfaqen për një organizator të panjohur"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-sv/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-sv/strings.xml
index 7703167..8537e2a 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-sv/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-sv/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Listflik"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Elnätsflik med lång fliktitel"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Sökflik"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Läser in flik"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Laddningsflikmallsdemo"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Flikmall utan flikdemo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Bilder kan inte visas för en okänd värd"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-tr/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-tr/strings.xml
index a0f833e..c56746b 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-tr/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-tr/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Liste Sekmesi"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Uzun Sekme Başlıklı Izgara Sekmesi"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Arama Sekmesi"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Sekme Yükleniyor"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Sekme Şablonu Yükleme Demosu"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Sekme Şablonu Sekme İçermeyen Demo"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Resimler, bilinmeyen ana makine için gösterilemez"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-uk/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-uk/strings.xml
index 7be0627..3b429dd 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-uk/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-uk/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Вкладка зі списком"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Вкладка із сіткою та довгою назвою"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Вкладка пошуку"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Завантаження вкладки"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Демонстрація шаблона вкладки: завантаження"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Демонстрація шаблона вкладки: немає вкладок"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Зображення не показуються для невідомого хосту"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-ur/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-ur/strings.xml
index b244662..d82e510 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-ur/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-ur/strings.xml
@@ -268,8 +268,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"فہرست ٹیب"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"طویل ٹیب کے عنوان کے ساتھ گرڈ ٹیب"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"تلاش ٹیب"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"ٹیب لوڈ ہو رہا ہے"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"ٹیب کی تمثیل ڈیمو لوڈ ہو رہا ہے"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"ٹیب کی تمثیل کوئی ٹیبز ڈیمو نہیں"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"نامعلوم میزبان کیلئے تصاویر کو ڈسپلے نہیں کیا جا سکتا"</string>
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-vi/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-vi/strings.xml
index 76ad292..69dd820 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-vi/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-vi/strings.xml
@@ -264,8 +264,7 @@
     <string name="tab_title_list" msgid="5104962518489668123">"Thẻ dạng danh sách"</string>
     <string name="tab_title_grid" msgid="5268168907976325154">"Thẻ dạng lưới có tiêu đề thẻ dài"</string>
     <string name="tab_title_search" msgid="1892925693146631173">"Thẻ tìm kiếm"</string>
-    <!-- no translation found for tab_title_loading (5385807479734490989) -->
-    <skip />
+    <string name="tab_title_loading" msgid="5385807479734490989">"Đang tải thẻ"</string>
     <string name="tab_template_loading_demo_title" msgid="4638051615030345574">"Bản demo mẫu thẻ đang tải"</string>
     <string name="tab_template_no_tabs_demo_title" msgid="6907466201298082309">"Bản demo mẫu thẻ không có thẻ"</string>
     <string name="images_unknown_host_error" msgid="3180661817432720076">"Không thể hiển thị hình ảnh cho một máy chủ không xác định"</string>
diff --git a/car/app/app-samples/showcase/mobile/build.gradle b/car/app/app-samples/showcase/mobile/build.gradle
index 5466dad..ac1eaeb 100644
--- a/car/app/app-samples/showcase/mobile/build.gradle
+++ b/car/app/app-samples/showcase/mobile/build.gradle
@@ -25,7 +25,7 @@
     defaultConfig {
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 33
         // Increment this to generate signed builds for uploading to Playstore
         // Make sure this is different from the showcase-automotive version
         versionCode 106
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
index 59a17ab..917662c 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
@@ -31,6 +31,9 @@
   <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
   <uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
   <!-- For Access to Car Hardware. -->
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
   <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
index 1c9c267..6f205ae 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -27,12 +27,15 @@
 
   <uses-sdk
       android:minSdkVersion="23"
-      android:targetSdkVersion="31" />
+      android:targetSdkVersion="33" />
 
   <uses-permission android:name="android.permission.INTERNET"/>
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
   <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
+  <!-- SDK 33 onwards, apps require this permission to send any notifications to the system -->
+  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
   <!-- For PlaceListMapTemplate -->
   <uses-permission android:name="androidx.car.app.MAP_TEMPLATES"/>
 
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt
index 5bcab89..ec619f8 100644
--- a/collection/collection/api/current.txt
+++ b/collection/collection/api/current.txt
@@ -94,6 +94,70 @@
     property public final int last;
   }
 
+  public abstract sealed class FloatFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(float key);
+    method public final int getCapacity();
+    method public final float getOrDefault(float key, float defaultValue);
+    method public final inline float getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class FloatFloatMapKt {
+    method public static androidx.collection.FloatFloatMap emptyFloatFloatMap();
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf();
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(kotlin.Pair<java.lang.Float,java.lang.Float>... pairs);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf();
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(kotlin.Pair<java.lang.Float,java.lang.Float>... pairs);
+  }
+
+  public abstract sealed class FloatIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(float key);
+    method public final int getCapacity();
+    method public final int getOrDefault(float key, int defaultValue);
+    method public final inline int getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class FloatIntMapKt {
+    method public static androidx.collection.FloatIntMap emptyFloatIntMap();
+    method public static androidx.collection.FloatIntMap floatIntMapOf();
+    method public static androidx.collection.FloatIntMap floatIntMapOf(kotlin.Pair<java.lang.Float,java.lang.Integer>... pairs);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf();
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(kotlin.Pair<java.lang.Float,java.lang.Integer>... pairs);
+  }
+
   public abstract sealed class FloatList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
@@ -146,6 +210,70 @@
     method public static inline androidx.collection.MutableFloatList mutableFloatListOf(float... elements);
   }
 
+  public abstract sealed class FloatLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(float key);
+    method public final int getCapacity();
+    method public final long getOrDefault(float key, long defaultValue);
+    method public final inline long getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class FloatLongMapKt {
+    method public static androidx.collection.FloatLongMap emptyFloatLongMap();
+    method public static androidx.collection.FloatLongMap floatLongMapOf();
+    method public static androidx.collection.FloatLongMap floatLongMapOf(kotlin.Pair<java.lang.Float,java.lang.Long>... pairs);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf();
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(kotlin.Pair<java.lang.Float,java.lang.Long>... pairs);
+  }
+
+  public abstract sealed class FloatObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(float key);
+    method public final int getCapacity();
+    method public final V getOrDefault(float key, V defaultValue);
+    method public final inline V getOrElse(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class FloatObjectMapKt {
+    method public static <V> androidx.collection.FloatObjectMap<V> emptyFloatObjectMap();
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf();
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(kotlin.Pair<java.lang.Float,? extends V>... pairs);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf();
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(kotlin.Pair<java.lang.Float,? extends V>... pairs);
+  }
+
   public abstract sealed class FloatSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -179,6 +307,70 @@
     method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float... elements);
   }
 
+  public abstract sealed class IntFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(int key);
+    method public final int getCapacity();
+    method public final float getOrDefault(int key, float defaultValue);
+    method public final inline float getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class IntFloatMapKt {
+    method public static androidx.collection.IntFloatMap emptyIntFloatMap();
+    method public static androidx.collection.IntFloatMap intFloatMapOf();
+    method public static androidx.collection.IntFloatMap intFloatMapOf(kotlin.Pair<java.lang.Integer,java.lang.Float>... pairs);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf();
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(kotlin.Pair<java.lang.Integer,java.lang.Float>... pairs);
+  }
+
+  public abstract sealed class IntIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(int key);
+    method public final int getCapacity();
+    method public final int getOrDefault(int key, int defaultValue);
+    method public final inline int getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class IntIntMapKt {
+    method public static androidx.collection.IntIntMap emptyIntIntMap();
+    method public static androidx.collection.IntIntMap intIntMapOf();
+    method public static androidx.collection.IntIntMap intIntMapOf(kotlin.Pair<java.lang.Integer,java.lang.Integer>... pairs);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf();
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(kotlin.Pair<java.lang.Integer,java.lang.Integer>... pairs);
+  }
+
   public abstract sealed class IntList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
@@ -231,6 +423,70 @@
     method public static inline androidx.collection.MutableIntList mutableIntListOf(int... elements);
   }
 
+  public abstract sealed class IntLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(int key);
+    method public final int getCapacity();
+    method public final long getOrDefault(int key, long defaultValue);
+    method public final inline long getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class IntLongMapKt {
+    method public static androidx.collection.IntLongMap emptyIntLongMap();
+    method public static androidx.collection.IntLongMap intLongMapOf();
+    method public static androidx.collection.IntLongMap intLongMapOf(kotlin.Pair<java.lang.Integer,java.lang.Long>... pairs);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf();
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(kotlin.Pair<java.lang.Integer,java.lang.Long>... pairs);
+  }
+
+  public abstract sealed class IntObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(int key);
+    method public final int getCapacity();
+    method public final V getOrDefault(int key, V defaultValue);
+    method public final inline V getOrElse(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class IntObjectMapKt {
+    method public static <V> androidx.collection.IntObjectMap<V> emptyIntObjectMap();
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf();
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(kotlin.Pair<java.lang.Integer,? extends V>... pairs);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf();
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(kotlin.Pair<java.lang.Integer,? extends V>... pairs);
+  }
+
   public abstract sealed class IntSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -264,6 +520,70 @@
     method public static androidx.collection.MutableIntSet mutableIntSetOf(int... elements);
   }
 
+  public abstract sealed class LongFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(long key);
+    method public final int getCapacity();
+    method public final float getOrDefault(long key, float defaultValue);
+    method public final inline float getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class LongFloatMapKt {
+    method public static androidx.collection.LongFloatMap emptyLongFloatMap();
+    method public static androidx.collection.LongFloatMap longFloatMapOf();
+    method public static androidx.collection.LongFloatMap longFloatMapOf(kotlin.Pair<java.lang.Long,java.lang.Float>... pairs);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf();
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(kotlin.Pair<java.lang.Long,java.lang.Float>... pairs);
+  }
+
+  public abstract sealed class LongIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(long key);
+    method public final int getCapacity();
+    method public final int getOrDefault(long key, int defaultValue);
+    method public final inline int getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class LongIntMapKt {
+    method public static androidx.collection.LongIntMap emptyLongIntMap();
+    method public static androidx.collection.LongIntMap longIntMapOf();
+    method public static androidx.collection.LongIntMap longIntMapOf(kotlin.Pair<java.lang.Long,java.lang.Integer>... pairs);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf();
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(kotlin.Pair<java.lang.Long,java.lang.Integer>... pairs);
+  }
+
   public abstract sealed class LongList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
@@ -316,6 +636,70 @@
     method public static inline androidx.collection.MutableLongList mutableLongListOf(long... elements);
   }
 
+  public abstract sealed class LongLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(long key);
+    method public final int getCapacity();
+    method public final long getOrDefault(long key, long defaultValue);
+    method public final inline long getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class LongLongMapKt {
+    method public static androidx.collection.LongLongMap emptyLongLongMap();
+    method public static androidx.collection.LongLongMap longLongMapOf();
+    method public static androidx.collection.LongLongMap longLongMapOf(kotlin.Pair<java.lang.Long,java.lang.Long>... pairs);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf();
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(kotlin.Pair<java.lang.Long,java.lang.Long>... pairs);
+  }
+
+  public abstract sealed class LongObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(long key);
+    method public final int getCapacity();
+    method public final V getOrDefault(long key, V defaultValue);
+    method public final inline V getOrElse(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class LongObjectMapKt {
+    method public static <V> androidx.collection.LongObjectMap<V> emptyLongObjectMap();
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf();
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(kotlin.Pair<java.lang.Long,? extends V>... pairs);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf();
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(kotlin.Pair<java.lang.Long,? extends V>... pairs);
+  }
+
   public abstract sealed class LongSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -416,6 +800,48 @@
     method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved);
   }
 
+  public final class MutableFloatFloatMap extends androidx.collection.FloatFloatMap {
+    ctor public MutableFloatFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatFloatMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Float> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Float>![] pairs);
+    method public void put(float key, float value);
+    method public void putAll(androidx.collection.FloatFloatMap from);
+    method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Float>![] pairs);
+    method public void remove(float key);
+    method public boolean remove(float key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(float key, float value);
+    method public int trim();
+  }
+
+  public final class MutableFloatIntMap extends androidx.collection.FloatIntMap {
+    ctor public MutableFloatIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatIntMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Integer> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Integer>![] pairs);
+    method public void put(float key, int value);
+    method public void putAll(androidx.collection.FloatIntMap from);
+    method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Integer>![] pairs);
+    method public void remove(float key);
+    method public boolean remove(float key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(float key, int value);
+    method public int trim();
+  }
+
   public final class MutableFloatList extends androidx.collection.FloatList {
     ctor public MutableFloatList(optional int initialCapacity);
     method public boolean add(float element);
@@ -447,6 +873,48 @@
     property public final inline int capacity;
   }
 
+  public final class MutableFloatLongMap extends androidx.collection.FloatLongMap {
+    ctor public MutableFloatLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatLongMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Long> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Long>![] pairs);
+    method public void put(float key, long value);
+    method public void putAll(androidx.collection.FloatLongMap from);
+    method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Long>![] pairs);
+    method public void remove(float key);
+    method public boolean remove(float key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(float key, long value);
+    method public int trim();
+  }
+
+  public final class MutableFloatObjectMap<V> extends androidx.collection.FloatObjectMap<V> {
+    ctor public MutableFloatObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatObjectMap<V> from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,? extends V> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,? extends V>![] pairs);
+    method public V? put(float key, V value);
+    method public void putAll(androidx.collection.FloatObjectMap<V> from);
+    method public void putAll(kotlin.Pair<java.lang.Float,? extends V>![] pairs);
+    method public V? remove(float key);
+    method public boolean remove(float key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public operator void set(float key, V value);
+    method public int trim();
+  }
+
   public final class MutableFloatSet extends androidx.collection.FloatSet {
     ctor public MutableFloatSet(optional int initialCapacity);
     method public boolean add(float element);
@@ -465,6 +933,48 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableIntFloatMap extends androidx.collection.IntFloatMap {
+    ctor public MutableIntFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntFloatMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Float> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Float>![] pairs);
+    method public void put(int key, float value);
+    method public void putAll(androidx.collection.IntFloatMap from);
+    method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Float>![] pairs);
+    method public void remove(int key);
+    method public boolean remove(int key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(int key, float value);
+    method public int trim();
+  }
+
+  public final class MutableIntIntMap extends androidx.collection.IntIntMap {
+    ctor public MutableIntIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntIntMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Integer> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Integer>![] pairs);
+    method public void put(int key, int value);
+    method public void putAll(androidx.collection.IntIntMap from);
+    method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Integer>![] pairs);
+    method public void remove(int key);
+    method public boolean remove(int key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(int key, int value);
+    method public int trim();
+  }
+
   public final class MutableIntList extends androidx.collection.IntList {
     ctor public MutableIntList(optional int initialCapacity);
     method public boolean add(int element);
@@ -496,6 +1006,48 @@
     property public final inline int capacity;
   }
 
+  public final class MutableIntLongMap extends androidx.collection.IntLongMap {
+    ctor public MutableIntLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntLongMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Long> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Long>![] pairs);
+    method public void put(int key, long value);
+    method public void putAll(androidx.collection.IntLongMap from);
+    method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Long>![] pairs);
+    method public void remove(int key);
+    method public boolean remove(int key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(int key, long value);
+    method public int trim();
+  }
+
+  public final class MutableIntObjectMap<V> extends androidx.collection.IntObjectMap<V> {
+    ctor public MutableIntObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntObjectMap<V> from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,? extends V> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,? extends V>![] pairs);
+    method public V? put(int key, V value);
+    method public void putAll(androidx.collection.IntObjectMap<V> from);
+    method public void putAll(kotlin.Pair<java.lang.Integer,? extends V>![] pairs);
+    method public V? remove(int key);
+    method public boolean remove(int key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public operator void set(int key, V value);
+    method public int trim();
+  }
+
   public final class MutableIntSet extends androidx.collection.IntSet {
     ctor public MutableIntSet(optional int initialCapacity);
     method public boolean add(int element);
@@ -514,6 +1066,48 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableLongFloatMap extends androidx.collection.LongFloatMap {
+    ctor public MutableLongFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongFloatMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Float> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Float>![] pairs);
+    method public void put(long key, float value);
+    method public void putAll(androidx.collection.LongFloatMap from);
+    method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Float>![] pairs);
+    method public void remove(long key);
+    method public boolean remove(long key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(long key, float value);
+    method public int trim();
+  }
+
+  public final class MutableLongIntMap extends androidx.collection.LongIntMap {
+    ctor public MutableLongIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongIntMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Integer> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Integer>![] pairs);
+    method public void put(long key, int value);
+    method public void putAll(androidx.collection.LongIntMap from);
+    method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Integer>![] pairs);
+    method public void remove(long key);
+    method public boolean remove(long key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(long key, int value);
+    method public int trim();
+  }
+
   public final class MutableLongList extends androidx.collection.LongList {
     ctor public MutableLongList(optional int initialCapacity);
     method public void add(@IntRange(from=0L) int index, long element);
@@ -545,6 +1139,48 @@
     property public final inline int capacity;
   }
 
+  public final class MutableLongLongMap extends androidx.collection.LongLongMap {
+    ctor public MutableLongLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongLongMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Long> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Long>![] pairs);
+    method public void put(long key, long value);
+    method public void putAll(androidx.collection.LongLongMap from);
+    method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Long>![] pairs);
+    method public void remove(long key);
+    method public boolean remove(long key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(long key, long value);
+    method public int trim();
+  }
+
+  public final class MutableLongObjectMap<V> extends androidx.collection.LongObjectMap<V> {
+    ctor public MutableLongObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongObjectMap<V> from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,? extends V> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,? extends V>![] pairs);
+    method public V? put(long key, V value);
+    method public void putAll(androidx.collection.LongObjectMap<V> from);
+    method public void putAll(kotlin.Pair<java.lang.Long,? extends V>![] pairs);
+    method public V? remove(long key);
+    method public boolean remove(long key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public operator void set(long key, V value);
+    method public int trim();
+  }
+
   public final class MutableLongSet extends androidx.collection.LongSet {
     ctor public MutableLongSet(optional int initialCapacity);
     method public boolean add(long element);
@@ -563,6 +1199,124 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableObjectFloatMap<K> extends androidx.collection.ObjectFloatMap<K> {
+    ctor public MutableObjectFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectFloatMap<K> from);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Float> pair);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Float>![] pairs);
+    method public void put(K key, float value);
+    method public void putAll(androidx.collection.ObjectFloatMap<K> from);
+    method public void putAll(kotlin.Pair<? extends K,java.lang.Float>![] pairs);
+    method public void remove(K key);
+    method public boolean remove(K key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(K key, float value);
+    method public int trim();
+  }
+
+  public final class MutableObjectIntMap<K> extends androidx.collection.ObjectIntMap<K> {
+    ctor public MutableObjectIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectIntMap<K> from);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Integer> pair);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Integer>![] pairs);
+    method public void put(K key, int value);
+    method public void putAll(androidx.collection.ObjectIntMap<K> from);
+    method public void putAll(kotlin.Pair<? extends K,java.lang.Integer>![] pairs);
+    method public void remove(K key);
+    method public boolean remove(K key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(K key, int value);
+    method public int trim();
+  }
+
+  public final class MutableObjectList<E> extends androidx.collection.ObjectList<E> {
+    ctor public MutableObjectList(optional int initialCapacity);
+    method public boolean add(E element);
+    method public void add(@IntRange(from=0L) int index, E element);
+    method public boolean addAll(androidx.collection.ObjectList<E> elements);
+    method public boolean addAll(androidx.collection.ScatterSet<E> elements);
+    method public boolean addAll(E![] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.ObjectList<E> elements);
+    method public boolean addAll(@IntRange(from=0L) int index, E![] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, java.util.Collection<? extends E> elements);
+    method public boolean addAll(Iterable<? extends E> elements);
+    method public boolean addAll(java.util.List<? extends E> elements);
+    method public boolean addAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public java.util.List<E> asList();
+    method public java.util.List<E> asMutableList();
+    method public void clear();
+    method public void ensureCapacity(int capacity);
+    method public inline int getCapacity();
+    method public operator void minusAssign(androidx.collection.ObjectList<E> elements);
+    method public operator void minusAssign(androidx.collection.ScatterSet<E> elements);
+    method public inline operator void minusAssign(E element);
+    method public operator void minusAssign(E![] elements);
+    method public operator void minusAssign(Iterable<? extends E> elements);
+    method public operator void minusAssign(java.util.List<? extends E> elements);
+    method public operator void minusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator void plusAssign(androidx.collection.ObjectList<E> elements);
+    method public operator void plusAssign(androidx.collection.ScatterSet<E> elements);
+    method public inline operator void plusAssign(E element);
+    method public operator void plusAssign(E![] elements);
+    method public operator void plusAssign(Iterable<? extends E> elements);
+    method public operator void plusAssign(java.util.List<? extends E> elements);
+    method public operator void plusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public boolean remove(E element);
+    method public boolean removeAll(androidx.collection.ObjectList<E> elements);
+    method public boolean removeAll(androidx.collection.ScatterSet<E> elements);
+    method public boolean removeAll(E![] elements);
+    method public boolean removeAll(Iterable<? extends E> elements);
+    method public boolean removeAll(java.util.List<? extends E> elements);
+    method public boolean removeAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public E removeAt(@IntRange(from=0L) int index);
+    method public inline void removeIf(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public void removeRange(@IntRange(from=0L) int start, @IntRange(from=0L) int end);
+    method public boolean retainAll(androidx.collection.ObjectList<E> elements);
+    method public boolean retainAll(E![] elements);
+    method public boolean retainAll(Iterable<? extends E> elements);
+    method public boolean retainAll(java.util.Collection<? extends E> elements);
+    method public boolean retainAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator E set(@IntRange(from=0L) int index, E element);
+    method public void trim(optional int minCapacity);
+    property public final inline int capacity;
+  }
+
+  public final class MutableObjectLongMap<K> extends androidx.collection.ObjectLongMap<K> {
+    ctor public MutableObjectLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectLongMap<K> from);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Long> pair);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Long>![] pairs);
+    method public void put(K key, long value);
+    method public void putAll(androidx.collection.ObjectLongMap<K> from);
+    method public void putAll(kotlin.Pair<? extends K,java.lang.Long>![] pairs);
+    method public void remove(K key);
+    method public boolean remove(K key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(K key, long value);
+    method public int trim();
+  }
+
   public final class MutableScatterMap<K, V> extends androidx.collection.ScatterMap<K,V> {
     ctor public MutableScatterMap(optional int initialCapacity);
     method public java.util.Map<K,V> asMutableMap();
@@ -619,6 +1373,162 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public abstract sealed class ObjectFloatMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(K key);
+    method public final int getCapacity();
+    method public final float getOrDefault(K key, float defaultValue);
+    method public final inline float getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class ObjectFloatMapKt {
+    method public static <K> androidx.collection.ObjectFloatMap<K> emptyObjectFloatMap();
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf();
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(kotlin.Pair<? extends K,java.lang.Float>... pairs);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMap();
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(kotlin.Pair<? extends K,java.lang.Float>... pairs);
+  }
+
+  public abstract sealed class ObjectIntMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(K key);
+    method public final int getCapacity();
+    method public final int getOrDefault(K key, int defaultValue);
+    method public final inline int getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class ObjectIntMapKt {
+    method public static <K> androidx.collection.ObjectIntMap<K> emptyObjectIntMap();
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf();
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(kotlin.Pair<? extends K,java.lang.Integer>... pairs);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMap();
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(kotlin.Pair<? extends K,java.lang.Integer>... pairs);
+  }
+
+  public abstract sealed class ObjectList<E> {
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public abstract java.util.List<E> asList();
+    method public final operator boolean contains(E element);
+    method public final boolean containsAll(androidx.collection.ObjectList<E> elements);
+    method public final boolean containsAll(E![] elements);
+    method public final boolean containsAll(Iterable<? extends E> elements);
+    method public final boolean containsAll(java.util.List<? extends E> elements);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final E elementAt(@IntRange(from=0L) int index);
+    method public final inline E elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends E> defaultValue);
+    method public final E first();
+    method public final inline E first(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline E? firstOrNull();
+    method public final inline E? firstOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline <R> R fold(R initial, kotlin.jvm.functions.Function2<? super R,? super E,? extends R> operation);
+    method public final inline <R> R foldIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super R,? super E,? extends R> operation);
+    method public final inline <R> R foldRight(R initial, kotlin.jvm.functions.Function2<? super E,? super R,? extends R> operation);
+    method public final inline <R> R foldRightIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super E,? super R,? extends R> operation);
+    method public final inline void forEach(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+    method public final inline void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+    method public final inline void forEachReversed(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+    method public final inline void forEachReversedIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+    method public final operator E get(@IntRange(from=0L) int index);
+    method public final inline kotlin.ranges.IntRange getIndices();
+    method @IntRange(from=-1L) public final inline int getLastIndex();
+    method @IntRange(from=0L) public final int getSize();
+    method public final int indexOf(E element);
+    method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final E last();
+    method public final inline E last(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final int lastIndexOf(E element);
+    method public final inline E? lastOrNull();
+    method public final inline E? lastOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final boolean none();
+    method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    property public final inline kotlin.ranges.IntRange indices;
+    property @IntRange(from=-1L) public final inline int lastIndex;
+    property @IntRange(from=0L) public final int size;
+  }
+
+  public final class ObjectListKt {
+    method public static <E> androidx.collection.ObjectList<E> emptyObjectList();
+    method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf();
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1);
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2);
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2, E element3);
+    method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E?... elements);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf();
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2, E element3);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E?... elements);
+  }
+
+  public abstract sealed class ObjectLongMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(K key);
+    method public final int getCapacity();
+    method public final long getOrDefault(K key, long defaultValue);
+    method public final inline long getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+  }
+
+  public final class ObjectLongMapKt {
+    method public static <K> androidx.collection.ObjectLongMap<K> emptyObjectLongMap();
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf();
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(kotlin.Pair<? extends K,java.lang.Long>... pairs);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMap();
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(kotlin.Pair<? extends K,java.lang.Long>... pairs);
+  }
+
   @kotlin.jvm.JvmInline public final value class PairFloatFloat {
     ctor public PairFloatFloat(float first, float second);
     method public inline operator float component1();
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt
index a46df5c..098c0eb 100644
--- a/collection/collection/api/restricted_current.txt
+++ b/collection/collection/api/restricted_current.txt
@@ -94,6 +94,80 @@
     property public final int last;
   }
 
+  public abstract sealed class FloatFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(float key);
+    method public final int getCapacity();
+    method public final float getOrDefault(float key, float defaultValue);
+    method public final inline float getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal float[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal float[] values;
+  }
+
+  public final class FloatFloatMapKt {
+    method public static androidx.collection.FloatFloatMap emptyFloatFloatMap();
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf();
+    method public static androidx.collection.FloatFloatMap floatFloatMapOf(kotlin.Pair<java.lang.Float,java.lang.Float>... pairs);
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf();
+    method public static androidx.collection.MutableFloatFloatMap mutableFloatFloatMapOf(kotlin.Pair<java.lang.Float,java.lang.Float>... pairs);
+  }
+
+  public abstract sealed class FloatIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(float key);
+    method public final int getCapacity();
+    method public final int getOrDefault(float key, int defaultValue);
+    method public final inline int getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal float[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal int[] values;
+  }
+
+  public final class FloatIntMapKt {
+    method public static androidx.collection.FloatIntMap emptyFloatIntMap();
+    method public static androidx.collection.FloatIntMap floatIntMapOf();
+    method public static androidx.collection.FloatIntMap floatIntMapOf(kotlin.Pair<java.lang.Float,java.lang.Integer>... pairs);
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf();
+    method public static androidx.collection.MutableFloatIntMap mutableFloatIntMapOf(kotlin.Pair<java.lang.Float,java.lang.Integer>... pairs);
+  }
+
   public abstract sealed class FloatList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
@@ -148,6 +222,79 @@
     method public static inline androidx.collection.MutableFloatList mutableFloatListOf(float... elements);
   }
 
+  public abstract sealed class FloatLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(float key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(float key);
+    method public final int getCapacity();
+    method public final long getOrDefault(float key, long defaultValue);
+    method public final inline long getOrElse(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal float[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal long[] values;
+  }
+
+  public final class FloatLongMapKt {
+    method public static androidx.collection.FloatLongMap emptyFloatLongMap();
+    method public static androidx.collection.FloatLongMap floatLongMapOf();
+    method public static androidx.collection.FloatLongMap floatLongMapOf(kotlin.Pair<java.lang.Float,java.lang.Long>... pairs);
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf();
+    method public static androidx.collection.MutableFloatLongMap mutableFloatLongMapOf(kotlin.Pair<java.lang.Float,java.lang.Long>... pairs);
+  }
+
+  public abstract sealed class FloatObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(float key);
+    method public final boolean containsKey(float key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(float key);
+    method public final int getCapacity();
+    method public final V getOrDefault(float key, V defaultValue);
+    method public final inline V getOrElse(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal float[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal Object![] values;
+  }
+
+  public final class FloatObjectMapKt {
+    method public static <V> androidx.collection.FloatObjectMap<V> emptyFloatObjectMap();
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf();
+    method public static <V> androidx.collection.FloatObjectMap<V> floatObjectMapOf(kotlin.Pair<java.lang.Float,? extends V>... pairs);
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf();
+    method public static <V> androidx.collection.MutableFloatObjectMap<V> mutableFloatObjectMapOf(kotlin.Pair<java.lang.Float,? extends V>... pairs);
+  }
+
   public abstract sealed class FloatSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -184,6 +331,80 @@
     method public static androidx.collection.MutableFloatSet mutableFloatSetOf(float... elements);
   }
 
+  public abstract sealed class IntFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(int key);
+    method public final int getCapacity();
+    method public final float getOrDefault(int key, float defaultValue);
+    method public final inline float getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal int[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal float[] values;
+  }
+
+  public final class IntFloatMapKt {
+    method public static androidx.collection.IntFloatMap emptyIntFloatMap();
+    method public static androidx.collection.IntFloatMap intFloatMapOf();
+    method public static androidx.collection.IntFloatMap intFloatMapOf(kotlin.Pair<java.lang.Integer,java.lang.Float>... pairs);
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf();
+    method public static androidx.collection.MutableIntFloatMap mutableIntFloatMapOf(kotlin.Pair<java.lang.Integer,java.lang.Float>... pairs);
+  }
+
+  public abstract sealed class IntIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(int key);
+    method public final int getCapacity();
+    method public final int getOrDefault(int key, int defaultValue);
+    method public final inline int getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal int[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal int[] values;
+  }
+
+  public final class IntIntMapKt {
+    method public static androidx.collection.IntIntMap emptyIntIntMap();
+    method public static androidx.collection.IntIntMap intIntMapOf();
+    method public static androidx.collection.IntIntMap intIntMapOf(kotlin.Pair<java.lang.Integer,java.lang.Integer>... pairs);
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf();
+    method public static androidx.collection.MutableIntIntMap mutableIntIntMapOf(kotlin.Pair<java.lang.Integer,java.lang.Integer>... pairs);
+  }
+
   public abstract sealed class IntList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
@@ -238,6 +459,79 @@
     method public static inline androidx.collection.MutableIntList mutableIntListOf(int... elements);
   }
 
+  public abstract sealed class IntLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(int key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(int key);
+    method public final int getCapacity();
+    method public final long getOrDefault(int key, long defaultValue);
+    method public final inline long getOrElse(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal int[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal long[] values;
+  }
+
+  public final class IntLongMapKt {
+    method public static androidx.collection.IntLongMap emptyIntLongMap();
+    method public static androidx.collection.IntLongMap intLongMapOf();
+    method public static androidx.collection.IntLongMap intLongMapOf(kotlin.Pair<java.lang.Integer,java.lang.Long>... pairs);
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf();
+    method public static androidx.collection.MutableIntLongMap mutableIntLongMapOf(kotlin.Pair<java.lang.Integer,java.lang.Long>... pairs);
+  }
+
+  public abstract sealed class IntObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(int key);
+    method public final boolean containsKey(int key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(int key);
+    method public final int getCapacity();
+    method public final V getOrDefault(int key, V defaultValue);
+    method public final inline V getOrElse(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal int[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal Object![] values;
+  }
+
+  public final class IntObjectMapKt {
+    method public static <V> androidx.collection.IntObjectMap<V> emptyIntObjectMap();
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf();
+    method public static <V> androidx.collection.IntObjectMap<V> intObjectMapOf(kotlin.Pair<java.lang.Integer,? extends V>... pairs);
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf();
+    method public static <V> androidx.collection.MutableIntObjectMap<V> mutableIntObjectMapOf(kotlin.Pair<java.lang.Integer,? extends V>... pairs);
+  }
+
   public abstract sealed class IntSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Integer,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -274,6 +568,80 @@
     method public static androidx.collection.MutableIntSet mutableIntSetOf(int... elements);
   }
 
+  public abstract sealed class LongFloatMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(long key);
+    method public final int getCapacity();
+    method public final float getOrDefault(long key, float defaultValue);
+    method public final inline float getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal long[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal float[] values;
+  }
+
+  public final class LongFloatMapKt {
+    method public static androidx.collection.LongFloatMap emptyLongFloatMap();
+    method public static androidx.collection.LongFloatMap longFloatMapOf();
+    method public static androidx.collection.LongFloatMap longFloatMapOf(kotlin.Pair<java.lang.Long,java.lang.Float>... pairs);
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf();
+    method public static androidx.collection.MutableLongFloatMap mutableLongFloatMapOf(kotlin.Pair<java.lang.Long,java.lang.Float>... pairs);
+  }
+
+  public abstract sealed class LongIntMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(long key);
+    method public final int getCapacity();
+    method public final int getOrDefault(long key, int defaultValue);
+    method public final inline int getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal long[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal int[] values;
+  }
+
+  public final class LongIntMapKt {
+    method public static androidx.collection.LongIntMap emptyLongIntMap();
+    method public static androidx.collection.LongIntMap longIntMapOf();
+    method public static androidx.collection.LongIntMap longIntMapOf(kotlin.Pair<java.lang.Long,java.lang.Integer>... pairs);
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf();
+    method public static androidx.collection.MutableLongIntMap mutableLongIntMapOf(kotlin.Pair<java.lang.Long,java.lang.Integer>... pairs);
+  }
+
   public abstract sealed class LongList {
     method public final boolean any();
     method public final inline boolean any(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
@@ -328,6 +696,79 @@
     method public static inline androidx.collection.MutableLongList mutableLongListOf(long... elements);
   }
 
+  public abstract sealed class LongLongMap {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(long key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(long key);
+    method public final int getCapacity();
+    method public final long getOrDefault(long key, long defaultValue);
+    method public final inline long getOrElse(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal long[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal long[] values;
+  }
+
+  public final class LongLongMapKt {
+    method public static androidx.collection.LongLongMap emptyLongLongMap();
+    method public static androidx.collection.LongLongMap longLongMapOf();
+    method public static androidx.collection.LongLongMap longLongMapOf(kotlin.Pair<java.lang.Long,java.lang.Long>... pairs);
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf();
+    method public static androidx.collection.MutableLongLongMap mutableLongLongMapOf(kotlin.Pair<java.lang.Long,java.lang.Long>... pairs);
+  }
+
+  public abstract sealed class LongObjectMap<V> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final operator boolean contains(long key);
+    method public final boolean containsKey(long key);
+    method public final boolean containsValue(V value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super V,kotlin.Unit> block);
+    method public final operator V? get(long key);
+    method public final int getCapacity();
+    method public final V getOrDefault(long key, V defaultValue);
+    method public final inline V getOrElse(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal long[] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal Object![] values;
+  }
+
+  public final class LongObjectMapKt {
+    method public static <V> androidx.collection.LongObjectMap<V> emptyLongObjectMap();
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf();
+    method public static <V> androidx.collection.LongObjectMap<V> longObjectMapOf(kotlin.Pair<java.lang.Long,? extends V>... pairs);
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf();
+    method public static <V> androidx.collection.MutableLongObjectMap<V> mutableLongObjectMapOf(kotlin.Pair<java.lang.Long,? extends V>... pairs);
+  }
+
   public abstract sealed class LongSet {
     method public final inline boolean all(kotlin.jvm.functions.Function1<? super java.lang.Long,java.lang.Boolean> predicate);
     method public final boolean any();
@@ -431,6 +872,48 @@
     method public static inline <K, V> androidx.collection.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved);
   }
 
+  public final class MutableFloatFloatMap extends androidx.collection.FloatFloatMap {
+    ctor public MutableFloatFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatFloatMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Float> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Float>![] pairs);
+    method public void put(float key, float value);
+    method public void putAll(androidx.collection.FloatFloatMap from);
+    method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Float>![] pairs);
+    method public void remove(float key);
+    method public boolean remove(float key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(float key, float value);
+    method public int trim();
+  }
+
+  public final class MutableFloatIntMap extends androidx.collection.FloatIntMap {
+    ctor public MutableFloatIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatIntMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Integer> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Integer>![] pairs);
+    method public void put(float key, int value);
+    method public void putAll(androidx.collection.FloatIntMap from);
+    method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Integer>![] pairs);
+    method public void remove(float key);
+    method public boolean remove(float key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(float key, int value);
+    method public int trim();
+  }
+
   public final class MutableFloatList extends androidx.collection.FloatList {
     ctor public MutableFloatList(optional int initialCapacity);
     method public boolean add(float element);
@@ -462,6 +945,48 @@
     property public final inline int capacity;
   }
 
+  public final class MutableFloatLongMap extends androidx.collection.FloatLongMap {
+    ctor public MutableFloatLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(float key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatLongMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Long> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,java.lang.Long>![] pairs);
+    method public void put(float key, long value);
+    method public void putAll(androidx.collection.FloatLongMap from);
+    method public void putAll(kotlin.Pair<java.lang.Float,java.lang.Long>![] pairs);
+    method public void remove(float key);
+    method public boolean remove(float key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(float key, long value);
+    method public int trim();
+  }
+
+  public final class MutableFloatObjectMap<V> extends androidx.collection.FloatObjectMap<V> {
+    ctor public MutableFloatObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(float key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.FloatList keys);
+    method public inline operator void minusAssign(androidx.collection.FloatSet keys);
+    method public inline operator void minusAssign(float key);
+    method public inline operator void minusAssign(float[] keys);
+    method public inline operator void plusAssign(androidx.collection.FloatObjectMap<V> from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,? extends V> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Float,? extends V>![] pairs);
+    method public V? put(float key, V value);
+    method public void putAll(androidx.collection.FloatObjectMap<V> from);
+    method public void putAll(kotlin.Pair<java.lang.Float,? extends V>![] pairs);
+    method public V? remove(float key);
+    method public boolean remove(float key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Float,? super V,java.lang.Boolean> predicate);
+    method public operator void set(float key, V value);
+    method public int trim();
+  }
+
   public final class MutableFloatSet extends androidx.collection.FloatSet {
     ctor public MutableFloatSet(optional int initialCapacity);
     method public boolean add(float element);
@@ -480,6 +1005,48 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableIntFloatMap extends androidx.collection.IntFloatMap {
+    ctor public MutableIntFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntFloatMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Float> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Float>![] pairs);
+    method public void put(int key, float value);
+    method public void putAll(androidx.collection.IntFloatMap from);
+    method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Float>![] pairs);
+    method public void remove(int key);
+    method public boolean remove(int key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(int key, float value);
+    method public int trim();
+  }
+
+  public final class MutableIntIntMap extends androidx.collection.IntIntMap {
+    ctor public MutableIntIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntIntMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Integer> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Integer>![] pairs);
+    method public void put(int key, int value);
+    method public void putAll(androidx.collection.IntIntMap from);
+    method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Integer>![] pairs);
+    method public void remove(int key);
+    method public boolean remove(int key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(int key, int value);
+    method public int trim();
+  }
+
   public final class MutableIntList extends androidx.collection.IntList {
     ctor public MutableIntList(optional int initialCapacity);
     method public boolean add(int element);
@@ -511,6 +1078,48 @@
     property public final inline int capacity;
   }
 
+  public final class MutableIntLongMap extends androidx.collection.IntLongMap {
+    ctor public MutableIntLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntLongMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Long> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,java.lang.Long>![] pairs);
+    method public void put(int key, long value);
+    method public void putAll(androidx.collection.IntLongMap from);
+    method public void putAll(kotlin.Pair<java.lang.Integer,java.lang.Long>![] pairs);
+    method public void remove(int key);
+    method public boolean remove(int key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(int key, long value);
+    method public int trim();
+  }
+
+  public final class MutableIntObjectMap<V> extends androidx.collection.IntObjectMap<V> {
+    ctor public MutableIntObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(int key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.IntList keys);
+    method public inline operator void minusAssign(androidx.collection.IntSet keys);
+    method public inline operator void minusAssign(int key);
+    method public inline operator void minusAssign(int[] keys);
+    method public inline operator void plusAssign(androidx.collection.IntObjectMap<V> from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,? extends V> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Integer,? extends V>![] pairs);
+    method public V? put(int key, V value);
+    method public void putAll(androidx.collection.IntObjectMap<V> from);
+    method public void putAll(kotlin.Pair<java.lang.Integer,? extends V>![] pairs);
+    method public V? remove(int key);
+    method public boolean remove(int key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super V,java.lang.Boolean> predicate);
+    method public operator void set(int key, V value);
+    method public int trim();
+  }
+
   public final class MutableIntSet extends androidx.collection.IntSet {
     ctor public MutableIntSet(optional int initialCapacity);
     method public boolean add(int element);
@@ -529,6 +1138,48 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableLongFloatMap extends androidx.collection.LongFloatMap {
+    ctor public MutableLongFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongFloatMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Float> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Float>![] pairs);
+    method public void put(long key, float value);
+    method public void putAll(androidx.collection.LongFloatMap from);
+    method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Float>![] pairs);
+    method public void remove(long key);
+    method public boolean remove(long key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(long key, float value);
+    method public int trim();
+  }
+
+  public final class MutableLongIntMap extends androidx.collection.LongIntMap {
+    ctor public MutableLongIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongIntMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Integer> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Integer>![] pairs);
+    method public void put(long key, int value);
+    method public void putAll(androidx.collection.LongIntMap from);
+    method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Integer>![] pairs);
+    method public void remove(long key);
+    method public boolean remove(long key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(long key, int value);
+    method public int trim();
+  }
+
   public final class MutableLongList extends androidx.collection.LongList {
     ctor public MutableLongList(optional int initialCapacity);
     method public void add(@IntRange(from=0L) int index, long element);
@@ -560,6 +1211,48 @@
     property public final inline int capacity;
   }
 
+  public final class MutableLongLongMap extends androidx.collection.LongLongMap {
+    ctor public MutableLongLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(long key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongLongMap from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Long> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,java.lang.Long>![] pairs);
+    method public void put(long key, long value);
+    method public void putAll(androidx.collection.LongLongMap from);
+    method public void putAll(kotlin.Pair<java.lang.Long,java.lang.Long>![] pairs);
+    method public void remove(long key);
+    method public boolean remove(long key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(long key, long value);
+    method public int trim();
+  }
+
+  public final class MutableLongObjectMap<V> extends androidx.collection.LongObjectMap<V> {
+    ctor public MutableLongObjectMap(optional int initialCapacity);
+    method public void clear();
+    method public inline V getOrPut(long key, kotlin.jvm.functions.Function0<? extends V> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.LongList keys);
+    method public inline operator void minusAssign(androidx.collection.LongSet keys);
+    method public inline operator void minusAssign(long key);
+    method public inline operator void minusAssign(long[] keys);
+    method public inline operator void plusAssign(androidx.collection.LongObjectMap<V> from);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,? extends V> pair);
+    method public inline operator void plusAssign(kotlin.Pair<java.lang.Long,? extends V>![] pairs);
+    method public V? put(long key, V value);
+    method public void putAll(androidx.collection.LongObjectMap<V> from);
+    method public void putAll(kotlin.Pair<java.lang.Long,? extends V>![] pairs);
+    method public V? remove(long key);
+    method public boolean remove(long key, V value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super java.lang.Long,? super V,java.lang.Boolean> predicate);
+    method public operator void set(long key, V value);
+    method public int trim();
+  }
+
   public final class MutableLongSet extends androidx.collection.LongSet {
     ctor public MutableLongSet(optional int initialCapacity);
     method public boolean add(long element);
@@ -578,6 +1271,124 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public final class MutableObjectFloatMap<K> extends androidx.collection.ObjectFloatMap<K> {
+    ctor public MutableObjectFloatMap(optional int initialCapacity);
+    method public void clear();
+    method public inline float getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectFloatMap<K> from);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Float> pair);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Float>![] pairs);
+    method public void put(K key, float value);
+    method public void putAll(androidx.collection.ObjectFloatMap<K> from);
+    method public void putAll(kotlin.Pair<? extends K,java.lang.Float>![] pairs);
+    method public void remove(K key);
+    method public boolean remove(K key, float value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public operator void set(K key, float value);
+    method public int trim();
+  }
+
+  public final class MutableObjectIntMap<K> extends androidx.collection.ObjectIntMap<K> {
+    ctor public MutableObjectIntMap(optional int initialCapacity);
+    method public void clear();
+    method public inline int getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectIntMap<K> from);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Integer> pair);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Integer>![] pairs);
+    method public void put(K key, int value);
+    method public void putAll(androidx.collection.ObjectIntMap<K> from);
+    method public void putAll(kotlin.Pair<? extends K,java.lang.Integer>![] pairs);
+    method public void remove(K key);
+    method public boolean remove(K key, int value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public operator void set(K key, int value);
+    method public int trim();
+  }
+
+  public final class MutableObjectList<E> extends androidx.collection.ObjectList<E> {
+    ctor public MutableObjectList(optional int initialCapacity);
+    method public boolean add(E element);
+    method public void add(@IntRange(from=0L) int index, E element);
+    method public boolean addAll(androidx.collection.ObjectList<E> elements);
+    method public boolean addAll(androidx.collection.ScatterSet<E> elements);
+    method public boolean addAll(E![] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, androidx.collection.ObjectList<E> elements);
+    method public boolean addAll(@IntRange(from=0L) int index, E![] elements);
+    method public boolean addAll(@IntRange(from=0L) int index, java.util.Collection<? extends E> elements);
+    method public boolean addAll(Iterable<? extends E> elements);
+    method public boolean addAll(java.util.List<? extends E> elements);
+    method public boolean addAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public java.util.List<E> asList();
+    method public java.util.List<E> asMutableList();
+    method public void clear();
+    method public void ensureCapacity(int capacity);
+    method public inline int getCapacity();
+    method public operator void minusAssign(androidx.collection.ObjectList<E> elements);
+    method public operator void minusAssign(androidx.collection.ScatterSet<E> elements);
+    method public inline operator void minusAssign(E element);
+    method public operator void minusAssign(E![] elements);
+    method public operator void minusAssign(Iterable<? extends E> elements);
+    method public operator void minusAssign(java.util.List<? extends E> elements);
+    method public operator void minusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator void plusAssign(androidx.collection.ObjectList<E> elements);
+    method public operator void plusAssign(androidx.collection.ScatterSet<E> elements);
+    method public inline operator void plusAssign(E element);
+    method public operator void plusAssign(E![] elements);
+    method public operator void plusAssign(Iterable<? extends E> elements);
+    method public operator void plusAssign(java.util.List<? extends E> elements);
+    method public operator void plusAssign(kotlin.sequences.Sequence<? extends E> elements);
+    method public boolean remove(E element);
+    method public boolean removeAll(androidx.collection.ObjectList<E> elements);
+    method public boolean removeAll(androidx.collection.ScatterSet<E> elements);
+    method public boolean removeAll(E![] elements);
+    method public boolean removeAll(Iterable<? extends E> elements);
+    method public boolean removeAll(java.util.List<? extends E> elements);
+    method public boolean removeAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public E removeAt(@IntRange(from=0L) int index);
+    method public inline void removeIf(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public void removeRange(@IntRange(from=0L) int start, @IntRange(from=0L) int end);
+    method public boolean retainAll(androidx.collection.ObjectList<E> elements);
+    method public boolean retainAll(E![] elements);
+    method public boolean retainAll(Iterable<? extends E> elements);
+    method public boolean retainAll(java.util.Collection<? extends E> elements);
+    method public boolean retainAll(kotlin.sequences.Sequence<? extends E> elements);
+    method public operator E set(@IntRange(from=0L) int index, E element);
+    method public void trim(optional int minCapacity);
+    property public final inline int capacity;
+  }
+
+  public final class MutableObjectLongMap<K> extends androidx.collection.ObjectLongMap<K> {
+    ctor public MutableObjectLongMap(optional int initialCapacity);
+    method public void clear();
+    method public inline long getOrPut(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public inline operator void minusAssign(androidx.collection.ScatterSet<K> keys);
+    method public inline operator void minusAssign(Iterable<? extends K> keys);
+    method public inline operator void minusAssign(K key);
+    method public inline operator void minusAssign(K![] keys);
+    method public inline operator void minusAssign(kotlin.sequences.Sequence<? extends K> keys);
+    method public inline operator void plusAssign(androidx.collection.ObjectLongMap<K> from);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Long> pair);
+    method public inline operator void plusAssign(kotlin.Pair<? extends K,java.lang.Long>![] pairs);
+    method public void put(K key, long value);
+    method public void putAll(androidx.collection.ObjectLongMap<K> from);
+    method public void putAll(kotlin.Pair<? extends K,java.lang.Long>![] pairs);
+    method public void remove(K key);
+    method public boolean remove(K key, long value);
+    method public void removeIf(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public operator void set(K key, long value);
+    method public int trim();
+  }
+
   public final class MutableScatterMap<K, V> extends androidx.collection.ScatterMap<K,V> {
     ctor public MutableScatterMap(optional int initialCapacity);
     method public java.util.Map<K,V> asMutableMap();
@@ -635,6 +1446,179 @@
     method @IntRange(from=0L) public int trim();
   }
 
+  public abstract sealed class ObjectFloatMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(float value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Float,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> block);
+    method public final operator float get(K key);
+    method public final int getCapacity();
+    method public final float getOrDefault(K key, float defaultValue);
+    method public final inline float getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Float> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal Object![] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal float[] values;
+  }
+
+  public final class ObjectFloatMapKt {
+    method public static <K> androidx.collection.ObjectFloatMap<K> emptyObjectFloatMap();
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf();
+    method public static <K> androidx.collection.MutableObjectFloatMap<K> mutableObjectFloatMapOf(kotlin.Pair<? extends K,java.lang.Float>... pairs);
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMap();
+    method public static <K> androidx.collection.ObjectFloatMap<K> objectFloatMapOf(kotlin.Pair<? extends K,java.lang.Float>... pairs);
+  }
+
+  public abstract sealed class ObjectIntMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(int value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Integer,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final operator int get(K key);
+    method public final int getCapacity();
+    method public final int getOrDefault(K key, int defaultValue);
+    method public final inline int getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal Object![] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal int[] values;
+  }
+
+  public final class ObjectIntMapKt {
+    method public static <K> androidx.collection.ObjectIntMap<K> emptyObjectIntMap();
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf();
+    method public static <K> androidx.collection.MutableObjectIntMap<K> mutableObjectIntMapOf(kotlin.Pair<? extends K,java.lang.Integer>... pairs);
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMap();
+    method public static <K> androidx.collection.ObjectIntMap<K> objectIntMapOf(kotlin.Pair<? extends K,java.lang.Integer>... pairs);
+  }
+
+  public abstract sealed class ObjectList<E> {
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public abstract java.util.List<E> asList();
+    method public final operator boolean contains(E element);
+    method public final boolean containsAll(androidx.collection.ObjectList<E> elements);
+    method public final boolean containsAll(E![] elements);
+    method public final boolean containsAll(Iterable<? extends E> elements);
+    method public final boolean containsAll(java.util.List<? extends E> elements);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final E elementAt(@IntRange(from=0L) int index);
+    method public final inline E elementAtOrElse(@IntRange(from=0L) int index, kotlin.jvm.functions.Function1<? super java.lang.Integer,? extends E> defaultValue);
+    method public final E first();
+    method public final inline E first(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline E? firstOrNull();
+    method public final inline E? firstOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline <R> R fold(R initial, kotlin.jvm.functions.Function2<? super R,? super E,? extends R> operation);
+    method public final inline <R> R foldIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super R,? super E,? extends R> operation);
+    method public final inline <R> R foldRight(R initial, kotlin.jvm.functions.Function2<? super E,? super R,? extends R> operation);
+    method public final inline <R> R foldRightIndexed(R initial, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super E,? super R,? extends R> operation);
+    method public final inline void forEach(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+    method public final inline void forEachIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+    method public final inline void forEachReversed(kotlin.jvm.functions.Function1<? super E,kotlin.Unit> block);
+    method public final inline void forEachReversedIndexed(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super E,kotlin.Unit> block);
+    method public final operator E get(@IntRange(from=0L) int index);
+    method public final inline kotlin.ranges.IntRange getIndices();
+    method @IntRange(from=-1L) public final inline int getLastIndex();
+    method @IntRange(from=0L) public final int getSize();
+    method public final int indexOf(E element);
+    method public final inline int indexOfFirst(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final inline int indexOfLast(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final E last();
+    method public final inline E last(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final int lastIndexOf(E element);
+    method public final inline E? lastOrNull();
+    method public final inline E? lastOrNull(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    method public final boolean none();
+    method public final inline boolean reversedAny(kotlin.jvm.functions.Function1<? super E,java.lang.Boolean> predicate);
+    property public final inline kotlin.ranges.IntRange indices;
+    property @IntRange(from=-1L) public final inline int lastIndex;
+    property @IntRange(from=0L) public final int size;
+    field @kotlin.PublishedApi internal int _size;
+    field @kotlin.PublishedApi internal Object![] content;
+  }
+
+  public final class ObjectListKt {
+    method public static <E> androidx.collection.ObjectList<E> emptyObjectList();
+    method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf();
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1);
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2);
+    method public static <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E element1, E element2, E element3);
+    method public static inline <E> androidx.collection.MutableObjectList<E> mutableObjectListOf(E?... elements);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf();
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E element1, E element2, E element3);
+    method public static <E> androidx.collection.ObjectList<E> objectListOf(E?... elements);
+  }
+
+  public abstract sealed class ObjectLongMap<K> {
+    method public final inline boolean all(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final boolean any();
+    method public final inline boolean any(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method public final operator boolean contains(K key);
+    method public final boolean containsKey(K key);
+    method public final boolean containsValue(long value);
+    method public final int count();
+    method public final inline int count(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,java.lang.Boolean> predicate);
+    method @kotlin.PublishedApi internal final int findKeyIndex(K key);
+    method public final inline void forEach(kotlin.jvm.functions.Function2<? super K,? super java.lang.Long,kotlin.Unit> block);
+    method @kotlin.PublishedApi internal final inline void forEachIndexed(kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> block);
+    method public final inline void forEachKey(kotlin.jvm.functions.Function1<? super K,kotlin.Unit> block);
+    method public final inline void forEachValue(kotlin.jvm.functions.Function1<? super java.lang.Long,kotlin.Unit> block);
+    method public final operator long get(K key);
+    method public final int getCapacity();
+    method public final long getOrDefault(K key, long defaultValue);
+    method public final inline long getOrElse(K key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+    method public final int getSize();
+    method public final boolean isEmpty();
+    method public final boolean isNotEmpty();
+    method public final boolean none();
+    property public final int capacity;
+    property public final int size;
+    field @kotlin.PublishedApi internal Object![] keys;
+    field @kotlin.PublishedApi internal long[] metadata;
+    field @kotlin.PublishedApi internal long[] values;
+  }
+
+  public final class ObjectLongMapKt {
+    method public static <K> androidx.collection.ObjectLongMap<K> emptyObjectLongMap();
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf();
+    method public static <K> androidx.collection.MutableObjectLongMap<K> mutableObjectLongMapOf(kotlin.Pair<? extends K,java.lang.Long>... pairs);
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMap();
+    method public static <K> androidx.collection.ObjectLongMap<K> objectLongMapOf(kotlin.Pair<? extends K,java.lang.Long>... pairs);
+  }
+
   @kotlin.jvm.JvmInline public final value class PairFloatFloat {
     ctor public PairFloatFloat(float first, float second);
     method public inline operator float component1();
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
new file mode 100644
index 0000000..bca375a
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatFloatMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatFloatMap = MutableFloatFloatMap(0)
+
+/**
+ * Returns an empty, read-only [FloatFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyFloatFloatMap(): FloatFloatMap = EmptyFloatFloatMap
+
+/**
+ * Returns a new [MutableFloatFloatMap].
+ */
+public fun floatFloatMapOf(): FloatFloatMap = EmptyFloatFloatMap
+
+/**
+ * Returns a new [FloatFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun floatFloatMapOf(vararg pairs: Pair<Float, Float>): FloatFloatMap =
+    MutableFloatFloatMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableFloatFloatMap].
+ */
+public fun mutableFloatFloatMapOf(): MutableFloatFloatMap = MutableFloatFloatMap()
+
+/**
+ * Returns a new [MutableFloatFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableFloatFloatMapOf(vararg pairs: Pair<Float, Float>): MutableFloatFloatMap =
+    MutableFloatFloatMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [FloatFloatMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatFloatMap].
+ *
+ * @see [MutableFloatFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatFloatMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: FloatArray = EmptyFloatArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: FloatArray = EmptyFloatArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Float): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Float, defaultValue: Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Float, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Float, value: Float) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Float) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Float) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Float, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Float, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Float, Float) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Float): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [FloatFloatMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is FloatFloatMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableFloatFloatMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatFloatMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : FloatFloatMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = FloatArray(newCapacity)
+        values = FloatArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Float, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Float, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Float, value: Float) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Float] key and [Float] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Float, Float>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: FloatFloatMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Float] key and [Float] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Float, Float>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Float] key and [Float] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Float, Float>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: FloatFloatMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Float) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Float, value: Float): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Float, Float) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Float) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableFloatFloatMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
new file mode 100644
index 0000000..29adea3
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatIntMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatIntMap = MutableFloatIntMap(0)
+
+/**
+ * Returns an empty, read-only [FloatIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyFloatIntMap(): FloatIntMap = EmptyFloatIntMap
+
+/**
+ * Returns a new [MutableFloatIntMap].
+ */
+public fun floatIntMapOf(): FloatIntMap = EmptyFloatIntMap
+
+/**
+ * Returns a new [FloatIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun floatIntMapOf(vararg pairs: Pair<Float, Int>): FloatIntMap =
+    MutableFloatIntMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableFloatIntMap].
+ */
+public fun mutableFloatIntMapOf(): MutableFloatIntMap = MutableFloatIntMap()
+
+/**
+ * Returns a new [MutableFloatIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableFloatIntMapOf(vararg pairs: Pair<Float, Int>): MutableFloatIntMap =
+    MutableFloatIntMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [FloatIntMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatIntMap].
+ *
+ * @see [MutableFloatIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatIntMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: FloatArray = EmptyFloatArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: IntArray = EmptyIntArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Float): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Float, defaultValue: Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Float, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Float, value: Int) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Float) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Int) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Float, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Float, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Float, Int) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Int): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [FloatIntMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is FloatIntMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableFloatIntMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatIntMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : FloatIntMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = FloatArray(newCapacity)
+        values = IntArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Float, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Float, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Float, value: Int) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Float] key and [Int] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Float, Int>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: FloatIntMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Float] key and [Int] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Float, Int>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Float] key and [Int] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Float, Int>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: FloatIntMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Float) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Float, value: Int): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Float, Int) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Float) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableFloatIntMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
index cf0c648..70b5787 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatList.kt
@@ -22,6 +22,14 @@
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 /**
  * [FloatList] is a [List]-like collection for [Float] values. It allows retrieving
  * the elements without boxing. [FloatList] is always backed by a [MutableFloatList],
@@ -835,10 +843,6 @@
     }
 }
 
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyFloatArray = FloatArray(0)
-
 private val EmptyFloatList: FloatList = MutableFloatList(0)
 
 /**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
new file mode 100644
index 0000000..ac71a3e
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatLongMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatLongMap = MutableFloatLongMap(0)
+
+/**
+ * Returns an empty, read-only [FloatLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyFloatLongMap(): FloatLongMap = EmptyFloatLongMap
+
+/**
+ * Returns a new [MutableFloatLongMap].
+ */
+public fun floatLongMapOf(): FloatLongMap = EmptyFloatLongMap
+
+/**
+ * Returns a new [FloatLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun floatLongMapOf(vararg pairs: Pair<Float, Long>): FloatLongMap =
+    MutableFloatLongMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableFloatLongMap].
+ */
+public fun mutableFloatLongMapOf(): MutableFloatLongMap = MutableFloatLongMap()
+
+/**
+ * Returns a new [MutableFloatLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Float] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableFloatLongMapOf(vararg pairs: Pair<Float, Long>): MutableFloatLongMap =
+    MutableFloatLongMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [FloatLongMap] is a container with a [Map]-like interface for
+ * [Float] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatLongMap].
+ *
+ * @see [MutableFloatLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class FloatLongMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: FloatArray = EmptyFloatArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: LongArray = EmptyLongArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Float): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Float, defaultValue: Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Float, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Float, value: Long) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Float) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Long) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Float, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Float, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Float, Long) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Long): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [FloatLongMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is FloatLongMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableFloatLongMap] is a container with a [MutableMap]-like interface for
+ * [Float] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableFloatLongMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : FloatLongMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = FloatArray(newCapacity)
+        values = LongArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Float, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Float, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Float, value: Long) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Float] key and [Long] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Float, Long>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: FloatLongMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Float] key and [Long] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Float, Long>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Float] key and [Long] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Float, Long>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: FloatLongMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Float) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Float, value: Long): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Float, Long) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Float) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableFloatLongMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
new file mode 100644
index 0000000..292347f
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatObjectMap.kt
@@ -0,0 +1,847 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyFloatObjectMap = MutableFloatObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [FloatObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyFloatObjectMap(): FloatObjectMap<V> = EmptyFloatObjectMap as FloatObjectMap<V>
+
+/**
+ * Returns an empty, read-only [FloatObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> floatObjectMapOf(): FloatObjectMap<V> = EmptyFloatObjectMap as FloatObjectMap<V>
+
+/**
+ * Returns a new [FloatObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> floatObjectMapOf(vararg pairs: Pair<Float, V>): FloatObjectMap<V> =
+    MutableFloatObjectMap<V>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableFloatObjectMap].
+ */
+public fun <V> mutableFloatObjectMapOf(): MutableFloatObjectMap<V> = MutableFloatObjectMap()
+
+/**
+ * Returns a new [MutableFloatObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> mutableFloatObjectMapOf(vararg pairs: Pair<Float, V>): MutableFloatObjectMap<V> =
+    MutableFloatObjectMap<V>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [FloatObjectMap] is a container with a [Map]-like interface for keys with
+ * [Float] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableFloatObjectMap].
+ *
+ * @see [MutableFloatObjectMap]
+ */
+public sealed class FloatObjectMap<V> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: FloatArray = EmptyFloatArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: Array<Any?> = EMPTY_OBJECTS
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     */
+    public operator fun get(key: Float): V? {
+        val index = findKeyIndex(key)
+        @Suppress("UNCHECKED_CAST")
+        return if (index >= 0) values[index] as V? else null
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Float, defaultValue: V): V {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            @Suppress("UNCHECKED_CAST")
+            return values[index] as V
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Float, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Float, value: V) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index], v[index] as V)
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Float) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: V) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(v[index] as V)
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Float, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Float, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Float, V) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Float): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: V): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [FloatObjectMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is FloatObjectMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value == null) {
+                if (other[key] != null || !other.containsKey(key)) {
+                    return false
+                }
+            } else if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(if (value === this) "(this)" else value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    internal inline fun findKeyIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableFloatObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Float] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableFloatObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableFloatObjectMap<V>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : FloatObjectMap<V>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = FloatArray(newCapacity)
+        values = arrayOfNulls(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Float, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue().also { set(key, it) }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Float, value: V) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Float, value: V): V? {
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
+        values[index] = value
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Float] key is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<Float, V>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: FloatObjectMap<V>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that the [Pair] is allocated and the [Float] key is boxed.
+     * Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Float, V>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Float] key is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<out Pair<Float, V>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: FloatObjectMap<V>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map. If the
+     * [key] was present in the map, this function returns the value that was
+     * present before removal.
+     */
+    public fun remove(key: Float): V? {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return removeValueAt(index)
+        }
+        return null
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Float, value: V): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Float, V) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index], values[index] as V)) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Float) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: FloatArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatSet) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: FloatList) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int): V? {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        val oldValue = values[index]
+        values[index] = null
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        values.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Float): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableFloatObjectMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
index 278902c4..f08a176 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/FloatSet.kt
@@ -29,13 +29,21 @@
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
 
-// This is a copy of ScatterSet, but with Float elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
 
 // Default empty set to avoid allocations
 private val EmptyFloatSet = MutableFloatSet(0)
 
 // An empty array of floats
-private val EmptyFloatArray = FloatArray(0)
+internal val EmptyFloatArray = FloatArray(0)
 
 /**
  * Returns an empty, read-only [FloatSet].
@@ -770,7 +778,7 @@
  * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
  * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
  */
-private inline fun hash(k: Float): Int {
+internal inline fun hash(k: Float): Int {
     val hash = k.hashCode()
     return hash xor (hash ushr 16)
 }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
new file mode 100644
index 0000000..b9151ff7
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntFloatMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntFloatMap = MutableIntFloatMap(0)
+
+/**
+ * Returns an empty, read-only [IntFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyIntFloatMap(): IntFloatMap = EmptyIntFloatMap
+
+/**
+ * Returns a new [MutableIntFloatMap].
+ */
+public fun intFloatMapOf(): IntFloatMap = EmptyIntFloatMap
+
+/**
+ * Returns a new [IntFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun intFloatMapOf(vararg pairs: Pair<Int, Float>): IntFloatMap =
+    MutableIntFloatMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableIntFloatMap].
+ */
+public fun mutableIntFloatMapOf(): MutableIntFloatMap = MutableIntFloatMap()
+
+/**
+ * Returns a new [MutableIntFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableIntFloatMapOf(vararg pairs: Pair<Int, Float>): MutableIntFloatMap =
+    MutableIntFloatMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [IntFloatMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntFloatMap].
+ *
+ * @see [MutableIntFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntFloatMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: IntArray = EmptyIntArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: FloatArray = EmptyFloatArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Int): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Int, defaultValue: Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Int, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Int, value: Float) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Int) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Float) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Int, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Int, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Int, Float) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Float): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [IntFloatMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is IntFloatMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableIntFloatMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntFloatMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : IntFloatMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = IntArray(newCapacity)
+        values = FloatArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Int, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Int, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Int, value: Float) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Int] key and [Float] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Int, Float>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: IntFloatMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Int] key and [Float] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Int, Float>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Int] key and [Float] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Int, Float>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: IntFloatMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Int) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Int, value: Float): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Int, Float) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Int) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableIntFloatMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
new file mode 100644
index 0000000..5d288b2
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntIntMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntIntMap = MutableIntIntMap(0)
+
+/**
+ * Returns an empty, read-only [IntIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyIntIntMap(): IntIntMap = EmptyIntIntMap
+
+/**
+ * Returns a new [MutableIntIntMap].
+ */
+public fun intIntMapOf(): IntIntMap = EmptyIntIntMap
+
+/**
+ * Returns a new [IntIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun intIntMapOf(vararg pairs: Pair<Int, Int>): IntIntMap =
+    MutableIntIntMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableIntIntMap].
+ */
+public fun mutableIntIntMapOf(): MutableIntIntMap = MutableIntIntMap()
+
+/**
+ * Returns a new [MutableIntIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableIntIntMapOf(vararg pairs: Pair<Int, Int>): MutableIntIntMap =
+    MutableIntIntMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [IntIntMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntIntMap].
+ *
+ * @see [MutableIntIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntIntMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: IntArray = EmptyIntArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: IntArray = EmptyIntArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Int): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Int, defaultValue: Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Int, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Int, value: Int) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Int) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Int) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Int, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Int, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Int, Int) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Int): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [IntIntMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is IntIntMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableIntIntMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntIntMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : IntIntMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = IntArray(newCapacity)
+        values = IntArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Int, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Int, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Int, value: Int) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Int] key and [Int] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Int, Int>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: IntIntMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Int] key and [Int] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Int, Int>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Int] key and [Int] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Int, Int>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: IntIntMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Int) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Int, value: Int): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Int, Int) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Int) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableIntIntMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
index 8c6122db..4260def 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntList.kt
@@ -22,6 +22,14 @@
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 /**
  * [IntList] is a [List]-like collection for [Int] values. It allows retrieving
  * the elements without boxing. [IntList] is always backed by a [MutableIntList],
@@ -835,10 +843,6 @@
     }
 }
 
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyIntArray = IntArray(0)
-
 private val EmptyIntList: IntList = MutableIntList(0)
 
 /**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
new file mode 100644
index 0000000..ce4455a
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntLongMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntLongMap = MutableIntLongMap(0)
+
+/**
+ * Returns an empty, read-only [IntLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyIntLongMap(): IntLongMap = EmptyIntLongMap
+
+/**
+ * Returns a new [MutableIntLongMap].
+ */
+public fun intLongMapOf(): IntLongMap = EmptyIntLongMap
+
+/**
+ * Returns a new [IntLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun intLongMapOf(vararg pairs: Pair<Int, Long>): IntLongMap =
+    MutableIntLongMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableIntLongMap].
+ */
+public fun mutableIntLongMapOf(): MutableIntLongMap = MutableIntLongMap()
+
+/**
+ * Returns a new [MutableIntLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Int] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableIntLongMapOf(vararg pairs: Pair<Int, Long>): MutableIntLongMap =
+    MutableIntLongMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [IntLongMap] is a container with a [Map]-like interface for
+ * [Int] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntLongMap].
+ *
+ * @see [MutableIntLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class IntLongMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: IntArray = EmptyIntArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: LongArray = EmptyLongArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Int): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Int, defaultValue: Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Int, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Int, value: Long) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Int) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Long) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Int, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Int, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Int, Long) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Long): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [IntLongMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is IntLongMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableIntLongMap] is a container with a [MutableMap]-like interface for
+ * [Int] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableIntLongMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : IntLongMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = IntArray(newCapacity)
+        values = LongArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Int, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Int, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Int, value: Long) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Int] key and [Long] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Int, Long>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: IntLongMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Int] key and [Long] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Int, Long>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Int] key and [Long] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Int, Long>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: IntLongMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Int) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Int, value: Long): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Int, Long) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Int) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableIntLongMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
new file mode 100644
index 0000000..1626d89
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntObjectMap.kt
@@ -0,0 +1,847 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyIntObjectMap = MutableIntObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [IntObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyIntObjectMap(): IntObjectMap<V> = EmptyIntObjectMap as IntObjectMap<V>
+
+/**
+ * Returns an empty, read-only [IntObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> intObjectMapOf(): IntObjectMap<V> = EmptyIntObjectMap as IntObjectMap<V>
+
+/**
+ * Returns a new [IntObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> intObjectMapOf(vararg pairs: Pair<Int, V>): IntObjectMap<V> =
+    MutableIntObjectMap<V>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableIntObjectMap].
+ */
+public fun <V> mutableIntObjectMapOf(): MutableIntObjectMap<V> = MutableIntObjectMap()
+
+/**
+ * Returns a new [MutableIntObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> mutableIntObjectMapOf(vararg pairs: Pair<Int, V>): MutableIntObjectMap<V> =
+    MutableIntObjectMap<V>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [IntObjectMap] is a container with a [Map]-like interface for keys with
+ * [Int] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableIntObjectMap].
+ *
+ * @see [MutableIntObjectMap]
+ */
+public sealed class IntObjectMap<V> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: IntArray = EmptyIntArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: Array<Any?> = EMPTY_OBJECTS
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     */
+    public operator fun get(key: Int): V? {
+        val index = findKeyIndex(key)
+        @Suppress("UNCHECKED_CAST")
+        return if (index >= 0) values[index] as V? else null
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Int, defaultValue: V): V {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            @Suppress("UNCHECKED_CAST")
+            return values[index] as V
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Int, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Int, value: V) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index], v[index] as V)
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Int) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: V) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(v[index] as V)
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Int, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Int, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Int, V) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Int): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: V): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [IntObjectMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is IntObjectMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value == null) {
+                if (other[key] != null || !other.containsKey(key)) {
+                    return false
+                }
+            } else if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(if (value === this) "(this)" else value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    internal inline fun findKeyIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableIntObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Int] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableIntObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableIntObjectMap<V>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : IntObjectMap<V>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = IntArray(newCapacity)
+        values = arrayOfNulls(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Int, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue().also { set(key, it) }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Int, value: V) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Int, value: V): V? {
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
+        values[index] = value
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Int] key is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<Int, V>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: IntObjectMap<V>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that the [Pair] is allocated and the [Int] key is boxed.
+     * Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Int, V>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Int] key is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<out Pair<Int, V>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: IntObjectMap<V>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map. If the
+     * [key] was present in the map, this function returns the value that was
+     * present before removal.
+     */
+    public fun remove(key: Int): V? {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return removeValueAt(index)
+        }
+        return null
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Int, value: V): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Int, V) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index], values[index] as V)) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Int) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: IntArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntSet) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: IntList) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int): V? {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        val oldValue = values[index]
+        values[index] = null
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        values.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Int): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableIntObjectMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
index 2db0f7f..282c94d 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/IntSet.kt
@@ -29,13 +29,21 @@
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
 
-// This is a copy of ScatterSet, but with Int elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
 
 // Default empty set to avoid allocations
 private val EmptyIntSet = MutableIntSet(0)
 
 // An empty array of ints
-private val EmptyIntArray = IntArray(0)
+internal val EmptyIntArray = IntArray(0)
 
 /**
  * Returns an empty, read-only [IntSet].
@@ -770,7 +778,7 @@
  * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
  * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
  */
-private inline fun hash(k: Int): Int {
+internal inline fun hash(k: Int): Int {
     val hash = k.hashCode()
     return hash xor (hash ushr 16)
 }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
new file mode 100644
index 0000000..797b7fb
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongFloatMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongFloatMap = MutableLongFloatMap(0)
+
+/**
+ * Returns an empty, read-only [LongFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyLongFloatMap(): LongFloatMap = EmptyLongFloatMap
+
+/**
+ * Returns a new [MutableLongFloatMap].
+ */
+public fun longFloatMapOf(): LongFloatMap = EmptyLongFloatMap
+
+/**
+ * Returns a new [LongFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun longFloatMapOf(vararg pairs: Pair<Long, Float>): LongFloatMap =
+    MutableLongFloatMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableLongFloatMap].
+ */
+public fun mutableLongFloatMapOf(): MutableLongFloatMap = MutableLongFloatMap()
+
+/**
+ * Returns a new [MutableLongFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Float] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableLongFloatMapOf(vararg pairs: Pair<Long, Float>): MutableLongFloatMap =
+    MutableLongFloatMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [LongFloatMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongFloatMap].
+ *
+ * @see [MutableLongFloatMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongFloatMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: LongArray = EmptyLongArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: FloatArray = EmptyFloatArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Long): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Long, defaultValue: Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Long, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Long, value: Float) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Long) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Float) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Long, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Long, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Long, Float) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Float): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [LongFloatMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is LongFloatMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableLongFloatMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Float] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongFloatMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : LongFloatMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = LongArray(newCapacity)
+        values = FloatArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Long, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Long, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Long, value: Float) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Long] key and [Float] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Long, Float>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: LongFloatMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Long] key and [Float] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Long, Float>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Long] key and [Float] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Long, Float>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: LongFloatMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Long) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Long, value: Float): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Long, Float) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Long) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableLongFloatMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
new file mode 100644
index 0000000..c9c5ef6
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongIntMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongIntMap = MutableLongIntMap(0)
+
+/**
+ * Returns an empty, read-only [LongIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyLongIntMap(): LongIntMap = EmptyLongIntMap
+
+/**
+ * Returns a new [MutableLongIntMap].
+ */
+public fun longIntMapOf(): LongIntMap = EmptyLongIntMap
+
+/**
+ * Returns a new [LongIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun longIntMapOf(vararg pairs: Pair<Long, Int>): LongIntMap =
+    MutableLongIntMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableLongIntMap].
+ */
+public fun mutableLongIntMapOf(): MutableLongIntMap = MutableLongIntMap()
+
+/**
+ * Returns a new [MutableLongIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Int] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableLongIntMapOf(vararg pairs: Pair<Long, Int>): MutableLongIntMap =
+    MutableLongIntMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [LongIntMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongIntMap].
+ *
+ * @see [MutableLongIntMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongIntMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: LongArray = EmptyLongArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: IntArray = EmptyIntArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Long): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Long, defaultValue: Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Long, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Long, value: Int) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Long) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Int) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Long, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Long, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Long, Int) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Int): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [LongIntMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is LongIntMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableLongIntMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Int] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongIntMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : LongIntMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = LongArray(newCapacity)
+        values = IntArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Long, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Long, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Long, value: Int) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Long] key and [Int] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Long, Int>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: LongIntMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Long] key and [Int] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Long, Int>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Long] key and [Int] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Long, Int>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: LongIntMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Long) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Long, value: Int): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Long, Int) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Long) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableLongIntMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
index 94dfd82..4f40b5b 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongList.kt
@@ -22,6 +22,14 @@
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 /**
  * [LongList] is a [List]-like collection for [Long] values. It allows retrieving
  * the elements without boxing. [LongList] is always backed by a [MutableLongList],
@@ -835,10 +843,6 @@
     }
 }
 
-// Empty array used when nothing is allocated
-@Suppress("PrivatePropertyName")
-private val EmptyLongArray = LongArray(0)
-
 private val EmptyLongList: LongList = MutableLongList(0)
 
 /**
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
new file mode 100644
index 0000000..e61a7a0
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongLongMap.kt
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongLongMap = MutableLongLongMap(0)
+
+/**
+ * Returns an empty, read-only [LongLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyLongLongMap(): LongLongMap = EmptyLongLongMap
+
+/**
+ * Returns a new [MutableLongLongMap].
+ */
+public fun longLongMapOf(): LongLongMap = EmptyLongLongMap
+
+/**
+ * Returns a new [LongLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun longLongMapOf(vararg pairs: Pair<Long, Long>): LongLongMap =
+    MutableLongLongMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableLongLongMap].
+ */
+public fun mutableLongLongMapOf(): MutableLongLongMap = MutableLongLongMap()
+
+/**
+ * Returns a new [MutableLongLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [Long] key and [Long] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutableLongLongMapOf(vararg pairs: Pair<Long, Long>): MutableLongLongMap =
+    MutableLongLongMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [LongLongMap] is a container with a [Map]-like interface for
+ * [Long] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongLongMap].
+ *
+ * @see [MutableLongLongMap]
+ * @see [ScatterMap]
+ */
+public sealed class LongLongMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: LongArray = EmptyLongArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: LongArray = EmptyLongArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: Long): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Long, defaultValue: Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Long, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Long, value: Long) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Long) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Long) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Long, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Long, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Long, Long) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Long): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [LongLongMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is LongLongMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableLongLongMap] is a container with a [MutableMap]-like interface for
+ * [Long] primitive keys and [Long] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableLongLongMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : LongLongMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = LongArray(newCapacity)
+        values = LongArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Long, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Long, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Long, value: Long) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Long] key and [Long] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<Long, Long>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: LongLongMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [Long] key and [Long] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Long, Long>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [Long] key and [Long] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<Long, Long>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: LongLongMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: Long) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Long, value: Long): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Long, Long) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Long) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongSet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableLongLongMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
new file mode 100644
index 0000000..f01c754
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongObjectMap.kt
@@ -0,0 +1,847 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyLongObjectMap = MutableLongObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [LongObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyLongObjectMap(): LongObjectMap<V> = EmptyLongObjectMap as LongObjectMap<V>
+
+/**
+ * Returns an empty, read-only [LongObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> longObjectMapOf(): LongObjectMap<V> = EmptyLongObjectMap as LongObjectMap<V>
+
+/**
+ * Returns a new [LongObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> longObjectMapOf(vararg pairs: Pair<Long, V>): LongObjectMap<V> =
+    MutableLongObjectMap<V>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableLongObjectMap].
+ */
+public fun <V> mutableLongObjectMapOf(): MutableLongObjectMap<V> = MutableLongObjectMap()
+
+/**
+ * Returns a new [MutableLongObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> mutableLongObjectMapOf(vararg pairs: Pair<Long, V>): MutableLongObjectMap<V> =
+    MutableLongObjectMap<V>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [LongObjectMap] is a container with a [Map]-like interface for keys with
+ * [Long] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableLongObjectMap].
+ *
+ * @see [MutableLongObjectMap]
+ */
+public sealed class LongObjectMap<V> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: LongArray = EmptyLongArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: Array<Any?> = EMPTY_OBJECTS
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     */
+    public operator fun get(key: Long): V? {
+        val index = findKeyIndex(key)
+        @Suppress("UNCHECKED_CAST")
+        return if (index >= 0) values[index] as V? else null
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: Long, defaultValue: V): V {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            @Suppress("UNCHECKED_CAST")
+            return values[index] as V
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: Long, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: Long, value: V) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index], v[index] as V)
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: Long) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: V) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(v[index] as V)
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (Long, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (Long, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (Long, V) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: Long): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: V): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [LongObjectMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is LongObjectMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value == null) {
+                if (other[key] != null || !other.containsKey(key)) {
+                    return false
+                }
+            } else if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(if (value === this) "(this)" else value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    internal inline fun findKeyIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableLongObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [Long] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableLongObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutableLongObjectMap<V>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : LongObjectMap<V>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = LongArray(newCapacity)
+        values = arrayOfNulls(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: Long, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue().also { set(key, it) }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: Long, value: V) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: Long, value: V): V? {
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
+        values[index] = value
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Long] key is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<Long, V>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: LongObjectMap<V>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that the [Pair] is allocated and the [Long] key is boxed.
+     * Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<Long, V>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Long] key is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<out Pair<Long, V>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: LongObjectMap<V>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map. If the
+     * [key] was present in the map, this function returns the value that was
+     * present before removal.
+     */
+    public fun remove(key: Long): V? {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return removeValueAt(index)
+        }
+        return null
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: Long, value: V): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (Long, V) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index], values[index] as V)) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: Long) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: LongArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongSet) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: LongList) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int): V? {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        val oldValue = values[index]
+        values[index] = null
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        values.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: Long): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableLongObjectMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
index f292716..45734c8 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/LongSet.kt
@@ -29,13 +29,21 @@
 import kotlin.contracts.contract
 import kotlin.jvm.JvmField
 
-// This is a copy of ScatterSet, but with Long elements
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
 
 // Default empty set to avoid allocations
 private val EmptyLongSet = MutableLongSet(0)
 
 // An empty array of longs
-private val EmptyLongArray = LongArray(0)
+internal val EmptyLongArray = LongArray(0)
 
 /**
  * Returns an empty, read-only [LongSet].
@@ -770,7 +778,7 @@
  * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
  * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
  */
-private inline fun hash(k: Long): Int {
+internal inline fun hash(k: Long): Int {
     val hash = k.hashCode()
     return hash xor (hash ushr 16)
 }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
new file mode 100644
index 0000000..95c1a3d
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectFloatMap.kt
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectFloatMap = MutableObjectFloatMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectFloatMap(): ObjectFloatMap<K> =
+    EmptyObjectFloatMap as ObjectFloatMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectFloatMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectFloatMap(): ObjectFloatMap<K> =
+    EmptyObjectFloatMap as ObjectFloatMap<K>
+
+/**
+ * Returns a new [MutableObjectFloatMap].
+ */
+public fun <K> mutableObjectFloatMapOf(): MutableObjectFloatMap<K> = MutableObjectFloatMap()
+
+/**
+ * Returns a new [MutableObjectFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> objectFloatMapOf(vararg pairs: Pair<K, Float>): ObjectFloatMap<K> =
+    MutableObjectFloatMap<K>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableObjectFloatMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Float] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> mutableObjectFloatMapOf(vararg pairs: Pair<K, Float>): MutableObjectFloatMap<K> =
+    MutableObjectFloatMap<K>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [ObjectFloatMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Float] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectFloatMap].
+ *
+ * @see [MutableObjectFloatMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectFloatMap<K> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+    @PublishedApi
+    @JvmField
+    internal var values: FloatArray = EmptyFloatArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     * @throws NoSuchElementException when [key] is not found
+     */
+    public operator fun get(key: K): Float {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("There is no key $key in the map")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: K, defaultValue: Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: K, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: K, value: Float) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K, v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: K) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K)
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Float) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (K, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (K, Float) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (K, Float) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Float): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [ObjectFloatMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is ObjectFloatMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val o = other as ObjectFloatMap<Any?>
+
+        forEach { key, value ->
+            if (value != o[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(if (key === this) "(this)" else key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: K): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableObjectFloatMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Float] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectFloatMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectFloatMap<K>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : ObjectFloatMap<K>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = arrayOfNulls(newCapacity)
+        values = FloatArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: K, defaultValue: () -> Float): Float {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        val value = defaultValue()
+        set(key, value)
+        return value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: K, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public fun put(key: K, value: Float) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Float] value is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<K, Float>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: ObjectFloatMap<K>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that the [Pair] is allocated and the [Float] value is boxed.
+     * Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<K, Float>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Float] value is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<out Pair<K, Float>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: ObjectFloatMap<K>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: K) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: K, value: Float): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (K, Float) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index] as K, values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: K) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Iterable<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Sequence<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        keys[index] = null
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        keys.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: K): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableObjectFloatMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
new file mode 100644
index 0000000..697f1b0
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectIntMap.kt
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectIntMap = MutableObjectIntMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectIntMap(): ObjectIntMap<K> =
+    EmptyObjectIntMap as ObjectIntMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectIntMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectIntMap(): ObjectIntMap<K> =
+    EmptyObjectIntMap as ObjectIntMap<K>
+
+/**
+ * Returns a new [MutableObjectIntMap].
+ */
+public fun <K> mutableObjectIntMapOf(): MutableObjectIntMap<K> = MutableObjectIntMap()
+
+/**
+ * Returns a new [MutableObjectIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> objectIntMapOf(vararg pairs: Pair<K, Int>): ObjectIntMap<K> =
+    MutableObjectIntMap<K>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableObjectIntMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Int] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> mutableObjectIntMapOf(vararg pairs: Pair<K, Int>): MutableObjectIntMap<K> =
+    MutableObjectIntMap<K>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [ObjectIntMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Int] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectIntMap].
+ *
+ * @see [MutableObjectIntMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectIntMap<K> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+    @PublishedApi
+    @JvmField
+    internal var values: IntArray = EmptyIntArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     * @throws NoSuchElementException when [key] is not found
+     */
+    public operator fun get(key: K): Int {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("There is no key $key in the map")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: K, defaultValue: Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: K, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: K, value: Int) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K, v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: K) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K)
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Int) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (K, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (K, Int) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (K, Int) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Int): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [ObjectIntMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is ObjectIntMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val o = other as ObjectIntMap<Any?>
+
+        forEach { key, value ->
+            if (value != o[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(if (key === this) "(this)" else key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: K): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableObjectIntMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Int] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectIntMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectIntMap<K>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : ObjectIntMap<K>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = arrayOfNulls(newCapacity)
+        values = IntArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: K, defaultValue: () -> Int): Int {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        val value = defaultValue()
+        set(key, value)
+        return value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: K, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public fun put(key: K, value: Int) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Int] value is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<K, Int>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: ObjectIntMap<K>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that the [Pair] is allocated and the [Int] value is boxed.
+     * Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<K, Int>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Int] value is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<out Pair<K, Int>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: ObjectIntMap<K>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: K) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: K, value: Int): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (K, Int) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index] as K, values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: K) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Iterable<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Sequence<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        keys[index] = null
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        keys.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: K): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableObjectIntMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt
new file mode 100644
index 0000000..105ebaf
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectList.kt
@@ -0,0 +1,1560 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier", "UNCHECKED_CAST")
+@file:OptIn(ExperimentalContracts::class)
+
+package androidx.collection
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+
+/**
+ * [ObjectList] is a [List]-like collection for reference types. It is optimized for fast
+ * access, avoiding virtual and interface method access. Methods avoid allocation whenever
+ * possible. For example [forEach] does not need allocate an [Iterator].
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * **Note** [List] access is available through [asList] when developers need access to the
+ * common API.
+ *
+ * @see MutableObjectList
+ * @see FloatList
+ * @see IntList
+ * @eee LongList
+ */
+public sealed class ObjectList<E>(initialCapacity: Int) {
+    @JvmField
+    @PublishedApi
+    internal var content: Array<Any?> = if (initialCapacity == 0) {
+        EmptyArray
+    } else {
+        arrayOfNulls(initialCapacity)
+    }
+
+    @Suppress("PropertyName")
+    @JvmField
+    @PublishedApi
+    internal var _size: Int = 0
+
+    /**
+     * The number of elements in the [ObjectList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns the last valid index in the [ObjectList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = _size - 1
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [ObjectList].
+     */
+    public inline val indices: IntRange get() = 0 until _size
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public fun none(): Boolean {
+        return isEmpty()
+    }
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public fun any(): Boolean {
+        return isNotEmpty()
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: E) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: E) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEachReversed {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains [element] or `false` otherwise.
+     */
+    public operator fun contains(element: E): Boolean {
+        forEach {
+            if (it == element) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+        for (i in elements.indices) {
+            if (!contains(elements[i])) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: List<E>): Boolean {
+        for (i in elements.indices) {
+            if (!contains(elements[i])) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: Iterable<E>): Boolean {
+        elements.forEach { element ->
+            if (!contains(element)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: ObjectList<E>): Boolean {
+        elements.forEach { element ->
+            if (!contains(element)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public fun count(): Int = _size
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: E) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        var count = 0
+        forEach { if (predicate(it)) count++ }
+        return count
+    }
+
+    /**
+     * Returns the first element in the [ObjectList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun first(): E {
+        if (isEmpty()) {
+            throw NoSuchElementException("ObjectList is empty.")
+        }
+        return content[0] as E
+    }
+
+    /**
+     * Returns the first element in the [ObjectList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     * @see firstOrNull
+     */
+    public inline fun first(predicate: (element: E) -> Boolean): E {
+        contract { callsInPlace(predicate) }
+        forEach { element ->
+            if (predicate(element)) return element
+        }
+        throw NoSuchElementException("ObjectList contains no element matching the predicate.")
+    }
+
+    /**
+     * Returns the first element in the [ObjectList] or `null` if it [isEmpty].
+     */
+    public inline fun firstOrNull(): E? = if (isEmpty()) null else get(0)
+
+    /**
+     * Returns the first element in the [ObjectList] for which [predicate] returns `true` or
+     * `null` if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun firstOrNull(predicate: (element: E) -> Boolean): E? {
+        contract { callsInPlace(predicate) }
+        forEach { element ->
+            if (predicate(element)) return element
+        }
+        return null
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [ObjectList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: E) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEach { element ->
+            acc = operation(acc, element)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [ObjectList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: E) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachIndexed { i, element ->
+            acc = operation(i, acc, element)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [ObjectList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: E, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversed { element ->
+            acc = operation(element, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [ObjectList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: E, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversedIndexed { i, element ->
+            acc = operation(i, element, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Calls [block] for each element in the [ObjectList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: E) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(content[i] as E)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [ObjectList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: E) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(i, content[i] as E)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [ObjectList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: E) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(content[i] as E)
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [ObjectList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: E) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(i, content[i] as E)
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): E {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index] as E
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): E {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index] as E
+    }
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> E
+    ): E {
+        if (index !in 0 until _size) {
+            return defaultValue(index)
+        }
+        return content[index] as E
+    }
+
+    /**
+     * Returns the index of [element] in the [ObjectList] or `-1` if [element] is not there.
+     */
+    public fun indexOf(element: E): Int {
+        forEachIndexed { i, item ->
+            if (element == item) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the first element in the [ObjectList] for which [predicate]
+     * returns `true` or -1 if there was no element for which predicate returned `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: E) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachIndexed { i, element ->
+            if (predicate(element)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the last element in the [ObjectList] for which [predicate]
+     * returns `true` or -1 if there was no element for which predicate returned `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: E) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachReversedIndexed { i, element ->
+            if (predicate(element)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns `true` if the [ObjectList] has no elements in it or `false` otherwise.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if there are elements in the [ObjectList] or `false` if it is empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the last element in the [ObjectList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun last(): E {
+        if (isEmpty()) {
+            throw NoSuchElementException("ObjectList is empty.")
+        }
+        return content[lastIndex] as E
+    }
+
+    /**
+     * Returns the last element in the [ObjectList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     * @see lastOrNull
+     */
+    public inline fun last(predicate: (element: E) -> Boolean): E {
+        contract { callsInPlace(predicate) }
+        forEachReversed { element ->
+            if (predicate(element)) {
+                return element
+            }
+        }
+        throw NoSuchElementException("ObjectList contains no element matching the predicate.")
+    }
+
+    /**
+     * Returns the last element in the [ObjectList] or `null` if it [isEmpty].
+     */
+    public inline fun lastOrNull(): E? = if (isEmpty()) null else content[lastIndex] as E
+
+    /**
+     * Returns the last element in the [ObjectList] for which [predicate] returns `true` or
+     * `null` if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun lastOrNull(predicate: (element: E) -> Boolean): E? {
+        contract { callsInPlace(predicate) }
+        forEachReversed { element ->
+            if (predicate(element)) {
+                return element
+            }
+        }
+        return null
+    }
+
+    /**
+     * Returns the index of the last element in the [ObjectList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public fun lastIndexOf(element: E): Int {
+        forEachReversedIndexed { i, item ->
+            if (element == item) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns a [List] view into the [ObjectList]. All access to the collection will be
+     * less efficient and abides by the allocation requirements of the [List]. For example,
+     * [List.forEach] will allocate an iterator. All access will go through the more expensive
+     * interface calls. Critical performance areas should use the [ObjectList] API rather than
+     * [List] API, when possible.
+     */
+    public abstract fun asList(): List<E>
+
+    /**
+     * Returns a hash code based on the contents of the [ObjectList].
+     */
+    override fun hashCode(): Int {
+        var hashCode = 0
+        forEach { element ->
+            hashCode += 31 * element.hashCode()
+        }
+        return hashCode
+    }
+
+    /**
+     * Returns `true` if [other] is a [ObjectList] and the contents of this and [other] are the
+     * same.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (other !is ObjectList<*> || other._size != _size) {
+            return false
+        }
+        val content = content
+        val otherContent = other.content
+        for (i in indices) {
+            if (content[i] != otherContent[i]) {
+                return false
+            }
+        }
+        return true
+    }
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String {
+        if (isEmpty()) {
+            return "[]"
+        }
+        val last = lastIndex
+        return buildString {
+            append('[')
+            val content = content
+            for (i in 0 until last) {
+                append(content[i])
+                append(',')
+                append(' ')
+            }
+            append(content[last])
+            append(']')
+        }
+    }
+}
+
+/**
+ * [MutableObjectList] is a [MutableList]-like collection for reference types. It is optimized
+ * for fast access, avoiding virtual and interface method access. Methods avoid allocation
+ * whenever possible. For example [forEach] does not need allocate an [Iterator].
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * **Note** [List] access is available through [asList] when developers need access to the
+ * common API.
+
+ * **Note** [MutableList] access is available through [asMutableList] when developers need
+ * access to the common API.
+ *
+ * @see ObjectList
+ * @see MutableFloatList
+ * @see MutableIntList
+ * @eee MutableLongList
+ */
+public class MutableObjectList<E>(
+    initialCapacity: Int = 16
+) : ObjectList<E>(initialCapacity) {
+    private var list: ObjectListMutableList<E>? = null
+
+    /**
+     * Returns the total number of elements that can be held before the [MutableObjectList] must
+     * grow.
+     *
+     * @see ensureCapacity
+     */
+    public inline val capacity: Int
+        get() = content.size
+
+    /**
+     * Adds [element] to the [MutableObjectList] and returns `true`.
+     */
+    public fun add(element: E): Boolean {
+        ensureCapacity(_size + 1)
+        content[_size] = element
+        _size++
+        return true
+    }
+
+    /**
+     * Adds [element] to the [MutableObjectList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: E) {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        ensureCapacity(_size + 1)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + 1,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        content[index] = element
+        _size++
+    }
+
+    /**
+     * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        @Suppress("ArrayReturn") elements: Array<E>
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements.size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements.size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.copyInto(content, index)
+        _size += elements.size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: Collection<E>
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements.size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements.size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.forEachIndexed { i, element ->
+            content[index + i] = element
+        }
+        _size += elements.size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the [MutableObjectList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutableObjectList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: ObjectList<E>
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements._size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements._size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.content.copyInto(
+            destination = content,
+            destinationOffset = index,
+            startIndex = 0,
+            endIndex = elements._size
+        )
+        _size += elements._size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: ObjectList<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: ScatterSet<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: List<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: Iterable<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList] and returns `true` if the
+     * [MutableObjectList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: Sequence<E>): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: ObjectList<E>) {
+        if (elements.isEmpty()) return
+        ensureCapacity(_size + elements._size)
+        val content = content
+        elements.content.copyInto(
+            destination = content,
+            destinationOffset = _size,
+            startIndex = 0,
+            endIndex = elements._size
+        )
+        _size += elements._size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: ScatterSet<E>) {
+        if (elements.isEmpty()) return
+        ensureCapacity(_size + elements.size)
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(@Suppress("ArrayReturn") elements: Array<E>) {
+        if (elements.isEmpty()) return
+        ensureCapacity(_size + elements.size)
+        val content = content
+        elements.copyInto(content, _size)
+        _size += elements.size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: List<E>) {
+        if (elements.isEmpty()) return
+        val size = _size
+        ensureCapacity(size + elements.size)
+        val content = content
+        for (i in elements.indices) {
+            content[i + size] = elements[i]
+        }
+        _size += elements.size
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: Iterable<E>) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutableObjectList].
+     */
+    public operator fun plusAssign(elements: Sequence<E>) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all elements in the [MutableObjectList]. The storage isn't released.
+     * @see trim
+     */
+    public fun clear() {
+        content.fill(null, fromIndex = 0, toIndex = _size)
+        _size = 0
+    }
+
+    /**
+     * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+     * internal storage is reduced to the maximum of [size] and [minCapacity].
+     * @see ensureCapacity
+     */
+    public fun trim(minCapacity: Int = _size) {
+        val minSize = maxOf(minCapacity, _size)
+        if (capacity > minSize) {
+            content = content.copyOf(minSize)
+        }
+    }
+
+    /**
+     * Ensures that there is enough space to store [capacity] elements in the [MutableObjectList].
+     * @see trim
+     */
+    public fun ensureCapacity(capacity: Int) {
+        val oldContent = content
+        if (oldContent.size < capacity) {
+            val newSize = maxOf(capacity, oldContent.size * 3 / 2)
+            content = oldContent.copyOf(newSize)
+        }
+    }
+
+    /**
+     * [add] [element] to the [MutableObjectList].
+     */
+    public inline operator fun plusAssign(element: E) {
+        add(element)
+    }
+
+    /**
+     * [remove] [element] from the [MutableObjectList]
+     */
+    public inline operator fun minusAssign(element: E) {
+        remove(element)
+    }
+
+    /**
+     * Removes [element] from the [MutableObjectList]. If [element] was in the [MutableObjectList]
+     * and was removed, `true` will be returned, or `false` will be returned if the element
+     * was not found.
+     */
+    public fun remove(element: E): Boolean {
+        val index = indexOf(element)
+        if (index >= 0) {
+            removeAt(index)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Removes all elements in this list for which [predicate] returns `true`.
+     */
+    public inline fun removeIf(predicate: (element: E) -> Boolean) {
+        var gap = 0
+        val size = _size
+        val content = content
+        for (i in indices) {
+            content[i - gap] = content[i]
+            if (predicate(content[i] as E)) {
+                gap++
+            }
+        }
+        content.fill(null, fromIndex = size - gap, toIndex = size)
+        _size -= gap
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+        val initialSize = _size
+        for (i in elements.indices) {
+            remove(elements[i])
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: ObjectList<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: ScatterSet<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: List<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: Iterable<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: Sequence<E>): Boolean {
+        val initialSize = _size
+        minusAssign(elements)
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(@Suppress("ArrayReturn") elements: Array<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: ObjectList<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: ScatterSet<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: List<E>) {
+        for (i in elements.indices) {
+            minusAssign(elements[i])
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: Iterable<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutableObjectList].
+     */
+    public operator fun minusAssign(elements: Sequence<E>) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes the element at the given [index] and returns it.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): E {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        val content = content
+        val element = content[index]
+        if (index != lastIndex) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index,
+                startIndex = index + 1,
+                endIndex = _size
+            )
+        }
+        _size--
+        content[_size] = null
+        return element as E
+    }
+
+    /**
+     * Removes elements from index [start] (inclusive) to [end] (exclusive).
+     * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+     * @throws IllegalArgumentException if [start] is greater than [end]
+     */
+    public fun removeRange(
+        @androidx.annotation.IntRange(from = 0) start: Int,
+        @androidx.annotation.IntRange(from = 0) end: Int
+    ) {
+        if (start !in 0.._size || end !in 0.._size) {
+            throw IndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+        }
+        if (end < start) {
+            throw IllegalArgumentException("Start ($start) is more than end ($end)")
+        }
+        if (end != start) {
+            if (end < _size) {
+                content.copyInto(
+                    destination = content,
+                    destinationOffset = start,
+                    startIndex = end,
+                    endIndex = _size
+                )
+            }
+            val newSize = _size - (end - start)
+            content.fill(null, fromIndex = newSize, toIndex = _size)
+            _size = newSize
+        }
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(@Suppress("ArrayReturn") elements: Array<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i]
+            if (elements.indexOfFirst { it == element } < 0) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(elements: ObjectList<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i] as E
+            if (element !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(elements: Collection<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i] as E
+            if (element !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(elements: Iterable<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i] as E
+            if (element !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutableObjectList] and removes all other values.
+     * @return `true` if the [MutableObjectList] has changed.
+     */
+    public fun retainAll(elements: Sequence<E>): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val element = content[i] as E
+            if (element !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Sets the value at [index] to [element].
+     * @return the previous value set at [index]
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public operator fun set(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: E
+    ): E {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+        }
+        val content = content
+        val old = content[index]
+        content[index] = element
+        return old as E
+    }
+
+    override fun asList(): List<E> = asMutableList()
+
+    /**
+     * Returns a [MutableList] view into the [MutableObjectList]. All access to the collection
+     * will be less efficient and abides by the allocation requirements of the
+     * [MutableList]. For example, [MutableList.forEach] will allocate an iterator.
+     * All access will go through the more expensive interface calls. Critical performance
+     * areas should use the [MutableObjectList] API rather than [MutableList] API, when possible.
+     */
+    public fun asMutableList(): MutableList<E> = list ?: ObjectListMutableList(this).also {
+        list = it
+    }
+
+    private class MutableObjectListIterator<T>(
+        private val list: MutableList<T>,
+        private var index: Int
+    ) : MutableListIterator<T> {
+        override fun hasNext(): Boolean {
+            return index < list.size
+        }
+
+        override fun next(): T {
+            return list[index++]
+        }
+
+        override fun remove() {
+            index--
+            list.removeAt(index)
+        }
+
+        override fun hasPrevious(): Boolean {
+            return index > 0
+        }
+
+        override fun nextIndex(): Int {
+            return index
+        }
+
+        override fun previous(): T {
+            index--
+            return list[index]
+        }
+
+        override fun previousIndex(): Int {
+            return index - 1
+        }
+
+        override fun add(element: T) {
+            list.add(index, element)
+            index++
+        }
+
+        override fun set(element: T) {
+            list[index] = element
+        }
+    }
+
+    /**
+     * [MutableList] implementation for a [MutableObjectList], used in [asMutableList].
+     */
+    private class ObjectListMutableList<T>(
+        private val objectList: MutableObjectList<T>
+    ) : MutableList<T> {
+        override val size: Int
+            get() = objectList.size
+
+        override fun contains(element: T): Boolean = objectList.contains(element)
+
+        override fun containsAll(elements: Collection<T>): Boolean =
+            objectList.containsAll(elements)
+
+        override fun get(index: Int): T {
+            checkIndex(index)
+            return objectList[index]
+        }
+
+        override fun indexOf(element: T): Int = objectList.indexOf(element)
+
+        override fun isEmpty(): Boolean = objectList.isEmpty()
+
+        override fun iterator(): MutableIterator<T> = MutableObjectListIterator(this, 0)
+
+        override fun lastIndexOf(element: T): Int = objectList.lastIndexOf(element)
+
+        override fun add(element: T): Boolean = objectList.add(element)
+
+        override fun add(index: Int, element: T) = objectList.add(index, element)
+
+        override fun addAll(index: Int, elements: Collection<T>): Boolean =
+            objectList.addAll(index, elements)
+
+        override fun addAll(elements: Collection<T>): Boolean = objectList.addAll(elements)
+
+        override fun clear() = objectList.clear()
+
+        override fun listIterator(): MutableListIterator<T> = MutableObjectListIterator(this, 0)
+
+        override fun listIterator(index: Int): MutableListIterator<T> =
+            MutableObjectListIterator(this, index)
+
+        override fun remove(element: T): Boolean = objectList.remove(element)
+
+        override fun removeAll(elements: Collection<T>): Boolean = objectList.removeAll(elements)
+
+        override fun removeAt(index: Int): T {
+            checkIndex(index)
+            return objectList.removeAt(index)
+        }
+
+        override fun retainAll(elements: Collection<T>): Boolean = objectList.retainAll(elements)
+
+        override fun set(index: Int, element: T): T {
+            checkIndex(index)
+            return objectList.set(index, element)
+        }
+
+        override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
+            checkSubIndex(fromIndex, toIndex)
+            return SubList(this, fromIndex, toIndex)
+        }
+    }
+
+    /**
+     * A view into an underlying [MutableList] that directly accesses the underlying [MutableList].
+     * This is important for the implementation of [List.subList]. A change to the [SubList]
+     * also changes the referenced [MutableList].
+     */
+    private class SubList<T>(
+        private val list: MutableList<T>,
+        private val start: Int,
+        private var end: Int
+    ) : MutableList<T> {
+        override val size: Int
+            get() = end - start
+
+        override fun contains(element: T): Boolean {
+            for (i in start until end) {
+                if (list[i] == element) {
+                    return true
+                }
+            }
+            return false
+        }
+
+        override fun containsAll(elements: Collection<T>): Boolean {
+            elements.forEach {
+                if (!contains(it)) {
+                    return false
+                }
+            }
+            return true
+        }
+
+        override fun get(index: Int): T {
+            checkIndex(index)
+            return list[index + start]
+        }
+
+        override fun indexOf(element: T): Int {
+            for (i in start until end) {
+                if (list[i] == element) {
+                    return i - start
+                }
+            }
+            return -1
+        }
+
+        override fun isEmpty(): Boolean = end == start
+
+        override fun iterator(): MutableIterator<T> = MutableObjectListIterator(this, 0)
+
+        override fun lastIndexOf(element: T): Int {
+            for (i in end - 1 downTo start) {
+                if (list[i] == element) {
+                    return i - start
+                }
+            }
+            return -1
+        }
+
+        override fun add(element: T): Boolean {
+            list.add(end++, element)
+            return true
+        }
+
+        override fun add(index: Int, element: T) {
+            list.add(index + start, element)
+            end++
+        }
+
+        override fun addAll(index: Int, elements: Collection<T>): Boolean {
+            list.addAll(index + start, elements)
+            end += elements.size
+            return elements.size > 0
+        }
+
+        override fun addAll(elements: Collection<T>): Boolean {
+            list.addAll(end, elements)
+            end += elements.size
+            return elements.size > 0
+        }
+
+        override fun clear() {
+            for (i in end - 1 downTo start) {
+                list.removeAt(i)
+            }
+            end = start
+        }
+
+        override fun listIterator(): MutableListIterator<T> = MutableObjectListIterator(this, 0)
+
+        override fun listIterator(index: Int): MutableListIterator<T> =
+            MutableObjectListIterator(this, index)
+
+        override fun remove(element: T): Boolean {
+            for (i in start until end) {
+                if (list[i] == element) {
+                    list.removeAt(i)
+                    end--
+                    return true
+                }
+            }
+            return false
+        }
+
+        override fun removeAll(elements: Collection<T>): Boolean {
+            val originalEnd = end
+            elements.forEach {
+                remove(it)
+            }
+            return originalEnd != end
+        }
+
+        override fun removeAt(index: Int): T {
+            checkIndex(index)
+            val element = list.removeAt(index + start)
+            end--
+            return element
+        }
+
+        override fun retainAll(elements: Collection<T>): Boolean {
+            val originalEnd = end
+            for (i in end - 1 downTo start) {
+                val element = list[i]
+                if (element !in elements) {
+                    list.removeAt(i)
+                    end--
+                }
+            }
+            return originalEnd != end
+        }
+
+        override fun set(index: Int, element: T): T {
+            checkIndex(index)
+            return list.set(index + start, element)
+        }
+
+        override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> {
+            checkSubIndex(fromIndex, toIndex)
+            return SubList(this, fromIndex, toIndex)
+        }
+    }
+}
+
+private fun List<*>.checkIndex(index: Int) {
+    val size = size
+    if (index < 0 || index >= size) {
+        throw IndexOutOfBoundsException("Index $index is out of bounds. " +
+            "The list has $size elements.")
+    }
+}
+
+private fun List<*>.checkSubIndex(fromIndex: Int, toIndex: Int) {
+    val size = size
+    if (fromIndex > toIndex) {
+        throw IllegalArgumentException("Indices are out of order. fromIndex ($fromIndex) is " +
+            "greater than toIndex ($toIndex).")
+    }
+    if (fromIndex < 0) {
+        throw IndexOutOfBoundsException("fromIndex ($fromIndex) is less than 0.")
+    }
+    if (toIndex > size) {
+        throw IndexOutOfBoundsException(
+            "toIndex ($toIndex) is more than than the list size ($size)"
+        )
+    }
+}
+
+// Empty array used when nothing is allocated
+private val EmptyArray = arrayOfNulls<Any>(0)
+
+private val EmptyObjectList: ObjectList<Any?> = MutableObjectList(0)
+
+/**
+ * @return a read-only [ObjectList] with nothing in it.
+ */
+public fun <E> emptyObjectList(): ObjectList<E> = EmptyObjectList as ObjectList<E>
+
+/**
+ * @return a read-only [ObjectList] with nothing in it.
+ */
+public fun <E> objectListOf(): ObjectList<E> = EmptyObjectList as ObjectList<E>
+
+/**
+ * @return a new read-only [ObjectList] with [element1] as the only element in the list.
+ */
+public fun <E> objectListOf(element1: E): ObjectList<E> = mutableObjectListOf(element1)
+
+/**
+ * @return a new read-only [ObjectList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun <E> objectListOf(element1: E, element2: E): ObjectList<E> =
+    mutableObjectListOf(element1, element2)
+
+/**
+ * @return a new read-only [ObjectList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun <E> objectListOf(element1: E, element2: E, element3: E): ObjectList<E> =
+    mutableObjectListOf(element1, element2, element3)
+
+/**
+ * @return a new read-only [ObjectList] with [elements] in order.
+ */
+public fun <E> objectListOf(vararg elements: E): ObjectList<E> =
+    MutableObjectList<E>(elements.size).apply { plusAssign(elements as Array<E>) }
+
+/**
+ * @return a new empty [MutableObjectList] with the default capacity.
+ */
+public inline fun <E> mutableObjectListOf(): MutableObjectList<E> = MutableObjectList()
+
+/**
+ * @return a new [MutableObjectList] with [element1] as the only element in the list.
+ */
+public fun <E> mutableObjectListOf(element1: E): MutableObjectList<E> {
+    val list = MutableObjectList<E>(1)
+    list += element1
+    return list
+}
+
+/**
+ * @return a new [MutableObjectList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun <E> mutableObjectListOf(element1: E, element2: E): MutableObjectList<E> {
+    val list = MutableObjectList<E>(2)
+    list += element1
+    list += element2
+    return list
+}
+
+/**
+ * @return a new [MutableObjectList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun <E> mutableObjectListOf(element1: E, element2: E, element3: E): MutableObjectList<E> {
+    val list = MutableObjectList<E>(3)
+    list += element1
+    list += element2
+    list += element3
+    return list
+}
+
+/**
+ * @return a new [MutableObjectList] with the given elements, in order.
+ */
+public inline fun <E> mutableObjectListOf(vararg elements: E): MutableObjectList<E> =
+    MutableObjectList<E>(elements.size).apply { plusAssign(elements as Array<E>) }
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
new file mode 100644
index 0000000..092855b
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ObjectLongMap.kt
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectLongMap = MutableObjectLongMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectLongMap(): ObjectLongMap<K> =
+    EmptyObjectLongMap as ObjectLongMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectLongMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectLongMap(): ObjectLongMap<K> =
+    EmptyObjectLongMap as ObjectLongMap<K>
+
+/**
+ * Returns a new [MutableObjectLongMap].
+ */
+public fun <K> mutableObjectLongMapOf(): MutableObjectLongMap<K> = MutableObjectLongMap()
+
+/**
+ * Returns a new [MutableObjectLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> objectLongMapOf(vararg pairs: Pair<K, Long>): ObjectLongMap<K> =
+    MutableObjectLongMap<K>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableObjectLongMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [Long] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> mutableObjectLongMapOf(vararg pairs: Pair<K, Long>): MutableObjectLongMap<K> =
+    MutableObjectLongMap<K>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [ObjectLongMap] is a container with a [Map]-like interface for keys with
+ * reference types and [Long] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectLongMap].
+ *
+ * @see [MutableObjectLongMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectLongMap<K> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+    @PublishedApi
+    @JvmField
+    internal var values: LongArray = EmptyLongArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     * @throws NoSuchElementException when [key] is not found
+     */
+    public operator fun get(key: K): Long {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("There is no key $key in the map")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: K, defaultValue: Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: K, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: K, value: Long) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K, v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: K) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K)
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: Long) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (K, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (K, Long) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (K, Long) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: Long): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [ObjectLongMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is ObjectLongMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val o = other as ObjectLongMap<Any?>
+
+        forEach { key, value ->
+            if (value != o[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(if (key === this) "(this)" else key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: K): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableObjectLongMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [Long] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectLongMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectLongMap<K>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : ObjectLongMap<K>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = arrayOfNulls(newCapacity)
+        values = LongArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: K, defaultValue: () -> Long): Long {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        val value = defaultValue()
+        set(key, value)
+        return value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: K, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public fun put(key: K, value: Long) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Long] value is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<K, Long>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: ObjectLongMap<K>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that the [Pair] is allocated and the [Long] value is boxed.
+     * Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<K, Long>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [Long] value is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<out Pair<K, Long>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: ObjectLongMap<K>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: K) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: K, value: Long): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (K, Long) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index] as K, values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: K) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Iterable<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Sequence<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        keys[index] = null
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        keys.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: K): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableObjectLongMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
new file mode 100644
index 0000000..fa56a25
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatFloatMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatFloatMapTest {
+    @Test
+    fun floatFloatMap() {
+        val map = MutableFloatFloatMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyFloatFloatMap() {
+        val map = emptyFloatFloatMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyFloatFloatMap(), map)
+    }
+
+    @Test
+    fun floatFloatMapFunction() {
+        val map = mutableFloatFloatMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableFloatFloatMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatFloatMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableFloatFloatMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatFloatMapPairsFunction() {
+        val map = mutableFloatFloatMapOf(
+            1f to 1f,
+            2f to 2f
+        )
+        assertEquals(2, map.size)
+        assertEquals(1f, map[1f])
+        assertEquals(2f, map[2f])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableFloatFloatMap(12)
+        map[1f] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableFloatFloatMap(2)
+        map[1f] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableFloatFloatMap(0)
+        map[1f] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[1f] = 2f
+
+        assertEquals(1, map.size)
+        assertEquals(2f, map[1f])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableFloatFloatMap()
+
+        map.put(1f, 1f)
+        assertEquals(1f, map[1f])
+        map.put(1f, 2f)
+        assertEquals(2f, map[1f])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+
+        map.putAll(arrayOf(3f to 3f, 7f to 7f))
+
+        assertEquals(4, map.size)
+        assertEquals(3f, map[3f])
+        assertEquals(7f, map[7f])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableFloatFloatMap()
+        map += 1f to 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1f])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableFloatFloatMap()
+        map += arrayOf(3f to 3f, 7f to 7f)
+
+        assertEquals(2, map.size)
+        assertEquals(3f, map[3f])
+        assertEquals(7f, map[7f])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertFailsWith<NoSuchElementException> {
+            map[2f]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertEquals(2f, map.getOrDefault(2f, 2f))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertEquals(3f, map.getOrElse(3f) { 3f })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        var counter = 0
+        map.getOrPut(1f) {
+            counter++
+            2f
+        }
+        assertEquals(1f, map[1f])
+        assertEquals(0, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            2f
+        }
+        assertEquals(2f, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            3f
+        }
+        assertEquals(2f, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            3f
+        }
+        assertEquals(3f, map[3f])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableFloatFloatMap()
+        map.remove(1f)
+
+        map[1f] = 1f
+        map.remove(1f)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableFloatFloatMap(6)
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1f)
+        map.remove(2f)
+        map.remove(3f)
+        map.remove(4f)
+        map.remove(5f)
+        map.remove(6f)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7f] = 7f
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        map.removeIf { key, _ -> key == 1f || key == 3f }
+
+        assertEquals(4, map.size)
+        assertEquals(2f, map[2f])
+        assertEquals(4f, map[4f])
+        assertEquals(5f, map[5f])
+        assertEquals(6f, map[6f])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+
+        map -= 1f
+
+        assertEquals(2, map.size)
+        assertFalse(1f in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+
+        map -= floatArrayOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+
+        map -= floatSetOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+
+        map -= floatListOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableFloatFloatMap()
+        assertFalse(map.remove(1f, 1f))
+
+        map[1f] = 1f
+        assertTrue(map.remove(1f, 1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableFloatFloatMap()
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toFloat()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableFloatFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toFloat())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableFloatFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toFloat()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableFloatFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toFloat()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableFloatFloatMap()
+
+        for (i in 0 until 32) {
+            map[i.toFloat()] = i.toFloat()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableFloatFloatMap()
+        assertEquals("{}", map.toString())
+
+        map[1f] = 1f
+        map[2f] = 2f
+        val oneValueString = 1f.toString()
+        val twoValueString = 2f.toString()
+        val oneKeyString = 1f.toString()
+        val twoKeyString = 2f.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableFloatFloatMap()
+        assertNotEquals(map, map2)
+
+        map2[1f] = 1f
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertTrue(map.containsKey(1f))
+        assertFalse(map.containsKey(2f))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertTrue(1f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+
+        assertTrue(map.containsValue(1f))
+        assertFalse(map.containsValue(3f))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableFloatFloatMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1f] = 1f
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableFloatFloatMap()
+        assertEquals(0, map.count())
+
+        map[1f] = 1f
+        assertEquals(1, map.count())
+
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        assertEquals(2, map.count { key, _ -> key <= 2f })
+        assertEquals(0, map.count { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        assertTrue(map.any { key, _ -> key == 4f })
+        assertFalse(map.any { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableFloatFloatMap()
+        map[1f] = 1f
+        map[2f] = 2f
+        map[3f] = 3f
+        map[4f] = 4f
+        map[5f] = 5f
+        map[6f] = 6f
+
+        assertTrue(map.all { key, value -> key > 0f && value >= 1f })
+        assertFalse(map.all { key, _ -> key < 6f })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableFloatFloatMap()
+        assertEquals(7, map.trim())
+
+        map[1f] = 1f
+        map[3f] = 3f
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toFloat()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toFloat()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
new file mode 100644
index 0000000..9d24ffc
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatIntMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatIntMapTest {
+    @Test
+    fun floatIntMap() {
+        val map = MutableFloatIntMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyFloatIntMap() {
+        val map = emptyFloatIntMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyFloatIntMap(), map)
+    }
+
+    @Test
+    fun floatIntMapFunction() {
+        val map = mutableFloatIntMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableFloatIntMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatIntMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableFloatIntMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatIntMapPairsFunction() {
+        val map = mutableFloatIntMapOf(
+            1f to 1,
+            2f to 2
+        )
+        assertEquals(2, map.size)
+        assertEquals(1, map[1f])
+        assertEquals(2, map[2f])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableFloatIntMap(12)
+        map[1f] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableFloatIntMap(2)
+        map[1f] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableFloatIntMap(0)
+        map[1f] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[1f] = 2
+
+        assertEquals(1, map.size)
+        assertEquals(2, map[1f])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableFloatIntMap()
+
+        map.put(1f, 1)
+        assertEquals(1, map[1f])
+        map.put(1f, 2)
+        assertEquals(2, map[1f])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+
+        map.putAll(arrayOf(3f to 3, 7f to 7))
+
+        assertEquals(4, map.size)
+        assertEquals(3, map[3f])
+        assertEquals(7, map[7f])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableFloatIntMap()
+        map += 1f to 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1f])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableFloatIntMap()
+        map += arrayOf(3f to 3, 7f to 7)
+
+        assertEquals(2, map.size)
+        assertEquals(3, map[3f])
+        assertEquals(7, map[7f])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertFailsWith<NoSuchElementException> {
+            map[2f]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertEquals(2, map.getOrDefault(2f, 2))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertEquals(3, map.getOrElse(3f) { 3 })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        var counter = 0
+        map.getOrPut(1f) {
+            counter++
+            2
+        }
+        assertEquals(1, map[1f])
+        assertEquals(0, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            2
+        }
+        assertEquals(2, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            3
+        }
+        assertEquals(2, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            3
+        }
+        assertEquals(3, map[3f])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableFloatIntMap()
+        map.remove(1f)
+
+        map[1f] = 1
+        map.remove(1f)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableFloatIntMap(6)
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1f)
+        map.remove(2f)
+        map.remove(3f)
+        map.remove(4f)
+        map.remove(5f)
+        map.remove(6f)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7f] = 7
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        map.removeIf { key, _ -> key == 1f || key == 3f }
+
+        assertEquals(4, map.size)
+        assertEquals(2, map[2f])
+        assertEquals(4, map[4f])
+        assertEquals(5, map[5f])
+        assertEquals(6, map[6f])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+
+        map -= 1f
+
+        assertEquals(2, map.size)
+        assertFalse(1f in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+
+        map -= floatArrayOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+
+        map -= floatSetOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+
+        map -= floatListOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableFloatIntMap()
+        assertFalse(map.remove(1f, 1))
+
+        map[1f] = 1
+        assertTrue(map.remove(1f, 1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableFloatIntMap()
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toInt()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableFloatIntMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toFloat())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableFloatIntMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toInt()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableFloatIntMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toInt()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableFloatIntMap()
+
+        for (i in 0 until 32) {
+            map[i.toFloat()] = i.toInt()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableFloatIntMap()
+        assertEquals("{}", map.toString())
+
+        map[1f] = 1
+        map[2f] = 2
+        val oneValueString = 1.toString()
+        val twoValueString = 2.toString()
+        val oneKeyString = 1f.toString()
+        val twoKeyString = 2f.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableFloatIntMap()
+        assertNotEquals(map, map2)
+
+        map2[1f] = 1
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertTrue(map.containsKey(1f))
+        assertFalse(map.containsKey(2f))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertTrue(1f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+
+        assertTrue(map.containsValue(1))
+        assertFalse(map.containsValue(3))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableFloatIntMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1f] = 1
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableFloatIntMap()
+        assertEquals(0, map.count())
+
+        map[1f] = 1
+        assertEquals(1, map.count())
+
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        assertEquals(2, map.count { key, _ -> key <= 2f })
+        assertEquals(0, map.count { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        assertTrue(map.any { key, _ -> key == 4f })
+        assertFalse(map.any { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableFloatIntMap()
+        map[1f] = 1
+        map[2f] = 2
+        map[3f] = 3
+        map[4f] = 4
+        map[5f] = 5
+        map[6f] = 6
+
+        assertTrue(map.all { key, value -> key > 0f && value >= 1 })
+        assertFalse(map.all { key, _ -> key < 6f })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableFloatIntMap()
+        assertEquals(7, map.trim())
+
+        map[1f] = 1
+        map[3f] = 3
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toInt()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toFloat()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
index b4e501b..12e6d66 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatListTest.kt
@@ -15,7 +15,6 @@
  */
 package androidx.collection
 
-import kotlin.math.roundToInt
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -23,6 +22,14 @@
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class FloatListTest {
     private val list: MutableFloatList = mutableFloatListOf(1f, 2f, 3f, 4f, 5f)
 
@@ -81,7 +88,7 @@
 
     @Test
     fun string() {
-        assertEquals("[1.0, 2.0, 3.0, 4.0, 5.0]", list.toString())
+        assertEquals("[${1f}, ${2f}, ${3f}, ${4f}, ${5f}]", list.toString())
         assertEquals("[]", mutableFloatListOf().toString())
     }
 
@@ -335,7 +342,7 @@
 
     @Test
     fun fold() {
-        assertEquals("12345", list.fold("") { acc, i -> acc + i.roundToInt().toString() })
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -343,14 +350,14 @@
         assertEquals(
             "01-12-23-34-45-",
             list.foldIndexed("") { index, acc, i ->
-                "$acc$index${i.roundToInt()}-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
 
     @Test
     fun foldRight() {
-        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.roundToInt().toString() })
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -358,7 +365,7 @@
         assertEquals(
             "45-34-23-12-01-",
             list.foldRightIndexed("") { index, i, acc ->
-                "$acc$index${i.roundToInt()}-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
new file mode 100644
index 0000000..cc90959
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatLongMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class FloatLongMapTest {
+    @Test
+    fun floatLongMap() {
+        val map = MutableFloatLongMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyFloatLongMap() {
+        val map = emptyFloatLongMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyFloatLongMap(), map)
+    }
+
+    @Test
+    fun floatLongMapFunction() {
+        val map = mutableFloatLongMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableFloatLongMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatLongMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableFloatLongMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatLongMapPairsFunction() {
+        val map = mutableFloatLongMapOf(
+            1f to 1L,
+            2f to 2L
+        )
+        assertEquals(2, map.size)
+        assertEquals(1L, map[1f])
+        assertEquals(2L, map[2f])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableFloatLongMap(12)
+        map[1f] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableFloatLongMap(2)
+        map[1f] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableFloatLongMap(0)
+        map[1f] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[1f] = 2L
+
+        assertEquals(1, map.size)
+        assertEquals(2L, map[1f])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableFloatLongMap()
+
+        map.put(1f, 1L)
+        assertEquals(1L, map[1f])
+        map.put(1f, 2L)
+        assertEquals(2L, map[1f])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+
+        map.putAll(arrayOf(3f to 3L, 7f to 7L))
+
+        assertEquals(4, map.size)
+        assertEquals(3L, map[3f])
+        assertEquals(7L, map[7f])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableFloatLongMap()
+        map += 1f to 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1f])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableFloatLongMap()
+        map += arrayOf(3f to 3L, 7f to 7L)
+
+        assertEquals(2, map.size)
+        assertEquals(3L, map[3f])
+        assertEquals(7L, map[7f])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertFailsWith<NoSuchElementException> {
+            map[2f]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertEquals(2L, map.getOrDefault(2f, 2L))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertEquals(3L, map.getOrElse(3f) { 3L })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        var counter = 0
+        map.getOrPut(1f) {
+            counter++
+            2L
+        }
+        assertEquals(1L, map[1f])
+        assertEquals(0, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            2L
+        }
+        assertEquals(2L, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            3L
+        }
+        assertEquals(2L, map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            3L
+        }
+        assertEquals(3L, map[3f])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableFloatLongMap()
+        map.remove(1f)
+
+        map[1f] = 1L
+        map.remove(1f)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableFloatLongMap(6)
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1f)
+        map.remove(2f)
+        map.remove(3f)
+        map.remove(4f)
+        map.remove(5f)
+        map.remove(6f)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7f] = 7L
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        map.removeIf { key, _ -> key == 1f || key == 3f }
+
+        assertEquals(4, map.size)
+        assertEquals(2L, map[2f])
+        assertEquals(4L, map[4f])
+        assertEquals(5L, map[5f])
+        assertEquals(6L, map[6f])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+
+        map -= 1f
+
+        assertEquals(2, map.size)
+        assertFalse(1f in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+
+        map -= floatArrayOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+
+        map -= floatSetOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+
+        map -= floatListOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertFalse(3f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableFloatLongMap()
+        assertFalse(map.remove(1f, 1L))
+
+        map[1f] = 1L
+        assertTrue(map.remove(1f, 1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableFloatLongMap()
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toLong()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableFloatLongMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toFloat())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableFloatLongMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toLong()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableFloatLongMap()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toLong()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableFloatLongMap()
+
+        for (i in 0 until 32) {
+            map[i.toFloat()] = i.toLong()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableFloatLongMap()
+        assertEquals("{}", map.toString())
+
+        map[1f] = 1L
+        map[2f] = 2L
+        val oneValueString = 1L.toString()
+        val twoValueString = 2L.toString()
+        val oneKeyString = 1f.toString()
+        val twoKeyString = 2f.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableFloatLongMap()
+        assertNotEquals(map, map2)
+
+        map2[1f] = 1L
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertTrue(map.containsKey(1f))
+        assertFalse(map.containsKey(2f))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertTrue(1f in map)
+        assertFalse(2f in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+
+        assertTrue(map.containsValue(1L))
+        assertFalse(map.containsValue(3L))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableFloatLongMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1f] = 1L
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableFloatLongMap()
+        assertEquals(0, map.count())
+
+        map[1f] = 1L
+        assertEquals(1, map.count())
+
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        assertEquals(2, map.count { key, _ -> key <= 2f })
+        assertEquals(0, map.count { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        assertTrue(map.any { key, _ -> key == 4f })
+        assertFalse(map.any { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableFloatLongMap()
+        map[1f] = 1L
+        map[2f] = 2L
+        map[3f] = 3L
+        map[4f] = 4L
+        map[5f] = 5L
+        map[6f] = 6L
+
+        assertTrue(map.all { key, value -> key > 0f && value >= 1L })
+        assertFalse(map.all { key, _ -> key < 6f })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableFloatLongMap()
+        assertEquals(7, map.trim())
+
+        map[1f] = 1L
+        map[3f] = 3L
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toLong()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toFloat()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
new file mode 100644
index 0000000..4a10fc7b
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatObjectMapTest.kt
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class FloatObjectMapTest {
+    @Test
+    fun floatObjectMap() {
+        val map = MutableFloatObjectMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyFloatObjectMap() {
+        val map = emptyFloatObjectMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyFloatObjectMap<String>(), map)
+    }
+
+    @Test
+    fun floatObjectMapFunction() {
+        val map = mutableFloatObjectMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableFloatObjectMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatObjectMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableFloatObjectMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun floatObjectMapPairsFunction() {
+        val map = mutableFloatObjectMapOf(
+            1f to "World",
+            2f to "Monde"
+        )
+        assertEquals(2, map.size)
+        assertEquals("World", map[1f])
+        assertEquals("Monde", map[2f])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun insertIndex0() {
+        val map = MutableFloatObjectMap<String>()
+        map.put(1f, "World")
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableFloatObjectMap<String>(12)
+        map[1f] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableFloatObjectMap<String>(2)
+        map[1f] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableFloatObjectMap<String>(0)
+        map[1f] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[1f] = "Monde"
+
+        assertEquals(1, map.size)
+        assertEquals("Monde", map[1f])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableFloatObjectMap<String?>()
+
+        assertNull(map.put(1f, "World"))
+        assertEquals("World", map.put(1f, "Monde"))
+        assertNull(map.put(2f, null))
+        assertNull(map.put(2f, "Monde"))
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        map.putAll(arrayOf(3f to "Welt", 7f to "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3f])
+        assertEquals("Mundo", map[7f])
+    }
+
+    @Test
+    fun putAllMap() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        map.putAll(mutableFloatObjectMapOf(3f to "Welt", 7f to "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3f])
+        assertEquals("Mundo", map[7f])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableFloatObjectMap<String>()
+        map += 1f to "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1f])
+    }
+
+    @Test
+    fun plusMap() {
+        val map = MutableFloatObjectMap<String>()
+        map += floatObjectMapOf(3f to "Welt", 7f to "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3f])
+        assertEquals("Mundo", map[7f])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableFloatObjectMap<String>()
+        map += arrayOf(3f to "Welt", 7f to "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3f])
+        assertEquals("Mundo", map[7f])
+    }
+
+    @Test
+    fun nullValue() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = null
+
+        assertEquals(1, map.size)
+        assertNull(map[1f])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+
+        assertNull(map[2f])
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+
+        assertEquals("Monde", map.getOrDefault(2f, "Monde"))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertEquals("Monde", map.getOrElse(2f) { "Monde" })
+        assertEquals("Welt", map.getOrElse(3f) { "Welt" })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+
+        var counter = 0
+        map.getOrPut(1f) {
+            counter++
+            "Monde"
+        }
+        assertEquals("World", map[1f])
+        assertEquals(0, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            "Monde"
+        }
+        assertEquals("Monde", map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(2f) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Monde", map[2f])
+        assertEquals(1, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            null
+        }
+        assertNull(map[3f])
+        assertEquals(2, counter)
+
+        map.getOrPut(3f) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Welt", map[3f])
+        assertEquals(3, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableFloatObjectMap<String?>()
+        assertNull(map.remove(1f))
+
+        map[1f] = "World"
+        assertEquals("World", map.remove(1f))
+        assertEquals(0, map.size)
+
+        map[1f] = null
+        assertNull(map.remove(1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableFloatObjectMap<String>(6)
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1f)
+        map.remove(2f)
+        map.remove(3f)
+        map.remove(4f)
+        map.remove(5f)
+        map.remove(6f)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7f] = "Mundo"
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        map.removeIf { key, value ->
+            key == 1f || key == 3f || value.startsWith('S')
+        }
+
+        assertEquals(2, map.size)
+        assertEquals("Monde", map[2f])
+        assertEquals("Mondo", map[5f])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+
+        map -= 1f
+
+        assertEquals(2, map.size)
+        assertNull(map[1f])
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+
+        map -= floatArrayOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertNull(map[3f])
+        assertNull(map[2f])
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+
+        map -= floatSetOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertNull(map[3f])
+        assertNull(map[2f])
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+
+        map -= floatListOf(3f, 2f)
+
+        assertEquals(1, map.size)
+        assertNull(map[3f])
+        assertNull(map[2f])
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableFloatObjectMap<String?>()
+        assertFalse(map.remove(1f, "World"))
+
+        map[1f] = "World"
+        assertTrue(map.remove(1f, "World"))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableFloatObjectMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toFloat()] = i.toString()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableFloatObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key.toInt().toString(), value)
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableFloatObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertEquals(key.toInt().toString(), map[key])
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableFloatObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toFloat()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachValue { value ->
+                assertNotNull(value.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableFloatObjectMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toFloat()] = i.toString()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableFloatObjectMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map[1f] = "World"
+        map[2f] = "Monde"
+        val oneKey = 1f.toString()
+        val twoKey = 2f.toString()
+        assertTrue(
+            "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+                "{$twoKey=Monde, $oneKey=World}" == map.toString()
+        )
+
+        map.clear()
+        map[1f] = null
+        assertEquals("{$oneKey=null}", map.toString())
+
+        val selfAsValueMap = MutableFloatObjectMap<Any>()
+        selfAsValueMap[1f] = selfAsValueMap
+        assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableFloatObjectMap<String?>()
+        map2[2f] = null
+
+        assertNotEquals(map, map2)
+
+        map2[1f] = "World"
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertTrue(map.containsKey(1f))
+        assertFalse(map.containsKey(3f))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertTrue(1f in map)
+        assertFalse(3f in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableFloatObjectMap<String?>()
+        map[1f] = "World"
+        map[2f] = null
+
+        assertTrue(map.containsValue("World"))
+        assertTrue(map.containsValue(null))
+        assertFalse(map.containsValue("Monde"))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableFloatObjectMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1f] = "World"
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableFloatObjectMap<String>()
+        assertEquals(0, map.count())
+
+        map[1f] = "World"
+        assertEquals(1, map.count())
+
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        assertEquals(2, map.count { key, _ -> key < 3f })
+        assertEquals(0, map.count { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        assertTrue(map.any { key, _ -> key > 5f })
+        assertFalse(map.any { key, _ -> key < 0f })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableFloatObjectMap<String>()
+        map[1f] = "World"
+        map[2f] = "Monde"
+        map[3f] = "Welt"
+        map[4f] = "Sekai"
+        map[5f] = "Mondo"
+        map[6f] = "Sesang"
+
+        assertTrue(map.all { key, value -> key < 7f && value.length > 0 })
+        assertFalse(map.all { key, _ -> key < 6f })
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
index 98f7b59..af07640 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/FloatSetTest.kt
@@ -23,6 +23,14 @@
 import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class FloatSetTest {
     @Test
     fun emptyFloatSetConstructor() {
@@ -147,9 +155,9 @@
         val set = MutableFloatSet()
         set += 1f
         set += 2f
-        var element: Float = Float.NaN
-        var otherElement: Float = Float.NaN
-        set.forEach { if (element.isNaN()) element = it else otherElement = it }
+        var element: Float = -1f
+        var otherElement: Float = -1f
+        set.forEach { if (element == -1f) element = it else otherElement = it }
         assertEquals(element, set.first())
         set -= element
         assertEquals(otherElement, set.first())
@@ -333,8 +341,8 @@
         set += 1f
         set += 5f
         assertTrue(
-            "[1.0, 5.0]" == set.toString() ||
-                "[5.0, 1.0]" == set.toString()
+            "[${1f}, ${5f}]" == set.toString() ||
+                "[${5f}, ${1f}]" == set.toString()
         )
     }
 
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
new file mode 100644
index 0000000..94c8ec2
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntFloatMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntFloatMapTest {
+    @Test
+    fun intFloatMap() {
+        val map = MutableIntFloatMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyIntFloatMap() {
+        val map = emptyIntFloatMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyIntFloatMap(), map)
+    }
+
+    @Test
+    fun intFloatMapFunction() {
+        val map = mutableIntFloatMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableIntFloatMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intFloatMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableIntFloatMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intFloatMapPairsFunction() {
+        val map = mutableIntFloatMapOf(
+            1 to 1f,
+            2 to 2f
+        )
+        assertEquals(2, map.size)
+        assertEquals(1f, map[1])
+        assertEquals(2f, map[2])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableIntFloatMap(12)
+        map[1] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableIntFloatMap(2)
+        map[1] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableIntFloatMap(0)
+        map[1] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[1] = 2f
+
+        assertEquals(1, map.size)
+        assertEquals(2f, map[1])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableIntFloatMap()
+
+        map.put(1, 1f)
+        assertEquals(1f, map[1])
+        map.put(1, 2f)
+        assertEquals(2f, map[1])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+
+        map.putAll(arrayOf(3 to 3f, 7 to 7f))
+
+        assertEquals(4, map.size)
+        assertEquals(3f, map[3])
+        assertEquals(7f, map[7])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableIntFloatMap()
+        map += 1 to 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableIntFloatMap()
+        map += arrayOf(3 to 3f, 7 to 7f)
+
+        assertEquals(2, map.size)
+        assertEquals(3f, map[3])
+        assertEquals(7f, map[7])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertFailsWith<NoSuchElementException> {
+            map[2]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertEquals(2f, map.getOrDefault(2, 2f))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertEquals(3f, map.getOrElse(3) { 3f })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        var counter = 0
+        map.getOrPut(1) {
+            counter++
+            2f
+        }
+        assertEquals(1f, map[1])
+        assertEquals(0, counter)
+
+        map.getOrPut(2) {
+            counter++
+            2f
+        }
+        assertEquals(2f, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(2) {
+            counter++
+            3f
+        }
+        assertEquals(2f, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(3) {
+            counter++
+            3f
+        }
+        assertEquals(3f, map[3])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableIntFloatMap()
+        map.remove(1)
+
+        map[1] = 1f
+        map.remove(1)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableIntFloatMap(6)
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1)
+        map.remove(2)
+        map.remove(3)
+        map.remove(4)
+        map.remove(5)
+        map.remove(6)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7] = 7f
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        map.removeIf { key, _ -> key == 1 || key == 3 }
+
+        assertEquals(4, map.size)
+        assertEquals(2f, map[2])
+        assertEquals(4f, map[4])
+        assertEquals(5f, map[5])
+        assertEquals(6f, map[6])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+
+        map -= 1
+
+        assertEquals(2, map.size)
+        assertFalse(1 in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+
+        map -= intArrayOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+
+        map -= intSetOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+
+        map -= intListOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableIntFloatMap()
+        assertFalse(map.remove(1, 1f))
+
+        map[1] = 1f
+        assertTrue(map.remove(1, 1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableIntFloatMap()
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toFloat()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableIntFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableIntFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toFloat()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableIntFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toFloat()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableIntFloatMap()
+
+        for (i in 0 until 32) {
+            map[i.toInt()] = i.toFloat()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableIntFloatMap()
+        assertEquals("{}", map.toString())
+
+        map[1] = 1f
+        map[2] = 2f
+        val oneValueString = 1f.toString()
+        val twoValueString = 2f.toString()
+        val oneKeyString = 1.toString()
+        val twoKeyString = 2.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableIntFloatMap()
+        assertNotEquals(map, map2)
+
+        map2[1] = 1f
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertTrue(map.containsKey(1))
+        assertFalse(map.containsKey(2))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertTrue(1 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+
+        assertTrue(map.containsValue(1f))
+        assertFalse(map.containsValue(3f))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableIntFloatMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1] = 1f
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableIntFloatMap()
+        assertEquals(0, map.count())
+
+        map[1] = 1f
+        assertEquals(1, map.count())
+
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        assertEquals(2, map.count { key, _ -> key <= 2 })
+        assertEquals(0, map.count { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        assertTrue(map.any { key, _ -> key == 4 })
+        assertFalse(map.any { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableIntFloatMap()
+        map[1] = 1f
+        map[2] = 2f
+        map[3] = 3f
+        map[4] = 4f
+        map[5] = 5f
+        map[6] = 6f
+
+        assertTrue(map.all { key, value -> key > 0 && value >= 1f })
+        assertFalse(map.all { key, _ -> key < 6 })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableIntFloatMap()
+        assertEquals(7, map.trim())
+
+        map[1] = 1f
+        map[3] = 3f
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toFloat()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toInt()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
new file mode 100644
index 0000000..933c5ba
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntIntMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntIntMapTest {
+    @Test
+    fun intIntMap() {
+        val map = MutableIntIntMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyIntIntMap() {
+        val map = emptyIntIntMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyIntIntMap(), map)
+    }
+
+    @Test
+    fun intIntMapFunction() {
+        val map = mutableIntIntMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableIntIntMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intIntMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableIntIntMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intIntMapPairsFunction() {
+        val map = mutableIntIntMapOf(
+            1 to 1,
+            2 to 2
+        )
+        assertEquals(2, map.size)
+        assertEquals(1, map[1])
+        assertEquals(2, map[2])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableIntIntMap(12)
+        map[1] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableIntIntMap(2)
+        map[1] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableIntIntMap(0)
+        map[1] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[1] = 2
+
+        assertEquals(1, map.size)
+        assertEquals(2, map[1])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableIntIntMap()
+
+        map.put(1, 1)
+        assertEquals(1, map[1])
+        map.put(1, 2)
+        assertEquals(2, map[1])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+
+        map.putAll(arrayOf(3 to 3, 7 to 7))
+
+        assertEquals(4, map.size)
+        assertEquals(3, map[3])
+        assertEquals(7, map[7])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableIntIntMap()
+        map += 1 to 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableIntIntMap()
+        map += arrayOf(3 to 3, 7 to 7)
+
+        assertEquals(2, map.size)
+        assertEquals(3, map[3])
+        assertEquals(7, map[7])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertFailsWith<NoSuchElementException> {
+            map[2]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertEquals(2, map.getOrDefault(2, 2))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertEquals(3, map.getOrElse(3) { 3 })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        var counter = 0
+        map.getOrPut(1) {
+            counter++
+            2
+        }
+        assertEquals(1, map[1])
+        assertEquals(0, counter)
+
+        map.getOrPut(2) {
+            counter++
+            2
+        }
+        assertEquals(2, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(2) {
+            counter++
+            3
+        }
+        assertEquals(2, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(3) {
+            counter++
+            3
+        }
+        assertEquals(3, map[3])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableIntIntMap()
+        map.remove(1)
+
+        map[1] = 1
+        map.remove(1)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableIntIntMap(6)
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1)
+        map.remove(2)
+        map.remove(3)
+        map.remove(4)
+        map.remove(5)
+        map.remove(6)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7] = 7
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        map.removeIf { key, _ -> key == 1 || key == 3 }
+
+        assertEquals(4, map.size)
+        assertEquals(2, map[2])
+        assertEquals(4, map[4])
+        assertEquals(5, map[5])
+        assertEquals(6, map[6])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+
+        map -= 1
+
+        assertEquals(2, map.size)
+        assertFalse(1 in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+
+        map -= intArrayOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+
+        map -= intSetOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+
+        map -= intListOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableIntIntMap()
+        assertFalse(map.remove(1, 1))
+
+        map[1] = 1
+        assertTrue(map.remove(1, 1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableIntIntMap()
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toInt()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableIntIntMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableIntIntMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toInt()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableIntIntMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toInt()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableIntIntMap()
+
+        for (i in 0 until 32) {
+            map[i.toInt()] = i.toInt()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableIntIntMap()
+        assertEquals("{}", map.toString())
+
+        map[1] = 1
+        map[2] = 2
+        val oneValueString = 1.toString()
+        val twoValueString = 2.toString()
+        val oneKeyString = 1.toString()
+        val twoKeyString = 2.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableIntIntMap()
+        assertNotEquals(map, map2)
+
+        map2[1] = 1
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertTrue(map.containsKey(1))
+        assertFalse(map.containsKey(2))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertTrue(1 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+
+        assertTrue(map.containsValue(1))
+        assertFalse(map.containsValue(3))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableIntIntMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1] = 1
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableIntIntMap()
+        assertEquals(0, map.count())
+
+        map[1] = 1
+        assertEquals(1, map.count())
+
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        assertEquals(2, map.count { key, _ -> key <= 2 })
+        assertEquals(0, map.count { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        assertTrue(map.any { key, _ -> key == 4 })
+        assertFalse(map.any { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableIntIntMap()
+        map[1] = 1
+        map[2] = 2
+        map[3] = 3
+        map[4] = 4
+        map[5] = 5
+        map[6] = 6
+
+        assertTrue(map.all { key, value -> key > 0 && value >= 1 })
+        assertFalse(map.all { key, _ -> key < 6 })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableIntIntMap()
+        assertEquals(7, map.trim())
+
+        map[1] = 1
+        map[3] = 3
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toInt()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toInt()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
index 66d89af..7a613eb 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntListTest.kt
@@ -22,6 +22,14 @@
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class IntListTest {
     private val list: MutableIntList = mutableIntListOf(1, 2, 3, 4, 5)
 
@@ -80,7 +88,7 @@
 
     @Test
     fun string() {
-        assertEquals("[1, 2, 3, 4, 5]", list.toString())
+        assertEquals("[${1}, ${2}, ${3}, ${4}, ${5}]", list.toString())
         assertEquals("[]", mutableIntListOf().toString())
     }
 
@@ -334,7 +342,7 @@
 
     @Test
     fun fold() {
-        assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -342,14 +350,14 @@
         assertEquals(
             "01-12-23-34-45-",
             list.foldIndexed("") { index, acc, i ->
-                "$acc$index$i-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
 
     @Test
     fun foldRight() {
-        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -357,7 +365,7 @@
         assertEquals(
             "45-34-23-12-01-",
             list.foldRightIndexed("") { index, i, acc ->
-                "$acc$index$i-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
new file mode 100644
index 0000000..734fb05
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntLongMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class IntLongMapTest {
+    @Test
+    fun intLongMap() {
+        val map = MutableIntLongMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyIntLongMap() {
+        val map = emptyIntLongMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyIntLongMap(), map)
+    }
+
+    @Test
+    fun intLongMapFunction() {
+        val map = mutableIntLongMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableIntLongMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intLongMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableIntLongMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intLongMapPairsFunction() {
+        val map = mutableIntLongMapOf(
+            1 to 1L,
+            2 to 2L
+        )
+        assertEquals(2, map.size)
+        assertEquals(1L, map[1])
+        assertEquals(2L, map[2])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableIntLongMap(12)
+        map[1] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableIntLongMap(2)
+        map[1] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableIntLongMap(0)
+        map[1] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[1] = 2L
+
+        assertEquals(1, map.size)
+        assertEquals(2L, map[1])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableIntLongMap()
+
+        map.put(1, 1L)
+        assertEquals(1L, map[1])
+        map.put(1, 2L)
+        assertEquals(2L, map[1])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+
+        map.putAll(arrayOf(3 to 3L, 7 to 7L))
+
+        assertEquals(4, map.size)
+        assertEquals(3L, map[3])
+        assertEquals(7L, map[7])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableIntLongMap()
+        map += 1 to 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableIntLongMap()
+        map += arrayOf(3 to 3L, 7 to 7L)
+
+        assertEquals(2, map.size)
+        assertEquals(3L, map[3])
+        assertEquals(7L, map[7])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertFailsWith<NoSuchElementException> {
+            map[2]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertEquals(2L, map.getOrDefault(2, 2L))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertEquals(3L, map.getOrElse(3) { 3L })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        var counter = 0
+        map.getOrPut(1) {
+            counter++
+            2L
+        }
+        assertEquals(1L, map[1])
+        assertEquals(0, counter)
+
+        map.getOrPut(2) {
+            counter++
+            2L
+        }
+        assertEquals(2L, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(2) {
+            counter++
+            3L
+        }
+        assertEquals(2L, map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(3) {
+            counter++
+            3L
+        }
+        assertEquals(3L, map[3])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableIntLongMap()
+        map.remove(1)
+
+        map[1] = 1L
+        map.remove(1)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableIntLongMap(6)
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1)
+        map.remove(2)
+        map.remove(3)
+        map.remove(4)
+        map.remove(5)
+        map.remove(6)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7] = 7L
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        map.removeIf { key, _ -> key == 1 || key == 3 }
+
+        assertEquals(4, map.size)
+        assertEquals(2L, map[2])
+        assertEquals(4L, map[4])
+        assertEquals(5L, map[5])
+        assertEquals(6L, map[6])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+
+        map -= 1
+
+        assertEquals(2, map.size)
+        assertFalse(1 in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+
+        map -= intArrayOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+
+        map -= intSetOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+
+        map -= intListOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertFalse(3 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableIntLongMap()
+        assertFalse(map.remove(1, 1L))
+
+        map[1] = 1L
+        assertTrue(map.remove(1, 1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableIntLongMap()
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toLong()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableIntLongMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableIntLongMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toLong()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableIntLongMap()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toLong()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableIntLongMap()
+
+        for (i in 0 until 32) {
+            map[i.toInt()] = i.toLong()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableIntLongMap()
+        assertEquals("{}", map.toString())
+
+        map[1] = 1L
+        map[2] = 2L
+        val oneValueString = 1L.toString()
+        val twoValueString = 2L.toString()
+        val oneKeyString = 1.toString()
+        val twoKeyString = 2.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableIntLongMap()
+        assertNotEquals(map, map2)
+
+        map2[1] = 1L
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertTrue(map.containsKey(1))
+        assertFalse(map.containsKey(2))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertTrue(1 in map)
+        assertFalse(2 in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+
+        assertTrue(map.containsValue(1L))
+        assertFalse(map.containsValue(3L))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableIntLongMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1] = 1L
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableIntLongMap()
+        assertEquals(0, map.count())
+
+        map[1] = 1L
+        assertEquals(1, map.count())
+
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        assertEquals(2, map.count { key, _ -> key <= 2 })
+        assertEquals(0, map.count { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        assertTrue(map.any { key, _ -> key == 4 })
+        assertFalse(map.any { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableIntLongMap()
+        map[1] = 1L
+        map[2] = 2L
+        map[3] = 3L
+        map[4] = 4L
+        map[5] = 5L
+        map[6] = 6L
+
+        assertTrue(map.all { key, value -> key > 0 && value >= 1L })
+        assertFalse(map.all { key, _ -> key < 6 })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableIntLongMap()
+        assertEquals(7, map.trim())
+
+        map[1] = 1L
+        map[3] = 3L
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toLong()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toInt()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
new file mode 100644
index 0000000..40038d2
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntObjectMapTest.kt
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class IntObjectMapTest {
+    @Test
+    fun intObjectMap() {
+        val map = MutableIntObjectMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyIntObjectMap() {
+        val map = emptyIntObjectMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyIntObjectMap<String>(), map)
+    }
+
+    @Test
+    fun intObjectMapFunction() {
+        val map = mutableIntObjectMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableIntObjectMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intObjectMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableIntObjectMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun intObjectMapPairsFunction() {
+        val map = mutableIntObjectMapOf(
+            1 to "World",
+            2 to "Monde"
+        )
+        assertEquals(2, map.size)
+        assertEquals("World", map[1])
+        assertEquals("Monde", map[2])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun insertIndex0() {
+        val map = MutableIntObjectMap<String>()
+        map.put(1, "World")
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableIntObjectMap<String>(12)
+        map[1] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableIntObjectMap<String>(2)
+        map[1] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableIntObjectMap<String>(0)
+        map[1] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[1] = "Monde"
+
+        assertEquals(1, map.size)
+        assertEquals("Monde", map[1])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableIntObjectMap<String?>()
+
+        assertNull(map.put(1, "World"))
+        assertEquals("World", map.put(1, "Monde"))
+        assertNull(map.put(2, null))
+        assertNull(map.put(2, "Monde"))
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        map.putAll(arrayOf(3 to "Welt", 7 to "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3])
+        assertEquals("Mundo", map[7])
+    }
+
+    @Test
+    fun putAllMap() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        map.putAll(mutableIntObjectMapOf(3 to "Welt", 7 to "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3])
+        assertEquals("Mundo", map[7])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableIntObjectMap<String>()
+        map += 1 to "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1])
+    }
+
+    @Test
+    fun plusMap() {
+        val map = MutableIntObjectMap<String>()
+        map += intObjectMapOf(3 to "Welt", 7 to "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3])
+        assertEquals("Mundo", map[7])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableIntObjectMap<String>()
+        map += arrayOf(3 to "Welt", 7 to "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3])
+        assertEquals("Mundo", map[7])
+    }
+
+    @Test
+    fun nullValue() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = null
+
+        assertEquals(1, map.size)
+        assertNull(map[1])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+
+        assertNull(map[2])
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+
+        assertEquals("Monde", map.getOrDefault(2, "Monde"))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertEquals("Monde", map.getOrElse(2) { "Monde" })
+        assertEquals("Welt", map.getOrElse(3) { "Welt" })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+
+        var counter = 0
+        map.getOrPut(1) {
+            counter++
+            "Monde"
+        }
+        assertEquals("World", map[1])
+        assertEquals(0, counter)
+
+        map.getOrPut(2) {
+            counter++
+            "Monde"
+        }
+        assertEquals("Monde", map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(2) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Monde", map[2])
+        assertEquals(1, counter)
+
+        map.getOrPut(3) {
+            counter++
+            null
+        }
+        assertNull(map[3])
+        assertEquals(2, counter)
+
+        map.getOrPut(3) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Welt", map[3])
+        assertEquals(3, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableIntObjectMap<String?>()
+        assertNull(map.remove(1))
+
+        map[1] = "World"
+        assertEquals("World", map.remove(1))
+        assertEquals(0, map.size)
+
+        map[1] = null
+        assertNull(map.remove(1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableIntObjectMap<String>(6)
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1)
+        map.remove(2)
+        map.remove(3)
+        map.remove(4)
+        map.remove(5)
+        map.remove(6)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7] = "Mundo"
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        map.removeIf { key, value ->
+            key == 1 || key == 3 || value.startsWith('S')
+        }
+
+        assertEquals(2, map.size)
+        assertEquals("Monde", map[2])
+        assertEquals("Mondo", map[5])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+
+        map -= 1
+
+        assertEquals(2, map.size)
+        assertNull(map[1])
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+
+        map -= intArrayOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertNull(map[3])
+        assertNull(map[2])
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+
+        map -= intSetOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertNull(map[3])
+        assertNull(map[2])
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+
+        map -= intListOf(3, 2)
+
+        assertEquals(1, map.size)
+        assertNull(map[3])
+        assertNull(map[2])
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableIntObjectMap<String?>()
+        assertFalse(map.remove(1, "World"))
+
+        map[1] = "World"
+        assertTrue(map.remove(1, "World"))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableIntObjectMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toInt()] = i.toString()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableIntObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key.toInt().toString(), value)
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableIntObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertEquals(key.toInt().toString(), map[key])
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableIntObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toInt()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachValue { value ->
+                assertNotNull(value.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableIntObjectMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toInt()] = i.toString()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableIntObjectMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map[1] = "World"
+        map[2] = "Monde"
+        val oneKey = 1.toString()
+        val twoKey = 2.toString()
+        assertTrue(
+            "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+                "{$twoKey=Monde, $oneKey=World}" == map.toString()
+        )
+
+        map.clear()
+        map[1] = null
+        assertEquals("{$oneKey=null}", map.toString())
+
+        val selfAsValueMap = MutableIntObjectMap<Any>()
+        selfAsValueMap[1] = selfAsValueMap
+        assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableIntObjectMap<String?>()
+        map2[2] = null
+
+        assertNotEquals(map, map2)
+
+        map2[1] = "World"
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertTrue(map.containsKey(1))
+        assertFalse(map.containsKey(3))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertTrue(1 in map)
+        assertFalse(3 in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableIntObjectMap<String?>()
+        map[1] = "World"
+        map[2] = null
+
+        assertTrue(map.containsValue("World"))
+        assertTrue(map.containsValue(null))
+        assertFalse(map.containsValue("Monde"))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableIntObjectMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1] = "World"
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableIntObjectMap<String>()
+        assertEquals(0, map.count())
+
+        map[1] = "World"
+        assertEquals(1, map.count())
+
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        assertEquals(2, map.count { key, _ -> key < 3 })
+        assertEquals(0, map.count { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        assertTrue(map.any { key, _ -> key > 5 })
+        assertFalse(map.any { key, _ -> key < 0 })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableIntObjectMap<String>()
+        map[1] = "World"
+        map[2] = "Monde"
+        map[3] = "Welt"
+        map[4] = "Sekai"
+        map[5] = "Mondo"
+        map[6] = "Sesang"
+
+        assertTrue(map.all { key, value -> key < 7 && value.length > 0 })
+        assertFalse(map.all { key, _ -> key < 6 })
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
index 9d326e1..9d55a17 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/IntSetTest.kt
@@ -23,6 +23,14 @@
 import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class IntSetTest {
     @Test
     fun emptyIntSetConstructor() {
@@ -147,9 +155,9 @@
         val set = MutableIntSet()
         set += 1
         set += 2
-        var element: Int = Int.MIN_VALUE
-        var otherElement: Int = Int.MIN_VALUE
-        set.forEach { if (element == Int.MIN_VALUE) element = it else otherElement = it }
+        var element: Int = -1
+        var otherElement: Int = -1
+        set.forEach { if (element == -1) element = it else otherElement = it }
         assertEquals(element, set.first())
         set -= element
         assertEquals(otherElement, set.first())
@@ -333,8 +341,8 @@
         set += 1
         set += 5
         assertTrue(
-            "[1, 5]" == set.toString() ||
-                "[5, 1]" == set.toString()
+            "[${1}, ${5}]" == set.toString() ||
+                "[${5}, ${1}]" == set.toString()
         )
     }
 
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
new file mode 100644
index 0000000..b395753
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongFloatMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongFloatMapTest {
+    @Test
+    fun longFloatMap() {
+        val map = MutableLongFloatMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyLongFloatMap() {
+        val map = emptyLongFloatMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyLongFloatMap(), map)
+    }
+
+    @Test
+    fun longFloatMapFunction() {
+        val map = mutableLongFloatMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableLongFloatMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longFloatMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableLongFloatMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longFloatMapPairsFunction() {
+        val map = mutableLongFloatMapOf(
+            1L to 1f,
+            2L to 2f
+        )
+        assertEquals(2, map.size)
+        assertEquals(1f, map[1L])
+        assertEquals(2f, map[2L])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableLongFloatMap(12)
+        map[1L] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableLongFloatMap(2)
+        map[1L] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableLongFloatMap(0)
+        map[1L] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[1L] = 2f
+
+        assertEquals(1, map.size)
+        assertEquals(2f, map[1L])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableLongFloatMap()
+
+        map.put(1L, 1f)
+        assertEquals(1f, map[1L])
+        map.put(1L, 2f)
+        assertEquals(2f, map[1L])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+
+        map.putAll(arrayOf(3L to 3f, 7L to 7f))
+
+        assertEquals(4, map.size)
+        assertEquals(3f, map[3L])
+        assertEquals(7f, map[7L])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableLongFloatMap()
+        map += 1L to 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[1L])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableLongFloatMap()
+        map += arrayOf(3L to 3f, 7L to 7f)
+
+        assertEquals(2, map.size)
+        assertEquals(3f, map[3L])
+        assertEquals(7f, map[7L])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertFailsWith<NoSuchElementException> {
+            map[2L]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertEquals(2f, map.getOrDefault(2L, 2f))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertEquals(3f, map.getOrElse(3L) { 3f })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        var counter = 0
+        map.getOrPut(1L) {
+            counter++
+            2f
+        }
+        assertEquals(1f, map[1L])
+        assertEquals(0, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            2f
+        }
+        assertEquals(2f, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            3f
+        }
+        assertEquals(2f, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            3f
+        }
+        assertEquals(3f, map[3L])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableLongFloatMap()
+        map.remove(1L)
+
+        map[1L] = 1f
+        map.remove(1L)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableLongFloatMap(6)
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1L)
+        map.remove(2L)
+        map.remove(3L)
+        map.remove(4L)
+        map.remove(5L)
+        map.remove(6L)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7L] = 7f
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        map.removeIf { key, _ -> key == 1L || key == 3L }
+
+        assertEquals(4, map.size)
+        assertEquals(2f, map[2L])
+        assertEquals(4f, map[4L])
+        assertEquals(5f, map[5L])
+        assertEquals(6f, map[6L])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+
+        map -= 1L
+
+        assertEquals(2, map.size)
+        assertFalse(1L in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+
+        map -= longArrayOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+
+        map -= longSetOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+
+        map -= longListOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableLongFloatMap()
+        assertFalse(map.remove(1L, 1f))
+
+        map[1L] = 1f
+        assertTrue(map.remove(1L, 1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableLongFloatMap()
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toFloat()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableLongFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toLong())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableLongFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toFloat()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableLongFloatMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toFloat()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableLongFloatMap()
+
+        for (i in 0 until 32) {
+            map[i.toLong()] = i.toFloat()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableLongFloatMap()
+        assertEquals("{}", map.toString())
+
+        map[1L] = 1f
+        map[2L] = 2f
+        val oneValueString = 1f.toString()
+        val twoValueString = 2f.toString()
+        val oneKeyString = 1L.toString()
+        val twoKeyString = 2L.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableLongFloatMap()
+        assertNotEquals(map, map2)
+
+        map2[1L] = 1f
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertTrue(map.containsKey(1L))
+        assertFalse(map.containsKey(2L))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertTrue(1L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+
+        assertTrue(map.containsValue(1f))
+        assertFalse(map.containsValue(3f))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableLongFloatMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1L] = 1f
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableLongFloatMap()
+        assertEquals(0, map.count())
+
+        map[1L] = 1f
+        assertEquals(1, map.count())
+
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        assertEquals(2, map.count { key, _ -> key <= 2L })
+        assertEquals(0, map.count { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        assertTrue(map.any { key, _ -> key == 4L })
+        assertFalse(map.any { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableLongFloatMap()
+        map[1L] = 1f
+        map[2L] = 2f
+        map[3L] = 3f
+        map[4L] = 4f
+        map[5L] = 5f
+        map[6L] = 6f
+
+        assertTrue(map.all { key, value -> key > 0L && value >= 1f })
+        assertFalse(map.all { key, _ -> key < 6L })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableLongFloatMap()
+        assertEquals(7, map.trim())
+
+        map[1L] = 1f
+        map[3L] = 3f
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toFloat()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toLong()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
new file mode 100644
index 0000000..cf64c64
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongIntMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongIntMapTest {
+    @Test
+    fun longIntMap() {
+        val map = MutableLongIntMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyLongIntMap() {
+        val map = emptyLongIntMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyLongIntMap(), map)
+    }
+
+    @Test
+    fun longIntMapFunction() {
+        val map = mutableLongIntMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableLongIntMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longIntMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableLongIntMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longIntMapPairsFunction() {
+        val map = mutableLongIntMapOf(
+            1L to 1,
+            2L to 2
+        )
+        assertEquals(2, map.size)
+        assertEquals(1, map[1L])
+        assertEquals(2, map[2L])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableLongIntMap(12)
+        map[1L] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableLongIntMap(2)
+        map[1L] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableLongIntMap(0)
+        map[1L] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[1L] = 2
+
+        assertEquals(1, map.size)
+        assertEquals(2, map[1L])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableLongIntMap()
+
+        map.put(1L, 1)
+        assertEquals(1, map[1L])
+        map.put(1L, 2)
+        assertEquals(2, map[1L])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+
+        map.putAll(arrayOf(3L to 3, 7L to 7))
+
+        assertEquals(4, map.size)
+        assertEquals(3, map[3L])
+        assertEquals(7, map[7L])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableLongIntMap()
+        map += 1L to 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[1L])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableLongIntMap()
+        map += arrayOf(3L to 3, 7L to 7)
+
+        assertEquals(2, map.size)
+        assertEquals(3, map[3L])
+        assertEquals(7, map[7L])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertFailsWith<NoSuchElementException> {
+            map[2L]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertEquals(2, map.getOrDefault(2L, 2))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertEquals(3, map.getOrElse(3L) { 3 })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        var counter = 0
+        map.getOrPut(1L) {
+            counter++
+            2
+        }
+        assertEquals(1, map[1L])
+        assertEquals(0, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            2
+        }
+        assertEquals(2, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            3
+        }
+        assertEquals(2, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            3
+        }
+        assertEquals(3, map[3L])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableLongIntMap()
+        map.remove(1L)
+
+        map[1L] = 1
+        map.remove(1L)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableLongIntMap(6)
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1L)
+        map.remove(2L)
+        map.remove(3L)
+        map.remove(4L)
+        map.remove(5L)
+        map.remove(6L)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7L] = 7
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        map.removeIf { key, _ -> key == 1L || key == 3L }
+
+        assertEquals(4, map.size)
+        assertEquals(2, map[2L])
+        assertEquals(4, map[4L])
+        assertEquals(5, map[5L])
+        assertEquals(6, map[6L])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+
+        map -= 1L
+
+        assertEquals(2, map.size)
+        assertFalse(1L in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+
+        map -= longArrayOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+
+        map -= longSetOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+
+        map -= longListOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableLongIntMap()
+        assertFalse(map.remove(1L, 1))
+
+        map[1L] = 1
+        assertTrue(map.remove(1L, 1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableLongIntMap()
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toInt()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableLongIntMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toLong())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableLongIntMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toInt()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableLongIntMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toInt()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableLongIntMap()
+
+        for (i in 0 until 32) {
+            map[i.toLong()] = i.toInt()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableLongIntMap()
+        assertEquals("{}", map.toString())
+
+        map[1L] = 1
+        map[2L] = 2
+        val oneValueString = 1.toString()
+        val twoValueString = 2.toString()
+        val oneKeyString = 1L.toString()
+        val twoKeyString = 2L.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableLongIntMap()
+        assertNotEquals(map, map2)
+
+        map2[1L] = 1
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertTrue(map.containsKey(1L))
+        assertFalse(map.containsKey(2L))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertTrue(1L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+
+        assertTrue(map.containsValue(1))
+        assertFalse(map.containsValue(3))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableLongIntMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1L] = 1
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableLongIntMap()
+        assertEquals(0, map.count())
+
+        map[1L] = 1
+        assertEquals(1, map.count())
+
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        assertEquals(2, map.count { key, _ -> key <= 2L })
+        assertEquals(0, map.count { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        assertTrue(map.any { key, _ -> key == 4L })
+        assertFalse(map.any { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableLongIntMap()
+        map[1L] = 1
+        map[2L] = 2
+        map[3L] = 3
+        map[4L] = 4
+        map[5L] = 5
+        map[6L] = 6
+
+        assertTrue(map.all { key, value -> key > 0L && value >= 1 })
+        assertFalse(map.all { key, _ -> key < 6L })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableLongIntMap()
+        assertEquals(7, map.trim())
+
+        map[1L] = 1
+        map[3L] = 3
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toInt()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toLong()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
index 45aa039..39bde8f 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongListTest.kt
@@ -22,6 +22,14 @@
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class LongListTest {
     private val list: MutableLongList = mutableLongListOf(1L, 2L, 3L, 4L, 5L)
 
@@ -80,7 +88,7 @@
 
     @Test
     fun string() {
-        assertEquals("[1, 2, 3, 4, 5]", list.toString())
+        assertEquals("[${1L}, ${2L}, ${3L}, ${4L}, ${5L}]", list.toString())
         assertEquals("[]", mutableLongListOf().toString())
     }
 
@@ -334,7 +342,7 @@
 
     @Test
     fun fold() {
-        assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -342,14 +350,14 @@
         assertEquals(
             "01-12-23-34-45-",
             list.foldIndexed("") { index, acc, i ->
-                "$acc$index$i-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
 
     @Test
     fun foldRight() {
-        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
     }
 
     @Test
@@ -357,7 +365,7 @@
         assertEquals(
             "45-34-23-12-01-",
             list.foldRightIndexed("") { index, i, acc ->
-                "$acc$index$i-"
+                "$acc$index${i.toInt()}-"
             }
         )
     }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
new file mode 100644
index 0000000..b45ae5a
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongLongMapTest.kt
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class LongLongMapTest {
+    @Test
+    fun longLongMap() {
+        val map = MutableLongLongMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyLongLongMap() {
+        val map = emptyLongLongMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyLongLongMap(), map)
+    }
+
+    @Test
+    fun longLongMapFunction() {
+        val map = mutableLongLongMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableLongLongMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longLongMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableLongLongMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longLongMapPairsFunction() {
+        val map = mutableLongLongMapOf(
+            1L to 1L,
+            2L to 2L
+        )
+        assertEquals(2, map.size)
+        assertEquals(1L, map[1L])
+        assertEquals(2L, map[2L])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableLongLongMap(12)
+        map[1L] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableLongLongMap(2)
+        map[1L] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableLongLongMap(0)
+        map[1L] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[1L] = 2L
+
+        assertEquals(1, map.size)
+        assertEquals(2L, map[1L])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableLongLongMap()
+
+        map.put(1L, 1L)
+        assertEquals(1L, map[1L])
+        map.put(1L, 2L)
+        assertEquals(2L, map[1L])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+
+        map.putAll(arrayOf(3L to 3L, 7L to 7L))
+
+        assertEquals(4, map.size)
+        assertEquals(3L, map[3L])
+        assertEquals(7L, map[7L])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableLongLongMap()
+        map += 1L to 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[1L])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableLongLongMap()
+        map += arrayOf(3L to 3L, 7L to 7L)
+
+        assertEquals(2, map.size)
+        assertEquals(3L, map[3L])
+        assertEquals(7L, map[7L])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertFailsWith<NoSuchElementException> {
+            map[2L]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertEquals(2L, map.getOrDefault(2L, 2L))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertEquals(3L, map.getOrElse(3L) { 3L })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        var counter = 0
+        map.getOrPut(1L) {
+            counter++
+            2L
+        }
+        assertEquals(1L, map[1L])
+        assertEquals(0, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            2L
+        }
+        assertEquals(2L, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            3L
+        }
+        assertEquals(2L, map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            3L
+        }
+        assertEquals(3L, map[3L])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableLongLongMap()
+        map.remove(1L)
+
+        map[1L] = 1L
+        map.remove(1L)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableLongLongMap(6)
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1L)
+        map.remove(2L)
+        map.remove(3L)
+        map.remove(4L)
+        map.remove(5L)
+        map.remove(6L)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7L] = 7L
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        map.removeIf { key, _ -> key == 1L || key == 3L }
+
+        assertEquals(4, map.size)
+        assertEquals(2L, map[2L])
+        assertEquals(4L, map[4L])
+        assertEquals(5L, map[5L])
+        assertEquals(6L, map[6L])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+
+        map -= 1L
+
+        assertEquals(2, map.size)
+        assertFalse(1L in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+
+        map -= longArrayOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+
+        map -= longSetOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+
+        map -= longListOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertFalse(3L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableLongLongMap()
+        assertFalse(map.remove(1L, 1L))
+
+        map[1L] = 1L
+        assertTrue(map.remove(1L, 1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableLongLongMap()
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toLong()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableLongLongMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toLong())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableLongLongMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toLong()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableLongLongMap()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toLong()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableLongLongMap()
+
+        for (i in 0 until 32) {
+            map[i.toLong()] = i.toLong()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableLongLongMap()
+        assertEquals("{}", map.toString())
+
+        map[1L] = 1L
+        map[2L] = 2L
+        val oneValueString = 1L.toString()
+        val twoValueString = 2L.toString()
+        val oneKeyString = 1L.toString()
+        val twoKeyString = 2L.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableLongLongMap()
+        assertNotEquals(map, map2)
+
+        map2[1L] = 1L
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertTrue(map.containsKey(1L))
+        assertFalse(map.containsKey(2L))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertTrue(1L in map)
+        assertFalse(2L in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+
+        assertTrue(map.containsValue(1L))
+        assertFalse(map.containsValue(3L))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableLongLongMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1L] = 1L
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableLongLongMap()
+        assertEquals(0, map.count())
+
+        map[1L] = 1L
+        assertEquals(1, map.count())
+
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        assertEquals(2, map.count { key, _ -> key <= 2L })
+        assertEquals(0, map.count { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        assertTrue(map.any { key, _ -> key == 4L })
+        assertFalse(map.any { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableLongLongMap()
+        map[1L] = 1L
+        map[2L] = 2L
+        map[3L] = 3L
+        map[4L] = 4L
+        map[5L] = 5L
+        map[6L] = 6L
+
+        assertTrue(map.all { key, value -> key > 0L && value >= 1L })
+        assertFalse(map.all { key, _ -> key < 6L })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableLongLongMap()
+        assertEquals(7, map.trim())
+
+        map[1L] = 1L
+        map[3L] = 3L
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toLong()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toLong()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
new file mode 100644
index 0000000..1a5fd8bf
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongObjectMapTest.kt
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class LongObjectMapTest {
+    @Test
+    fun longObjectMap() {
+        val map = MutableLongObjectMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyLongObjectMap() {
+        val map = emptyLongObjectMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyLongObjectMap<String>(), map)
+    }
+
+    @Test
+    fun longObjectMapFunction() {
+        val map = mutableLongObjectMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableLongObjectMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longObjectMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableLongObjectMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun longObjectMapPairsFunction() {
+        val map = mutableLongObjectMapOf(
+            1L to "World",
+            2L to "Monde"
+        )
+        assertEquals(2, map.size)
+        assertEquals("World", map[1L])
+        assertEquals("Monde", map[2L])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun insertIndex0() {
+        val map = MutableLongObjectMap<String>()
+        map.put(1L, "World")
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableLongObjectMap<String>(12)
+        map[1L] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableLongObjectMap<String>(2)
+        map[1L] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableLongObjectMap<String>(0)
+        map[1L] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[1L] = "Monde"
+
+        assertEquals(1, map.size)
+        assertEquals("Monde", map[1L])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableLongObjectMap<String?>()
+
+        assertNull(map.put(1L, "World"))
+        assertEquals("World", map.put(1L, "Monde"))
+        assertNull(map.put(2L, null))
+        assertNull(map.put(2L, "Monde"))
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        map.putAll(arrayOf(3L to "Welt", 7L to "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3L])
+        assertEquals("Mundo", map[7L])
+    }
+
+    @Test
+    fun putAllMap() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        map.putAll(mutableLongObjectMapOf(3L to "Welt", 7L to "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3L])
+        assertEquals("Mundo", map[7L])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableLongObjectMap<String>()
+        map += 1L to "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1L])
+    }
+
+    @Test
+    fun plusMap() {
+        val map = MutableLongObjectMap<String>()
+        map += longObjectMapOf(3L to "Welt", 7L to "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3L])
+        assertEquals("Mundo", map[7L])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableLongObjectMap<String>()
+        map += arrayOf(3L to "Welt", 7L to "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3L])
+        assertEquals("Mundo", map[7L])
+    }
+
+    @Test
+    fun nullValue() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = null
+
+        assertEquals(1, map.size)
+        assertNull(map[1L])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+
+        assertNull(map[2L])
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+
+        assertEquals("Monde", map.getOrDefault(2L, "Monde"))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertEquals("Monde", map.getOrElse(2L) { "Monde" })
+        assertEquals("Welt", map.getOrElse(3L) { "Welt" })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+
+        var counter = 0
+        map.getOrPut(1L) {
+            counter++
+            "Monde"
+        }
+        assertEquals("World", map[1L])
+        assertEquals(0, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            "Monde"
+        }
+        assertEquals("Monde", map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(2L) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Monde", map[2L])
+        assertEquals(1, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            null
+        }
+        assertNull(map[3L])
+        assertEquals(2, counter)
+
+        map.getOrPut(3L) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Welt", map[3L])
+        assertEquals(3, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableLongObjectMap<String?>()
+        assertNull(map.remove(1L))
+
+        map[1L] = "World"
+        assertEquals("World", map.remove(1L))
+        assertEquals(0, map.size)
+
+        map[1L] = null
+        assertNull(map.remove(1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableLongObjectMap<String>(6)
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1L)
+        map.remove(2L)
+        map.remove(3L)
+        map.remove(4L)
+        map.remove(5L)
+        map.remove(6L)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7L] = "Mundo"
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        map.removeIf { key, value ->
+            key == 1L || key == 3L || value.startsWith('S')
+        }
+
+        assertEquals(2, map.size)
+        assertEquals("Monde", map[2L])
+        assertEquals("Mondo", map[5L])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+
+        map -= 1L
+
+        assertEquals(2, map.size)
+        assertNull(map[1L])
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+
+        map -= longArrayOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertNull(map[3L])
+        assertNull(map[2L])
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+
+        map -= longSetOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertNull(map[3L])
+        assertNull(map[2L])
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+
+        map -= longListOf(3L, 2L)
+
+        assertEquals(1, map.size)
+        assertNull(map[3L])
+        assertNull(map[2L])
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableLongObjectMap<String?>()
+        assertFalse(map.remove(1L, "World"))
+
+        map[1L] = "World"
+        assertTrue(map.remove(1L, "World"))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableLongObjectMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toLong()] = i.toString()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableLongObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key.toInt().toString(), value)
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableLongObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertEquals(key.toInt().toString(), map[key])
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableLongObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toLong()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachValue { value ->
+                assertNotNull(value.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableLongObjectMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toLong()] = i.toString()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableLongObjectMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map[1L] = "World"
+        map[2L] = "Monde"
+        val oneKey = 1L.toString()
+        val twoKey = 2L.toString()
+        assertTrue(
+            "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+                "{$twoKey=Monde, $oneKey=World}" == map.toString()
+        )
+
+        map.clear()
+        map[1L] = null
+        assertEquals("{$oneKey=null}", map.toString())
+
+        val selfAsValueMap = MutableLongObjectMap<Any>()
+        selfAsValueMap[1L] = selfAsValueMap
+        assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableLongObjectMap<String?>()
+        map2[2L] = null
+
+        assertNotEquals(map, map2)
+
+        map2[1L] = "World"
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertTrue(map.containsKey(1L))
+        assertFalse(map.containsKey(3L))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertTrue(1L in map)
+        assertFalse(3L in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableLongObjectMap<String?>()
+        map[1L] = "World"
+        map[2L] = null
+
+        assertTrue(map.containsValue("World"))
+        assertTrue(map.containsValue(null))
+        assertFalse(map.containsValue("Monde"))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableLongObjectMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1L] = "World"
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableLongObjectMap<String>()
+        assertEquals(0, map.count())
+
+        map[1L] = "World"
+        assertEquals(1, map.count())
+
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        assertEquals(2, map.count { key, _ -> key < 3L })
+        assertEquals(0, map.count { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        assertTrue(map.any { key, _ -> key > 5L })
+        assertFalse(map.any { key, _ -> key < 0L })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableLongObjectMap<String>()
+        map[1L] = "World"
+        map[2L] = "Monde"
+        map[3L] = "Welt"
+        map[4L] = "Sekai"
+        map[5L] = "Mondo"
+        map[6L] = "Sesang"
+
+        assertTrue(map.all { key, value -> key < 7L && value.length > 0 })
+        assertFalse(map.all { key, _ -> key < 6L })
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
index 1278fcf..2ee0e96 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/LongSetTest.kt
@@ -23,6 +23,14 @@
 import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
 class LongSetTest {
     @Test
     fun emptyLongSetConstructor() {
@@ -147,9 +155,9 @@
         val set = MutableLongSet()
         set += 1L
         set += 2L
-        var element: Long = Long.MIN_VALUE
-        var otherElement: Long = Long.MIN_VALUE
-        set.forEach { if (element == Long.MIN_VALUE) element = it else otherElement = it }
+        var element: Long = -1L
+        var otherElement: Long = -1L
+        set.forEach { if (element == -1L) element = it else otherElement = it }
         assertEquals(element, set.first())
         set -= element
         assertEquals(otherElement, set.first())
@@ -333,8 +341,8 @@
         set += 1L
         set += 5L
         assertTrue(
-            "[1, 5]" == set.toString() ||
-                "[5, 1]" == set.toString()
+            "[${1L}, ${5L}]" == set.toString() ||
+                "[${5L}, ${1L}]" == set.toString()
         )
     }
 
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
new file mode 100644
index 0000000..3ae3e6e
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectFloatMapTest.kt
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectFloatTest {
+    @Test
+    fun objectFloatMap() {
+        val map = MutableObjectFloatMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun emptyObjectFloatMap() {
+        val map = emptyObjectFloatMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyObjectFloatMap<String>(), map)
+    }
+
+    @Test
+    fun objectFloatMapFunction() {
+        val map = mutableObjectFloatMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableObjectFloatMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectFloatMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableObjectFloatMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectFloatMapPairsFunction() {
+        val map = mutableObjectFloatMapOf(
+            "Hello" to 1f,
+            "Bonjour" to 2f
+        )
+        assertEquals(2, map.size)
+        assertEquals(1f, map["Hello"])
+        assertEquals(2f, map["Bonjour"])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableObjectFloatMap<String>(12)
+        map["Hello"] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableObjectFloatMap<String>(2)
+        map["Hello"] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableObjectFloatMap<String>(0)
+        map["Hello"] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Hello"] = 2f
+
+        assertEquals(1, map.size)
+        assertEquals(2f, map["Hello"])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableObjectFloatMap<String>()
+
+        map.put("Hello", 1f)
+        assertEquals(1f, map["Hello"])
+        map.put("Hello", 2f)
+        assertEquals(2f, map["Hello"])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+        map["Bonjour"] = 2f
+
+        map.putAll(arrayOf("Hallo" to 3f, "Hola" to 7f))
+
+        assertEquals(5, map.size)
+        assertEquals(3f, map["Hallo"])
+        assertEquals(7f, map["Hola"])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableObjectFloatMap<String>()
+        map += "Hello" to 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map["Hello"])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableObjectFloatMap<String>()
+        map += arrayOf("Hallo" to 3f, "Hola" to 7f)
+
+        assertEquals(2, map.size)
+        assertEquals(3f, map["Hallo"])
+        assertEquals(7f, map["Hola"])
+    }
+
+    @Test
+    fun nullKey() {
+        val map = MutableObjectFloatMap<String?>()
+        map[null] = 1f
+
+        assertEquals(1, map.size)
+        assertEquals(1f, map[null])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        assertFailsWith<NoSuchElementException> {
+            map["Bonjour"]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        assertEquals(2f, map.getOrDefault("Bonjour", 2f))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        assertEquals(3f, map.getOrElse("Hallo") { 3f })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+
+        var counter = 0
+        map.getOrPut("Hello") {
+            counter++
+            2f
+        }
+        assertEquals(1f, map["Hello"])
+        assertEquals(0, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            2f
+        }
+        assertEquals(2f, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            3f
+        }
+        assertEquals(2f, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Hallo") {
+            counter++
+            3f
+        }
+        assertEquals(3f, map["Hallo"])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableObjectFloatMap<String?>()
+        map.remove("Hello")
+
+        map["Hello"] = 1f
+        map.remove("Hello")
+        assertEquals(0, map.size)
+
+        map[null] = 1f
+        map.remove(null)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableObjectFloatMap<String>(6)
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove("Hello")
+        map.remove("Bonjour")
+        map.remove("Hallo")
+        map.remove("Konnichiwa")
+        map.remove("Ciao")
+        map.remove("Annyeong")
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map["Hola"] = 7f
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        map.removeIf { key, _ -> key.startsWith('H') }
+
+        assertEquals(4, map.size)
+        assertEquals(2f, map["Bonjour"])
+        assertEquals(4f, map["Konnichiwa"])
+        assertEquals(5f, map["Ciao"])
+        assertEquals(6f, map["Annyeong"])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+
+        map -= "Hello"
+
+        assertEquals(2, map.size)
+        assertFalse("Hello" in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+
+        map -= arrayOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusIterable() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+
+        map -= listOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusSequence() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+
+        map -= listOf("Hallo", "Bonjour").asSequence()
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableObjectFloatMap<String?>()
+        assertFalse(map.remove("Hello", 1f))
+
+        map["Hello"] = 1f
+        assertTrue(map.remove("Hello", 1f))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableObjectFloatMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toFloat()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableObjectFloatMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt().toString())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableObjectFloatMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toFloat()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertNotNull(key.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableObjectFloatMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toFloat()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableObjectFloatMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toString()] = i.toFloat()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableObjectFloatMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        val oneString = 1f.toString()
+        val twoString = 2f.toString()
+        assertTrue(
+            "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+                "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+        )
+
+        map.clear()
+        map[null] = 2f
+        assertEquals("{null=$twoString}", map.toString())
+
+        val selfAsKeyMap = MutableObjectFloatMap<Any>()
+        selfAsKeyMap[selfAsKeyMap] = 1f
+        assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableObjectFloatMap<String?>()
+        map2[null] = 2f
+
+        assertNotEquals(map, map2)
+
+        map2["Hello"] = 1f
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+
+        assertTrue(map.containsKey("Hello"))
+        assertTrue(map.containsKey(null))
+        assertFalse(map.containsKey("Bonjour"))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+
+        assertTrue("Hello" in map)
+        assertTrue(null in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableObjectFloatMap<String?>()
+        map["Hello"] = 1f
+        map[null] = 2f
+
+        assertTrue(map.containsValue(1f))
+        assertTrue(map.containsValue(2f))
+        assertFalse(map.containsValue(3f))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableObjectFloatMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map["Hello"] = 1f
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableObjectFloatMap<String>()
+        assertEquals(0, map.count())
+
+        map["Hello"] = 1f
+        assertEquals(1, map.count())
+
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+        assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        assertTrue(map.any { key, _ -> key.startsWith("K") })
+        assertFalse(map.any { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableObjectFloatMap<String>()
+        map["Hello"] = 1f
+        map["Bonjour"] = 2f
+        map["Hallo"] = 3f
+        map["Konnichiwa"] = 4f
+        map["Ciao"] = 5f
+        map["Annyeong"] = 6f
+
+        assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+        assertFalse(map.all { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableObjectFloatMap<String>()
+        assertEquals(7, map.trim())
+
+        map["Hello"] = 1f
+        map["Hallo"] = 3f
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toFloat()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toString()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
new file mode 100644
index 0000000..d3bf7c7
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectIntMapTest.kt
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectIntTest {
+    @Test
+    fun objectIntMap() {
+        val map = MutableObjectIntMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun emptyObjectIntMap() {
+        val map = emptyObjectIntMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyObjectIntMap<String>(), map)
+    }
+
+    @Test
+    fun objectIntMapFunction() {
+        val map = mutableObjectIntMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableObjectIntMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectIntMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableObjectIntMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectIntMapPairsFunction() {
+        val map = mutableObjectIntMapOf(
+            "Hello" to 1,
+            "Bonjour" to 2
+        )
+        assertEquals(2, map.size)
+        assertEquals(1, map["Hello"])
+        assertEquals(2, map["Bonjour"])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableObjectIntMap<String>(12)
+        map["Hello"] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableObjectIntMap<String>(2)
+        map["Hello"] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableObjectIntMap<String>(0)
+        map["Hello"] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Hello"] = 2
+
+        assertEquals(1, map.size)
+        assertEquals(2, map["Hello"])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableObjectIntMap<String>()
+
+        map.put("Hello", 1)
+        assertEquals(1, map["Hello"])
+        map.put("Hello", 2)
+        assertEquals(2, map["Hello"])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+        map["Bonjour"] = 2
+
+        map.putAll(arrayOf("Hallo" to 3, "Hola" to 7))
+
+        assertEquals(5, map.size)
+        assertEquals(3, map["Hallo"])
+        assertEquals(7, map["Hola"])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableObjectIntMap<String>()
+        map += "Hello" to 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map["Hello"])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableObjectIntMap<String>()
+        map += arrayOf("Hallo" to 3, "Hola" to 7)
+
+        assertEquals(2, map.size)
+        assertEquals(3, map["Hallo"])
+        assertEquals(7, map["Hola"])
+    }
+
+    @Test
+    fun nullKey() {
+        val map = MutableObjectIntMap<String?>()
+        map[null] = 1
+
+        assertEquals(1, map.size)
+        assertEquals(1, map[null])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        assertFailsWith<NoSuchElementException> {
+            map["Bonjour"]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        assertEquals(2, map.getOrDefault("Bonjour", 2))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        assertEquals(3, map.getOrElse("Hallo") { 3 })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+
+        var counter = 0
+        map.getOrPut("Hello") {
+            counter++
+            2
+        }
+        assertEquals(1, map["Hello"])
+        assertEquals(0, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            2
+        }
+        assertEquals(2, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            3
+        }
+        assertEquals(2, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Hallo") {
+            counter++
+            3
+        }
+        assertEquals(3, map["Hallo"])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableObjectIntMap<String?>()
+        map.remove("Hello")
+
+        map["Hello"] = 1
+        map.remove("Hello")
+        assertEquals(0, map.size)
+
+        map[null] = 1
+        map.remove(null)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableObjectIntMap<String>(6)
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove("Hello")
+        map.remove("Bonjour")
+        map.remove("Hallo")
+        map.remove("Konnichiwa")
+        map.remove("Ciao")
+        map.remove("Annyeong")
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map["Hola"] = 7
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        map.removeIf { key, _ -> key.startsWith('H') }
+
+        assertEquals(4, map.size)
+        assertEquals(2, map["Bonjour"])
+        assertEquals(4, map["Konnichiwa"])
+        assertEquals(5, map["Ciao"])
+        assertEquals(6, map["Annyeong"])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+
+        map -= "Hello"
+
+        assertEquals(2, map.size)
+        assertFalse("Hello" in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+
+        map -= arrayOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusIterable() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+
+        map -= listOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusSequence() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+
+        map -= listOf("Hallo", "Bonjour").asSequence()
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableObjectIntMap<String?>()
+        assertFalse(map.remove("Hello", 1))
+
+        map["Hello"] = 1
+        assertTrue(map.remove("Hello", 1))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableObjectIntMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toInt()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableObjectIntMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt().toString())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableObjectIntMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toInt()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertNotNull(key.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableObjectIntMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toInt()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableObjectIntMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toString()] = i.toInt()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableObjectIntMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        val oneString = 1.toString()
+        val twoString = 2.toString()
+        assertTrue(
+            "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+                "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+        )
+
+        map.clear()
+        map[null] = 2
+        assertEquals("{null=$twoString}", map.toString())
+
+        val selfAsKeyMap = MutableObjectIntMap<Any>()
+        selfAsKeyMap[selfAsKeyMap] = 1
+        assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableObjectIntMap<String?>()
+        map2[null] = 2
+
+        assertNotEquals(map, map2)
+
+        map2["Hello"] = 1
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+
+        assertTrue(map.containsKey("Hello"))
+        assertTrue(map.containsKey(null))
+        assertFalse(map.containsKey("Bonjour"))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+
+        assertTrue("Hello" in map)
+        assertTrue(null in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableObjectIntMap<String?>()
+        map["Hello"] = 1
+        map[null] = 2
+
+        assertTrue(map.containsValue(1))
+        assertTrue(map.containsValue(2))
+        assertFalse(map.containsValue(3))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableObjectIntMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map["Hello"] = 1
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableObjectIntMap<String>()
+        assertEquals(0, map.count())
+
+        map["Hello"] = 1
+        assertEquals(1, map.count())
+
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+        assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        assertTrue(map.any { key, _ -> key.startsWith("K") })
+        assertFalse(map.any { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableObjectIntMap<String>()
+        map["Hello"] = 1
+        map["Bonjour"] = 2
+        map["Hallo"] = 3
+        map["Konnichiwa"] = 4
+        map["Ciao"] = 5
+        map["Annyeong"] = 6
+
+        assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+        assertFalse(map.all { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableObjectIntMap<String>()
+        assertEquals(7, map.trim())
+
+        map["Hello"] = 1
+        map["Hallo"] = 3
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toInt()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toString()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt
new file mode 100644
index 0000000..d524dcf
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectListTest.kt
@@ -0,0 +1,1312 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class ObjectListTest {
+    private val list: MutableObjectList<Int> = mutableObjectListOf(1, 2, 3, 4, 5)
+
+    @Test
+    fun emptyConstruction() {
+        val l = mutableObjectListOf<Int>()
+        assertEquals(0, l.size)
+        assertEquals(16, l.capacity)
+    }
+
+    @Test
+    fun sizeConstruction() {
+        val l = MutableObjectList<Int>(4)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun contentConstruction() {
+        val l = mutableObjectListOf(1, 2, 3)
+        assertEquals(3, l.size)
+        assertEquals(1, l[0])
+        assertEquals(2, l[1])
+        assertEquals(3, l[2])
+        assertEquals(3, l.capacity)
+        repeat(2) {
+            val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+            assertEquals(list, l2)
+            l2.removeAt(0)
+        }
+    }
+
+    @Test
+    fun hashCodeTest() {
+        val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.removeAt(4)
+        assertNotEquals(list.hashCode(), l2.hashCode())
+        l2.add(5)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.clear()
+        assertNotEquals(list.hashCode(), l2.hashCode())
+    }
+
+    @Test
+    fun equalsTest() {
+        val l2 = mutableObjectListOf(1, 2, 3, 4, 5)
+        assertEquals(list, l2)
+        assertNotEquals(list, mutableObjectListOf())
+        l2.removeAt(4)
+        assertNotEquals(list, l2)
+        l2.add(5)
+        assertEquals(list, l2)
+        l2.clear()
+        assertNotEquals(list, l2)
+    }
+
+    @Test
+    fun string() {
+        assertEquals("[1, 2, 3, 4, 5]", list.toString())
+        assertEquals("[]", mutableObjectListOf<Int>().toString())
+    }
+
+    @Test
+    fun size() {
+        assertEquals(5, list.size)
+        assertEquals(5, list.count())
+        val l2 = mutableObjectListOf<Int>()
+        assertEquals(0, l2.size)
+        assertEquals(0, l2.count())
+        l2 += 1
+        assertEquals(1, l2.size)
+        assertEquals(1, l2.count())
+    }
+
+    @Test
+    fun get() {
+        assertEquals(1, list[0])
+        assertEquals(5, list[4])
+        assertEquals(1, list.elementAt(0))
+        assertEquals(5, list.elementAt(4))
+    }
+
+    @Test
+    fun getOutOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[5]
+        }
+    }
+
+    @Test
+    fun getOutOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[-1]
+        }
+    }
+
+    @Test
+    fun elementAtOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(5)
+        }
+    }
+
+    @Test
+    fun elementAtOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(-1)
+        }
+    }
+
+    @Test
+    fun elementAtOrElse() {
+        assertEquals(1, list.elementAtOrElse(0) {
+            assertEquals(0, it)
+            0
+        })
+        assertEquals(0, list.elementAtOrElse(-1) {
+            assertEquals(-1, it)
+            0
+        })
+        assertEquals(0, list.elementAtOrElse(5) {
+            assertEquals(5, it)
+            0
+        })
+    }
+
+    @Test
+    fun count() {
+        assertEquals(1, list.count { it < 2 })
+        assertEquals(0, list.count { it < 0 })
+        assertEquals(5, list.count { it < 10 })
+    }
+
+    @Test
+    fun isEmpty() {
+        assertFalse(list.isEmpty())
+        assertFalse(list.none())
+        assertTrue(mutableObjectListOf<Int>().isEmpty())
+        assertTrue(mutableObjectListOf<Int>().none())
+    }
+
+    @Test
+    fun isNotEmpty() {
+        assertTrue(list.isNotEmpty())
+        assertTrue(list.any())
+        assertFalse(mutableObjectListOf<Int>().isNotEmpty())
+    }
+
+    @Test
+    fun indices() {
+        assertEquals(IntRange(0, 4), list.indices)
+        assertEquals(IntRange(0, -1), mutableObjectListOf<Int>().indices)
+    }
+
+    @Test
+    fun any() {
+        assertTrue(list.any { it == 5 })
+        assertTrue(list.any { it == 1 })
+        assertFalse(list.any { it == 0 })
+    }
+
+    @Test
+    fun reversedAny() {
+        val reversedList = mutableObjectListOf<Int>()
+        assertFalse(
+            list.reversedAny {
+                reversedList.add(it)
+                false
+            }
+        )
+        val reversedContent = mutableObjectListOf(5, 4, 3, 2, 1)
+        assertEquals(reversedContent, reversedList)
+
+        val reversedSublist = mutableObjectListOf<Int>()
+        assertTrue(
+            list.reversedAny {
+                reversedSublist.add(it)
+                reversedSublist.size == 2
+            }
+        )
+        assertEquals(reversedSublist, mutableObjectListOf(5, 4))
+    }
+
+    @Test
+    fun forEach() {
+        val copy = mutableObjectListOf<Int>()
+        list.forEach { copy += it }
+        assertEquals(list, copy)
+    }
+
+    @Test
+    fun forEachReversed() {
+        val copy = mutableObjectListOf<Int>()
+        list.forEachReversed { copy += it }
+        assertEquals(copy, mutableObjectListOf(5, 4, 3, 2, 1))
+    }
+
+    @Test
+    fun forEachIndexed() {
+        val copy = mutableObjectListOf<Int>()
+        val indices = mutableObjectListOf<Int>()
+        list.forEachIndexed { index, open ->
+            copy += open
+            indices += index
+        }
+        assertEquals(list, copy)
+        assertEquals(indices, mutableObjectListOf(0, 1, 2, 3, 4))
+    }
+
+    @Test
+    fun forEachReversedIndexed() {
+        val copy = mutableObjectListOf<Int>()
+        val indices = mutableObjectListOf<Int>()
+        list.forEachReversedIndexed { index, open ->
+            copy += open
+            indices += index
+        }
+        assertEquals(copy, mutableObjectListOf(5, 4, 3, 2, 1))
+        assertEquals(indices, mutableObjectListOf(4, 3, 2, 1, 0))
+    }
+
+    @Test
+    fun indexOfFirst() {
+        assertEquals(0, list.indexOfFirst { it < 2 })
+        assertEquals(4, list.indexOfFirst { it > 4 })
+        assertEquals(-1, list.indexOfFirst { it < 0 })
+        assertEquals(0, mutableObjectListOf(8, 8).indexOfFirst { it > 7 })
+    }
+
+    @Test
+    fun firstOrNullNoParam() {
+        assertEquals(1, list.firstOrNull())
+        assertNull(emptyObjectList<Int>().firstOrNull())
+    }
+
+    @Test
+    fun firstOrNull() {
+        assertEquals(1, list.firstOrNull { it < 5 })
+        assertEquals(3, list.firstOrNull { it > 2 })
+        assertEquals(5, list.firstOrNull { it > 4 })
+        assertNull(list.firstOrNull { it > 5 })
+    }
+
+    @Test
+    fun lastOrNullNoParam() {
+        assertEquals(5, list.lastOrNull())
+        assertNull(emptyObjectList<Int>().lastOrNull())
+    }
+
+    @Test
+    fun lastOrNull() {
+        assertEquals(4, list.lastOrNull { it < 5 })
+        assertEquals(5, list.lastOrNull { it > 2 })
+        assertEquals(1, list.lastOrNull { it < 2 })
+        assertNull(list.firstOrNull { it > 5 })
+    }
+
+    @Test
+    fun indexOfLast() {
+        assertEquals(0, list.indexOfLast { it < 2 })
+        assertEquals(4, list.indexOfLast { it > 4 })
+        assertEquals(-1, list.indexOfLast { it < 0 })
+        assertEquals(1, objectListOf(8, 8).indexOfLast { it > 7 })
+    }
+
+    @Test
+    fun contains() {
+        assertTrue(list.contains(5))
+        assertTrue(list.contains(1))
+        assertFalse(list.contains(0))
+    }
+
+    @Test
+    fun containsAllList() {
+        assertTrue(list.containsAll(mutableObjectListOf(2, 3, 1)))
+        assertFalse(list.containsAll(mutableObjectListOf(2, 3, 6)))
+    }
+
+    @Test
+    fun lastIndexOf() {
+        assertEquals(4, list.lastIndexOf(5))
+        assertEquals(1, list.lastIndexOf(2))
+        val copy = mutableObjectListOf<Int>()
+        copy.addAll(list)
+        copy.addAll(list)
+        assertEquals(5, copy.lastIndexOf(1))
+    }
+
+    @Test
+    fun first() {
+        assertEquals(1, list.first())
+    }
+
+    @Test
+    fun firstException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableObjectListOf<Int>().first()
+        }
+    }
+
+    @Test
+    fun firstWithPredicate() {
+        assertEquals(5, list.first { it > 4 })
+        assertEquals(1, mutableObjectListOf(1, 5).first { it > 0 })
+    }
+
+    @Test
+    fun firstWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableObjectListOf<Int>().first { it > 8 }
+        }
+    }
+
+    @Test
+    fun last() {
+        assertEquals(5, list.last())
+    }
+
+    @Test
+    fun lastException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutableObjectListOf<Int>().last()
+        }
+    }
+
+    @Test
+    fun lastWithPredicate() {
+        assertEquals(1, list.last { it < 2 })
+        assertEquals(5, objectListOf(1, 5).last { it > 0 })
+    }
+
+    @Test
+    fun lastWithPredicateException() {
+        assertFailsWith<NoSuchElementException> {
+            objectListOf(2).last { it > 2 }
+        }
+    }
+
+    @Test
+    fun fold() {
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toString() })
+    }
+
+    @Test
+    fun foldIndexed() {
+        assertEquals(
+            "01-12-23-34-45-",
+            list.foldIndexed("") { index, acc, i ->
+                "$acc$index$i-"
+            }
+        )
+    }
+
+    @Test
+    fun foldRight() {
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toString() })
+    }
+
+    @Test
+    fun foldRightIndexed() {
+        assertEquals(
+            "45-34-23-12-01-",
+            list.foldRightIndexed("") { index, i, acc ->
+                "$acc$index$i-"
+            }
+        )
+    }
+
+    @Test
+    fun add() {
+        val l = mutableObjectListOf(1, 2, 3)
+        l += 4
+        l.add(5)
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun addAtIndex() {
+        val l = mutableObjectListOf(2, 4)
+        l.add(2, 5)
+        l.add(0, 1)
+        l.add(2, 3)
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(-1, 2)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(6, 2)
+        }
+    }
+
+    @Test
+    fun addAllListAtIndex() {
+        val l = mutableObjectListOf(4)
+        val l2 = mutableObjectListOf(1, 2)
+        val l3 = mutableObjectListOf(5)
+        val l4 = mutableObjectListOf(3)
+        assertTrue(l4.addAll(1, l3))
+        assertTrue(l4.addAll(0, l2))
+        assertTrue(l4.addAll(3, l))
+        assertFalse(l4.addAll(0, mutableObjectListOf()))
+        assertEquals(list, l4)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(6, mutableObjectListOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(-1, mutableObjectListOf())
+        }
+    }
+
+    @Test
+    fun addAllObjectList() {
+        val l = MutableObjectList<Int>()
+        l.add(3)
+        l.add(4)
+        l.add(5)
+        val l2 = mutableObjectListOf(1, 2)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableObjectListOf()))
+    }
+
+    @Test
+    fun addAllList() {
+        val l = listOf(3, 4, 5)
+        val l2 = mutableObjectListOf(1, 2)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableObjectListOf()))
+    }
+
+    @Test
+    fun addAllIterable() {
+        val l = listOf(3, 4, 5) as Iterable<Int>
+        val l2 = mutableObjectListOf(1, 2)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableObjectListOf()))
+    }
+
+    @Test
+    fun addAllSequence() {
+        val l = listOf(3, 4, 5).asSequence()
+        val l2 = mutableObjectListOf(1, 2)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutableObjectListOf()))
+    }
+
+    @Test
+    fun plusAssignObjectList() {
+        val l = objectListOf(3, 4, 5)
+        val l2 = mutableObjectListOf(1, 2)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun plusAssignIterable() {
+        val l = listOf(3, 4, 5) as Iterable<Int>
+        val l2 = mutableObjectListOf(1, 2)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun plusAssignSequence() {
+        val l = arrayOf(3, 4, 5).asSequence()
+        val l2 = mutableObjectListOf(1, 2)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun addAllArrayAtIndex() {
+        val a1 = arrayOf(4)
+        val a2 = arrayOf(1, 2)
+        val a3 = arrayOf(5)
+        val l = mutableObjectListOf(3)
+        assertTrue(l.addAll(1, a3))
+        assertTrue(l.addAll(0, a2))
+        assertTrue(l.addAll(3, a1))
+        assertFalse(l.addAll(0, arrayOf()))
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(6, arrayOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(-1, arrayOf())
+        }
+    }
+
+    @Test
+    fun addAllArray() {
+        val a = arrayOf(3, 4, 5)
+        val v = mutableObjectListOf(1, 2)
+        v.addAll(a)
+        assertEquals(5, v.size)
+        assertEquals(3, v[2])
+        assertEquals(4, v[3])
+        assertEquals(5, v[4])
+    }
+
+    @Test
+    fun plusAssignArray() {
+        val a = arrayOf(3, 4, 5)
+        val v = mutableObjectListOf(1, 2)
+        v += a
+        assertEquals(list, v)
+    }
+
+    @Test
+    fun clear() {
+        val l = mutableObjectListOf<Int>()
+        l.addAll(list)
+        assertTrue(l.isNotEmpty())
+        l.clear()
+        assertTrue(l.isEmpty())
+        repeat(5) { index ->
+            assertNull(l.content[index])
+        }
+    }
+
+    @Test
+    fun trim() {
+        val l = mutableObjectListOf(1)
+        l.trim()
+        assertEquals(1, l.capacity)
+        l += arrayOf(1, 2, 3, 4, 5)
+        l.trim()
+        assertEquals(6, l.capacity)
+        assertEquals(6, l.size)
+        l.clear()
+        l.trim()
+        assertEquals(0, l.capacity)
+        l.trim(100)
+        assertEquals(0, l.capacity)
+        l += arrayOf(1, 2, 3, 4, 5)
+        l -= 5
+        l.trim(5)
+        assertEquals(5, l.capacity)
+        l.trim(4)
+        assertEquals(4, l.capacity)
+        l.trim(3)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun remove() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        l.remove(3)
+        assertEquals(mutableObjectListOf(1, 2, 4, 5), l)
+    }
+
+    @Test
+    fun removeIf() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5, 6)
+        l.removeIf { it == 100 }
+        assertEquals(objectListOf(1, 2, 3, 4, 5, 6), l)
+        l.removeIf { it % 2 == 0 }
+        assertEquals(objectListOf(1, 3, 5), l)
+        repeat(3) {
+            assertNull(l.content[3 + it])
+        }
+        l.removeIf { it != 3 }
+        assertEquals(objectListOf(3), l)
+    }
+
+    @Test
+    fun removeAt() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        l.removeAt(2)
+        assertNull(l.content[4])
+        assertEquals(mutableObjectListOf(1, 2, 4, 5), l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(6)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun set() {
+        val l = mutableObjectListOf(0, 0, 0, 0, 0)
+        l[0] = 1
+        l[4] = 5
+        l[2] = 3
+        l[1] = 2
+        l[3] = 4
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l[-1] = 1
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l[6] = 1
+        }
+        assertEquals(4, l.set(3, 1));
+    }
+
+    @Test
+    fun ensureCapacity() {
+        val l = mutableObjectListOf(1)
+        assertEquals(1, l.capacity)
+        l.ensureCapacity(5)
+        assertEquals(5, l.capacity)
+    }
+
+    @Test
+    fun removeAllObjectList() {
+        assertFalse(list.removeAll(mutableObjectListOf(0, 10, 15)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(mutableObjectListOf(20, 0, 15, 10, 5)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllScatterSet() {
+        assertFalse(list.removeAll(scatterSetOf(0, 10, 15)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(scatterSetOf(20, 0, 15, 10, 5)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllArray() {
+        assertFalse(list.removeAll(arrayOf(0, 10, 15)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(arrayOf(20, 0, 15, 10, 5)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllList() {
+        assertFalse(list.removeAll(listOf(0, 10, 15)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllIterable() {
+        assertFalse(list.removeAll(listOf(0, 10, 15) as Iterable<Int>))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5) as Iterable<Int>))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllSequence() {
+        assertFalse(list.removeAll(listOf(0, 10, 15).asSequence()))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        assertTrue(l.removeAll(listOf(20, 0, 15, 10, 5).asSequence()))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun minusAssignObjectList() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= mutableObjectListOf(0, 10, 15)
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= mutableObjectListOf(20, 0, 15, 10, 5)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignScatterSet() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= scatterSetOf(0, 10, 15)
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= scatterSetOf(20, 0, 15, 10, 5)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignArray() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= arrayOf(0, 10, 15)
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= arrayOf(20, 0, 15, 10, 5)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignList() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= listOf(0, 10, 15)
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= listOf(20, 0, 15, 10, 5)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignIterable() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= listOf(0, 10, 15) as Iterable<Int>
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= listOf(20, 0, 15, 10, 5) as Iterable<Int>
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignSequence() {
+        val l = mutableObjectListOf<Int>().also { it += list }
+        l -= listOf(0, 10, 15).asSequence()
+        assertEquals(list, l)
+        val l2 = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20, 5)
+        l2 -= listOf(20, 0, 15, 10, 5).asSequence()
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun retainAll() {
+        assertFalse(list.retainAll(mutableObjectListOf(1, 2, 3, 4, 5, 6)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(mutableObjectListOf(1, 2, 3, 4, 5, 6)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllArray() {
+        assertFalse(list.retainAll(arrayOf(1, 2, 3, 4, 5, 6)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(arrayOf(1, 2, 3, 4, 5, 6)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllCollection() {
+        assertFalse(list.retainAll(listOf(1, 2, 3, 4, 5, 6)))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(listOf(1, 2, 3, 4, 5, 6)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllIterable() {
+        assertFalse(list.retainAll(listOf(1, 2, 3, 4, 5, 6) as Iterable<Int>))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(listOf(1, 2, 3, 4, 5, 6) as Iterable<Int>))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllSequence() {
+        assertFalse(list.retainAll(arrayOf(1, 2, 3, 4, 5, 6).asSequence()))
+        val l = mutableObjectListOf(0, 1, 15, 10, 2, 3, 4, 5, 20)
+        assertTrue(l.retainAll(arrayOf(1, 2, 3, 4, 5, 6).asSequence()))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeRange() {
+        val l = mutableObjectListOf(1, 9, 7, 6, 2, 3, 4, 5)
+        l.removeRange(1, 4)
+        assertNull(l.content[5])
+        assertNull(l.content[6])
+        assertNull(l.content[7])
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(6, 6)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(100, 200)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(-1, 0)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            l.removeRange(3, 2)
+        }
+    }
+
+    @Test
+    fun testEmptyObjectList() {
+        val l = emptyObjectList<Int>()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun objectListOfEmpty() {
+        val l = objectListOf<Int>()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun objectListOfOneValue() {
+        val l = objectListOf(2)
+        assertEquals(1, l.size)
+        assertEquals(2, l[0])
+    }
+
+    @Test
+    fun objectListOfTwoValues() {
+        val l = objectListOf(2, 1)
+        assertEquals(2, l.size)
+        assertEquals(2, l[0])
+        assertEquals(1, l[1])
+    }
+
+    @Test
+    fun objectListOfThreeValues() {
+        val l = objectListOf(2, 10, -1)
+        assertEquals(3, l.size)
+        assertEquals(2, l[0])
+        assertEquals(10, l[1])
+        assertEquals(-1, l[2])
+    }
+
+    @Test
+    fun objectListOfFourValues() {
+        val l = objectListOf(2, 10, -1, 10)
+        assertEquals(4, l.size)
+        assertEquals(2, l[0])
+        assertEquals(10, l[1])
+        assertEquals(-1, l[2])
+        assertEquals(10, l[3])
+    }
+
+    @Test
+    fun mutableObjectListOfOneValue() {
+        val l = mutableObjectListOf(2)
+        assertEquals(1, l.size)
+        assertEquals(1, l.capacity)
+        assertEquals(2, l[0])
+    }
+
+    @Test
+    fun mutableObjectListOfTwoValues() {
+        val l = mutableObjectListOf(2, 1)
+        assertEquals(2, l.size)
+        assertEquals(2, l.capacity)
+        assertEquals(2, l[0])
+        assertEquals(1, l[1])
+    }
+
+    @Test
+    fun mutableObjectListOfThreeValues() {
+        val l = mutableObjectListOf(2, 10, -1)
+        assertEquals(3, l.size)
+        assertEquals(3, l.capacity)
+        assertEquals(2, l[0])
+        assertEquals(10, l[1])
+        assertEquals(-1, l[2])
+    }
+
+    @Test
+    fun mutableObjectListOfFourValues() {
+        val l = mutableObjectListOf(2, 10, -1, 10)
+        assertEquals(4, l.size)
+        assertEquals(4, l.capacity)
+        assertEquals(2, l[0])
+        assertEquals(10, l[1])
+        assertEquals(-1, l[2])
+        assertEquals(10, l[3])
+    }
+
+    @Test
+    fun iterator() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val iterator = l.asMutableList().iterator()
+        assertTrue(iterator.hasNext())
+        assertEquals(1, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals(2, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals(3, iterator.next())
+        assertTrue(iterator.hasNext())
+        iterator.remove()
+        assertTrue(iterator.hasNext())
+        assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+
+        assertEquals(4, iterator.next())
+        assertTrue(iterator.hasNext())
+        assertEquals(5, iterator.next())
+        assertFalse(iterator.hasNext())
+        iterator.remove()
+        assertEquals(l, mutableObjectListOf(1, 2, 4))
+    }
+
+    @Test
+    fun listIterator() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val iterator = l.asMutableList().listIterator()
+        assertEquals(1, iterator.next())
+        assertEquals(1, iterator.previous())
+        assertEquals(0, iterator.nextIndex())
+        iterator.add(6)
+        assertEquals(1, iterator.nextIndex())
+        assertEquals(0, iterator.previousIndex())
+        assertEquals(6, iterator.previous())
+        assertEquals(l, mutableObjectListOf(6, 1, 2, 3, 4, 5))
+    }
+
+    @Test
+    fun listIteratorInitialIndex() {
+        val iterator = list.asMutableList().listIterator(2)
+        assertEquals(2, iterator.nextIndex())
+    }
+
+    @Test
+    fun subList() {
+        val l = list.asMutableList().subList(1, 4)
+        assertEquals(3, l.size)
+        assertEquals(2, l[0])
+        assertEquals(3, l[1])
+        assertEquals(4, l[2])
+    }
+
+    @Test
+    fun subListContains() {
+        val l = list.asMutableList().subList(1, 4)
+        assertTrue(l.contains(2))
+        assertTrue(l.contains(3))
+        assertTrue(l.contains(4))
+        assertFalse(l.contains(5))
+        assertFalse(l.contains(1))
+    }
+
+    @Test
+    fun subListContainsAll() {
+        val l = list.asMutableList().subList(1, 4)
+        val smallList = listOf(2, 3, 4)
+        assertTrue(l.containsAll(smallList))
+        val largeList = listOf(3, 4, 5)
+        assertFalse(l.containsAll(largeList))
+    }
+
+    @Test
+    fun subListIndexOf() {
+        val l = list.asMutableList().subList(1, 4)
+        assertEquals(0, l.indexOf(2))
+        assertEquals(2, l.indexOf(4))
+        assertEquals(-1, l.indexOf(1))
+        val l2 = mutableObjectListOf(2, 1, 1, 3).asMutableList().subList(1, 2)
+        assertEquals(0, l2.indexOf(1))
+    }
+
+    @Test
+    fun subListIsEmpty() {
+        val l = list.asMutableList().subList(1, 4)
+        assertFalse(l.isEmpty())
+        assertTrue(list.asMutableList().subList(4, 4).isEmpty())
+    }
+
+    @Test
+    fun subListIterator() {
+        val l = list.asMutableList().subList(1, 4)
+        val l2 = mutableListOf<Int>()
+        l.forEach { l2 += it }
+        assertEquals(3, l2.size)
+        assertEquals(2, l2[0])
+        assertEquals(3, l2[1])
+        assertEquals(4, l2[2])
+    }
+
+    @Test
+    fun subListLastIndexOf() {
+        val l = list.asMutableList().subList(1, 4)
+        assertEquals(0, l.lastIndexOf(2))
+        assertEquals(2, l.lastIndexOf(4))
+        assertEquals(-1, l.lastIndexOf(1))
+        val l2 = mutableObjectListOf(2, 1, 1, 3).asMutableList().subList(1, 3)
+        assertEquals(1, l2.lastIndexOf(1))
+    }
+
+    @Test
+    fun subListAdd() {
+        val v = mutableObjectListOf(1, 2, 3)
+        val l = v.asMutableList().subList(1, 2)
+        assertTrue(l.add(4))
+        assertEquals(2, l.size)
+        assertEquals(4, v.size)
+        assertEquals(2, l[0])
+        assertEquals(4, l[1])
+        assertEquals(2, v[1])
+        assertEquals(4, v[2])
+        assertEquals(3, v[3])
+    }
+
+    @Test
+    fun subListAddIndex() {
+        val v = mutableObjectListOf(6, 1, 2, 3)
+        val l = v.asMutableList().subList(1, 3)
+        l.add(1, 4)
+        assertEquals(3, l.size)
+        assertEquals(5, v.size)
+        assertEquals(1, l[0])
+        assertEquals(4, l[1])
+        assertEquals(2, l[2])
+        assertEquals(1, v[1])
+        assertEquals(4, v[2])
+        assertEquals(2, v[3])
+    }
+
+    @Test
+    fun subListAddAllAtIndex() {
+        val v = mutableObjectListOf(6, 1, 2, 3)
+        val l = v.asMutableList().subList(1, 3)
+        l.addAll(1, listOf(4, 5))
+        assertEquals(4, l.size)
+        assertEquals(6, v.size)
+        assertEquals(1, l[0])
+        assertEquals(4, l[1])
+        assertEquals(5, l[2])
+        assertEquals(2, l[3])
+        assertEquals(1, v[1])
+        assertEquals(4, v[2])
+        assertEquals(5, v[3])
+        assertEquals(2, v[4])
+    }
+
+    @Test
+    fun subListAddAll() {
+        val v = mutableObjectListOf(6, 1, 2, 3)
+        val l = v.asMutableList().subList(1, 3)
+        l.addAll(listOf(4, 5))
+        assertEquals(4, l.size)
+        assertEquals(6, v.size)
+        assertEquals(1, l[0])
+        assertEquals(2, l[1])
+        assertEquals(4, l[2])
+        assertEquals(5, l[3])
+        assertEquals(1, v[1])
+        assertEquals(2, v[2])
+        assertEquals(4, v[3])
+        assertEquals(5, v[4])
+        assertEquals(3, v[5])
+    }
+
+    @Test
+    fun subListClear() {
+        val v = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l = v.asMutableList().subList(1, 4)
+        l.clear()
+        assertEquals(0, l.size)
+        assertEquals(2, v.size)
+        assertEquals(1, v[0])
+        assertEquals(5, v[1])
+        kotlin.test.assertNull(v.content[2])
+        kotlin.test.assertNull(v.content[3])
+        kotlin.test.assertNull(v.content[4])
+    }
+
+    @Test
+    fun subListListIterator() {
+        val l = list.asMutableList().subList(1, 4)
+        val listIterator = l.listIterator()
+        assertTrue(listIterator.hasNext())
+        assertFalse(listIterator.hasPrevious())
+        assertEquals(0, listIterator.nextIndex())
+        assertEquals(2, listIterator.next())
+    }
+
+    @Test
+    fun subListListIteratorWithIndex() {
+        val l = list.asMutableList().subList(1, 4)
+        val listIterator = l.listIterator(1)
+        assertTrue(listIterator.hasNext())
+        assertTrue(listIterator.hasPrevious())
+        assertEquals(1, listIterator.nextIndex())
+        assertEquals(3, listIterator.next())
+    }
+
+    @Test
+    fun subListRemove() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        assertTrue(l2.remove(3))
+        assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+        assertEquals(2, l2.size)
+        assertEquals(2, l2[0])
+        assertEquals(4, l2[1])
+        assertFalse(l2.remove(3))
+        assertEquals(l, mutableObjectListOf(1, 2, 4, 5))
+        assertEquals(2, l2.size)
+    }
+
+    @Test
+    fun subListRemoveAll() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        assertFalse(l2.removeAll(listOf(1, 5, -1)))
+        assertEquals(5, l.size)
+        assertEquals(3, l2.size)
+        assertTrue(l2.removeAll(listOf(3, 4, 5)))
+        assertEquals(3, l.size)
+        assertEquals(1, l2.size)
+    }
+
+    @Test
+    fun subListRemoveAt() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        assertEquals(3, l2.removeAt(1))
+        assertEquals(4, l.size)
+        assertEquals(2, l2.size)
+        assertEquals(4, l2.removeAt(1))
+        assertEquals(1, l2.size)
+    }
+
+    @Test
+    fun subListRetainAll() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        val l3 = objectListOf(1, 2, 3, 4, 5)
+        assertFalse(l2.retainAll(l3.asList()))
+        assertFalse(l2.retainAll(listOf(2, 3, 4)))
+        assertEquals(3, l2.size)
+        assertEquals(5, l.size)
+        assertTrue(l2.retainAll(setOf(1, 2, 4)))
+        assertEquals(4, l.size)
+        assertEquals(2, l2.size)
+        assertEquals(l, objectListOf(1, 2, 4, 5))
+    }
+
+    @Test
+    fun subListSet() {
+        val l = mutableObjectListOf(1, 2, 3, 4, 5)
+        val l2 = l.asMutableList().subList(1, 4)
+        l2[1] = 10
+        assertEquals(10, l2[1])
+        assertEquals(3, l2.size)
+        assertEquals(10, l[2])
+    }
+
+    @Test
+    fun subListSubList() {
+        val l = objectListOf(1, 2, 3, 4, 5).asList().subList(1, 5)
+        val l2 = l.subList(1, 3)
+        assertEquals(2, l2.size)
+        assertEquals(3, l2[0])
+    }
+
+    @Suppress("KotlinConstantConditions")
+    @Test
+    fun list_outOfBounds_Get_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(1, 2, 3, 4).asMutableList()
+            l[-1]
+        }
+    }
+
+    @Suppress("KotlinConstantConditions")
+    @Test
+    fun sublist_outOfBounds_Get_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(1, 2, 3, 4).asMutableList().subList(1, 2)
+            l[-1]
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_Get_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(1, 2, 3, 4).asMutableList()
+            l[4]
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_Get_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(1, 2, 3, 4).asMutableList().subList(1, 2)
+            l[1]
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_RemoveAt_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_RemoveAt_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_RemoveAt_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.removeAt(4)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_RemoveAt_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.removeAt(1)
+        }
+    }
+
+    @Suppress("KotlinConstantConditions")
+    @Test
+    fun list_outOfBounds_Set_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l[-1] = 1
+        }
+    }
+
+    @Suppress("KotlinConstantConditions")
+    @Test
+    fun sublist_outOfBounds_Set_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l[-1] = 1
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_Set_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l[4] = 1
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_Set_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l[1] = 1
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_SubList_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.subList(-1, 1)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_SubList_Below() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.subList(-1, 1)
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_SubList_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.subList(5, 5)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_SubList_Above() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.subList(1, 2)
+        }
+    }
+
+    @Test
+    fun list_outOfBounds_SubList_Order() {
+        assertFailsWith(IllegalArgumentException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList()
+            l.subList(3, 2)
+        }
+    }
+
+    @Test
+    fun sublist_outOfBounds_SubList_Order() {
+        assertFailsWith(IllegalArgumentException::class) {
+            val l = mutableObjectListOf(0, 1, 2, 3).asMutableList().subList(1, 2)
+            l.subList(1, 0)
+        }
+    }
+}
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
new file mode 100644
index 0000000..3cf8c73
--- /dev/null
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ObjectLongMapTest.kt
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectLongTest {
+    @Test
+    fun objectLongMap() {
+        val map = MutableObjectLongMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun emptyObjectLongMap() {
+        val map = emptyObjectLongMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyObjectLongMap<String>(), map)
+    }
+
+    @Test
+    fun objectLongMapFunction() {
+        val map = mutableObjectLongMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableObjectLongMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectLongMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableObjectLongMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectLongMapPairsFunction() {
+        val map = mutableObjectLongMapOf(
+            "Hello" to 1L,
+            "Bonjour" to 2L
+        )
+        assertEquals(2, map.size)
+        assertEquals(1L, map["Hello"])
+        assertEquals(2L, map["Bonjour"])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableObjectLongMap<String>(12)
+        map["Hello"] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableObjectLongMap<String>(2)
+        map["Hello"] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableObjectLongMap<String>(0)
+        map["Hello"] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Hello"] = 2L
+
+        assertEquals(1, map.size)
+        assertEquals(2L, map["Hello"])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableObjectLongMap<String>()
+
+        map.put("Hello", 1L)
+        assertEquals(1L, map["Hello"])
+        map.put("Hello", 2L)
+        assertEquals(2L, map["Hello"])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+        map["Bonjour"] = 2L
+
+        map.putAll(arrayOf("Hallo" to 3L, "Hola" to 7L))
+
+        assertEquals(5, map.size)
+        assertEquals(3L, map["Hallo"])
+        assertEquals(7L, map["Hola"])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableObjectLongMap<String>()
+        map += "Hello" to 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map["Hello"])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableObjectLongMap<String>()
+        map += arrayOf("Hallo" to 3L, "Hola" to 7L)
+
+        assertEquals(2, map.size)
+        assertEquals(3L, map["Hallo"])
+        assertEquals(7L, map["Hola"])
+    }
+
+    @Test
+    fun nullKey() {
+        val map = MutableObjectLongMap<String?>()
+        map[null] = 1L
+
+        assertEquals(1, map.size)
+        assertEquals(1L, map[null])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        assertFailsWith<NoSuchElementException> {
+            map["Bonjour"]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        assertEquals(2L, map.getOrDefault("Bonjour", 2L))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        assertEquals(3L, map.getOrElse("Hallo") { 3L })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+
+        var counter = 0
+        map.getOrPut("Hello") {
+            counter++
+            2L
+        }
+        assertEquals(1L, map["Hello"])
+        assertEquals(0, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            2L
+        }
+        assertEquals(2L, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            3L
+        }
+        assertEquals(2L, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Hallo") {
+            counter++
+            3L
+        }
+        assertEquals(3L, map["Hallo"])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableObjectLongMap<String?>()
+        map.remove("Hello")
+
+        map["Hello"] = 1L
+        map.remove("Hello")
+        assertEquals(0, map.size)
+
+        map[null] = 1L
+        map.remove(null)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableObjectLongMap<String>(6)
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove("Hello")
+        map.remove("Bonjour")
+        map.remove("Hallo")
+        map.remove("Konnichiwa")
+        map.remove("Ciao")
+        map.remove("Annyeong")
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map["Hola"] = 7L
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        map.removeIf { key, _ -> key.startsWith('H') }
+
+        assertEquals(4, map.size)
+        assertEquals(2L, map["Bonjour"])
+        assertEquals(4L, map["Konnichiwa"])
+        assertEquals(5L, map["Ciao"])
+        assertEquals(6L, map["Annyeong"])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+
+        map -= "Hello"
+
+        assertEquals(2, map.size)
+        assertFalse("Hello" in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+
+        map -= arrayOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusIterable() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+
+        map -= listOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusSequence() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+
+        map -= listOf("Hallo", "Bonjour").asSequence()
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableObjectLongMap<String?>()
+        assertFalse(map.remove("Hello", 1L))
+
+        map["Hello"] = 1L
+        assertTrue(map.remove("Hello", 1L))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableObjectLongMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toLong()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableObjectLongMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt().toString())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableObjectLongMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toLong()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertNotNull(key.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableObjectLongMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toLong()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableObjectLongMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toString()] = i.toLong()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableObjectLongMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        val oneString = 1L.toString()
+        val twoString = 2L.toString()
+        assertTrue(
+            "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+                "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+        )
+
+        map.clear()
+        map[null] = 2L
+        assertEquals("{null=$twoString}", map.toString())
+
+        val selfAsKeyMap = MutableObjectLongMap<Any>()
+        selfAsKeyMap[selfAsKeyMap] = 1L
+        assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableObjectLongMap<String?>()
+        map2[null] = 2L
+
+        assertNotEquals(map, map2)
+
+        map2["Hello"] = 1L
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+
+        assertTrue(map.containsKey("Hello"))
+        assertTrue(map.containsKey(null))
+        assertFalse(map.containsKey("Bonjour"))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+
+        assertTrue("Hello" in map)
+        assertTrue(null in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableObjectLongMap<String?>()
+        map["Hello"] = 1L
+        map[null] = 2L
+
+        assertTrue(map.containsValue(1L))
+        assertTrue(map.containsValue(2L))
+        assertFalse(map.containsValue(3L))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableObjectLongMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map["Hello"] = 1L
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableObjectLongMap<String>()
+        assertEquals(0, map.count())
+
+        map["Hello"] = 1L
+        assertEquals(1, map.count())
+
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+        assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        assertTrue(map.any { key, _ -> key.startsWith("K") })
+        assertFalse(map.any { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableObjectLongMap<String>()
+        map["Hello"] = 1L
+        map["Bonjour"] = 2L
+        map["Hallo"] = 3L
+        map["Konnichiwa"] = 4L
+        map["Ciao"] = 5L
+        map["Annyeong"] = 6L
+
+        assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+        assertFalse(map.all { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableObjectLongMap<String>()
+        assertEquals(7, map.trim())
+
+        map["Hello"] = 1L
+        map["Hallo"] = 3L
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toLong()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toString()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/template/ObjectPValueMap.kt.template b/collection/collection/template/ObjectPValueMap.kt.template
new file mode 100644
index 0000000..ba8284e
--- /dev/null
+++ b/collection/collection/template/ObjectPValueMap.kt.template
@@ -0,0 +1,855 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyObjectPValueMap = MutableObjectPValueMap<Any?>(0)
+
+/**
+ * Returns an empty, read-only [ObjectPValueMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> emptyObjectPValueMap(): ObjectPValueMap<K> =
+    EmptyObjectPValueMap as ObjectPValueMap<K>
+
+/**
+ * Returns an empty, read-only [ObjectPValueMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <K> objectPValueMap(): ObjectPValueMap<K> =
+    EmptyObjectPValueMap as ObjectPValueMap<K>
+
+/**
+ * Returns a new [MutableObjectPValueMap].
+ */
+public fun <K> mutableObjectPValueMapOf(): MutableObjectPValueMap<K> = MutableObjectPValueMap()
+
+/**
+ * Returns a new [MutableObjectPValueMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PValue] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> objectPValueMapOf(vararg pairs: Pair<K, PValue>): ObjectPValueMap<K> =
+    MutableObjectPValueMap<K>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutableObjectPValueMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PValue] value is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <K> mutableObjectPValueMapOf(vararg pairs: Pair<K, PValue>): MutableObjectPValueMap<K> =
+    MutableObjectPValueMap<K>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [ObjectPValueMap] is a container with a [Map]-like interface for keys with
+ * reference types and [PValue] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutableObjectPValueMap].
+ *
+ * @see [MutableObjectPValueMap]
+ * @see ScatterMap
+ */
+public sealed class ObjectPValueMap<K> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: Array<Any?> = EMPTY_OBJECTS
+
+    @PublishedApi
+    @JvmField
+    internal var values: PValueArray = EmptyPValueArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     * @throws NoSuchElementException when [key] is not found
+     */
+    public operator fun get(key: K): PValue {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("There is no key $key in the map")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: K, defaultValue: PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: K, defaultValue: () -> PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: K, value: PValue) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K, v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: K) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index] as K)
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: PValue) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (K, PValue) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (K, PValue) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (K, PValue) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: K): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: PValue): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [ObjectPValueMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is ObjectPValueMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        val o = other as ObjectPValueMap<Any?>
+
+        forEach { key, value ->
+            if (value != o[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(if (key === this) "(this)" else key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: K): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutableObjectPValueMap] is a container with a [MutableMap]-like interface for keys with
+ * reference types and [PValue] primitives for values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutableObjectPValueMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutableObjectPValueMap<K>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : ObjectPValueMap<K>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = arrayOfNulls(newCapacity)
+        values = PValueArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: K, defaultValue: () -> PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        val value = defaultValue()
+        set(key, value)
+        return value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: K, value: PValue) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public fun put(key: K, value: PValue) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [PValue] value is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<K, PValue>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: ObjectPValueMap<K>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that the [Pair] is allocated and the [PValue] value is boxed.
+     * Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<K, PValue>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [PValue] value is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<out Pair<K, PValue>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: ObjectPValueMap<K>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: K) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: K, value: PValue): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (K, PValue) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index] as K, values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: K) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: Array<out K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Iterable<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: Sequence<K>) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: ScatterSet<K>) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        keys[index] = null
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        keys.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: K): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutableObjectPValueMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/template/ObjectPValueMapTest.kt.template b/collection/collection/template/ObjectPValueMapTest.kt.template
new file mode 100644
index 0000000..d2c6f43
--- /dev/null
+++ b/collection/collection/template/ObjectPValueMapTest.kt.template
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class ObjectPValueTest {
+    @Test
+    fun objectPValueMap() {
+        val map = MutableObjectPValueMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun emptyObjectPValueMap() {
+        val map = emptyObjectPValueMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyObjectPValueMap<String>(), map)
+    }
+
+    @Test
+    fun objectPValueMapFunction() {
+        val map = mutableObjectPValueMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutableObjectPValueMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectPValueMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutableObjectPValueMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun objectPValueMapPairsFunction() {
+        val map = mutableObjectPValueMapOf(
+            "Hello" to 1ValueSuffix,
+            "Bonjour" to 2ValueSuffix
+        )
+        assertEquals(2, map.size)
+        assertEquals(1ValueSuffix, map["Hello"])
+        assertEquals(2ValueSuffix, map["Bonjour"])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutableObjectPValueMap<String>(12)
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutableObjectPValueMap<String>(2)
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutableObjectPValueMap<String>(0)
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Hello"] = 2ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(2ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun put() {
+        val map = MutableObjectPValueMap<String>()
+
+        map.put("Hello", 1ValueSuffix)
+        assertEquals(1ValueSuffix, map["Hello"])
+        map.put("Hello", 2ValueSuffix)
+        assertEquals(2ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+
+        map.putAll(arrayOf("Hallo" to 3ValueSuffix, "Hola" to 7ValueSuffix))
+
+        assertEquals(5, map.size)
+        assertEquals(3ValueSuffix, map["Hallo"])
+        assertEquals(7ValueSuffix, map["Hola"])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutableObjectPValueMap<String>()
+        map += "Hello" to 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map["Hello"])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutableObjectPValueMap<String>()
+        map += arrayOf("Hallo" to 3ValueSuffix, "Hola" to 7ValueSuffix)
+
+        assertEquals(2, map.size)
+        assertEquals(3ValueSuffix, map["Hallo"])
+        assertEquals(7ValueSuffix, map["Hola"])
+    }
+
+    @Test
+    fun nullKey() {
+        val map = MutableObjectPValueMap<String?>()
+        map[null] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[null])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        assertFailsWith<NoSuchElementException> {
+            map["Bonjour"]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(2ValueSuffix, map.getOrDefault("Bonjour", 2ValueSuffix))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        assertEquals(3ValueSuffix, map.getOrElse("Hallo") { 3ValueSuffix })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+
+        var counter = 0
+        map.getOrPut("Hello") {
+            counter++
+            2ValueSuffix
+        }
+        assertEquals(1ValueSuffix, map["Hello"])
+        assertEquals(0, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            2ValueSuffix
+        }
+        assertEquals(2ValueSuffix, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Bonjour") {
+            counter++
+            3ValueSuffix
+        }
+        assertEquals(2ValueSuffix, map["Bonjour"])
+        assertEquals(1, counter)
+
+        map.getOrPut("Hallo") {
+            counter++
+            3ValueSuffix
+        }
+        assertEquals(3ValueSuffix, map["Hallo"])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutableObjectPValueMap<String?>()
+        map.remove("Hello")
+
+        map["Hello"] = 1ValueSuffix
+        map.remove("Hello")
+        assertEquals(0, map.size)
+
+        map[null] = 1ValueSuffix
+        map.remove(null)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutableObjectPValueMap<String>(6)
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove("Hello")
+        map.remove("Bonjour")
+        map.remove("Hallo")
+        map.remove("Konnichiwa")
+        map.remove("Ciao")
+        map.remove("Annyeong")
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map["Hola"] = 7ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        map.removeIf { key, _ -> key.startsWith('H') }
+
+        assertEquals(4, map.size)
+        assertEquals(2ValueSuffix, map["Bonjour"])
+        assertEquals(4ValueSuffix, map["Konnichiwa"])
+        assertEquals(5ValueSuffix, map["Ciao"])
+        assertEquals(6ValueSuffix, map["Annyeong"])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        map -= "Hello"
+
+        assertEquals(2, map.size)
+        assertFalse("Hello" in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        map -= arrayOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusIterable() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        map -= listOf("Hallo", "Bonjour")
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun minusSequence() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        map -= listOf("Hallo", "Bonjour").asSequence()
+
+        assertEquals(1, map.size)
+        assertFalse("Hallo" in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutableObjectPValueMap<String?>()
+        assertFalse(map.remove("Hello", 1ValueSuffix))
+
+        map["Hello"] = 1ValueSuffix
+        assertTrue(map.remove("Hello", 1ValueSuffix))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutableObjectPValueMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toPValue()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutableObjectPValueMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toPValue()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toInt().toString())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutableObjectPValueMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toPValue()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertNotNull(key.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutableObjectPValueMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toString()] = j.toPValue()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutableObjectPValueMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toString()] = i.toPValue()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutableObjectPValueMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        val oneString = 1ValueSuffix.toString()
+        val twoString = 2ValueSuffix.toString()
+        assertTrue(
+            "{Hello=$oneString, Bonjour=$twoString}" == map.toString() ||
+                "{Bonjour=$twoString, Hello=$oneString}" == map.toString()
+        )
+
+        map.clear()
+        map[null] = 2ValueSuffix
+        assertEquals("{null=$twoString}", map.toString())
+
+        val selfAsKeyMap = MutableObjectPValueMap<Any>()
+        selfAsKeyMap[selfAsKeyMap] = 1ValueSuffix
+        assertEquals("{(this)=$oneString}", selfAsKeyMap.toString())
+    }
+
+    @Test
+    fun equals() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutableObjectPValueMap<String?>()
+        map2[null] = 2ValueSuffix
+
+        assertNotEquals(map, map2)
+
+        map2["Hello"] = 1ValueSuffix
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+
+        assertTrue(map.containsKey("Hello"))
+        assertTrue(map.containsKey(null))
+        assertFalse(map.containsKey("Bonjour"))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+
+        assertTrue("Hello" in map)
+        assertTrue(null in map)
+        assertFalse("Bonjour" in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutableObjectPValueMap<String?>()
+        map["Hello"] = 1ValueSuffix
+        map[null] = 2ValueSuffix
+
+        assertTrue(map.containsValue(1ValueSuffix))
+        assertTrue(map.containsValue(2ValueSuffix))
+        assertFalse(map.containsValue(3ValueSuffix))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutableObjectPValueMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map["Hello"] = 1ValueSuffix
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutableObjectPValueMap<String>()
+        assertEquals(0, map.count())
+
+        map["Hello"] = 1ValueSuffix
+        assertEquals(1, map.count())
+
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        assertEquals(2, map.count { key, _ -> key.startsWith("H") })
+        assertEquals(0, map.count { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun any() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        assertTrue(map.any { key, _ -> key.startsWith("K") })
+        assertFalse(map.any { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun all() {
+        val map = MutableObjectPValueMap<String>()
+        map["Hello"] = 1ValueSuffix
+        map["Bonjour"] = 2ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+        map["Konnichiwa"] = 4ValueSuffix
+        map["Ciao"] = 5ValueSuffix
+        map["Annyeong"] = 6ValueSuffix
+
+        assertTrue(map.all { key, value -> key.length >= 4 && value.toInt() >= 1 })
+        assertFalse(map.all { key, _ -> key.startsWith("W") })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutableObjectPValueMap<String>()
+        assertEquals(7, map.trim())
+
+        map["Hello"] = 1ValueSuffix
+        map["Hallo"] = 3ValueSuffix
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toString()] = i.toPValue()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toString()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/template/PKeyList.kt.template b/collection/collection/template/PKeyList.kt.template
new file mode 100644
index 0000000..620d2e4
--- /dev/null
+++ b/collection/collection/template/PKeyList.kt.template
@@ -0,0 +1,922 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier")
+@file:OptIn(ExperimentalContracts::class)
+
+package androidx.collection
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+/**
+ * [PKeyList] is a [List]-like collection for [PKey] values. It allows retrieving
+ * the elements without boxing. [PKeyList] is always backed by a [MutablePKeyList],
+ * its [MutableList]-like subclass.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ */
+public sealed class PKeyList(initialCapacity: Int) {
+    @JvmField
+    @PublishedApi
+    internal var content: PKeyArray = if (initialCapacity == 0) {
+        EmptyPKeyArray
+    } else {
+        PKeyArray(initialCapacity)
+    }
+
+    @Suppress("PropertyName")
+    @JvmField
+    @PublishedApi
+    internal var _size: Int = 0
+
+    /**
+     * The number of elements in the [PKeyList].
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns the last valid index in the [PKeyList]. This can be `-1` when the list is empty.
+     */
+    @get:androidx.annotation.IntRange(from = -1)
+    public inline val lastIndex: Int get() = _size - 1
+
+    /**
+     * Returns an [IntRange] of the valid indices for this [PKeyList].
+     */
+    public inline val indices: IntRange get() = 0 until _size
+
+    /**
+     * Returns `true` if the collection has no elements in it.
+     */
+    public fun none(): Boolean {
+        return isEmpty()
+    }
+
+    /**
+     * Returns `true` if there's at least one element in the collection.
+     */
+    public fun any(): Boolean {
+        return isNotEmpty()
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate].
+     */
+    public inline fun any(predicate: (element: PKey) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if any of the elements give a `true` return value for [predicate] while
+     * iterating in the reverse order.
+     */
+    public inline fun reversedAny(predicate: (element: PKey) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEachReversed {
+            if (predicate(it)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [PKeyList] contains [element] or `false` otherwise.
+     */
+    public operator fun contains(element: PKey): Boolean {
+        forEach {
+            if (it == element) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns `true` if the [PKeyList] contains all elements in [elements] or `false` if
+     * one or more are missing.
+     */
+    public fun containsAll(elements: PKeyList): Boolean {
+        for (i in elements.indices) {
+            if (!contains(elements[i])) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns the number of elements in this list.
+     */
+    public fun count(): Int = _size
+
+    /**
+     * Counts the number of elements matching [predicate].
+     * @return The number of elements in this list for which [predicate] returns true.
+     */
+    public inline fun count(predicate: (element: PKey) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        var count = 0
+        forEach { if (predicate(it)) count++ }
+        return count
+    }
+
+    /**
+     * Returns the first element in the [PKeyList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun first(): PKey {
+        if (isEmpty()) {
+            throw NoSuchElementException("PKeyList is empty.")
+        }
+        return content[0]
+    }
+
+    /**
+     * Returns the first element in the [PKeyList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfFirst
+     */
+    public inline fun first(predicate: (element: PKey) -> Boolean): PKey {
+        contract { callsInPlace(predicate) }
+        forEach { item ->
+            if (predicate(item)) return item
+        }
+        throw NoSuchElementException("PKeyList contains no element matching the predicate.")
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [PKeyList] in order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes current accumulator value and an element, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> fold(initial: R, operation: (acc: R, element: PKey) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEach { item ->
+            acc = operation(acc, item)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [PKeyList] in order.
+     */
+    public inline fun <R> foldIndexed(
+        initial: R,
+        operation: (index: Int, acc: R, element: PKey) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachIndexed { i, item ->
+            acc = operation(i, acc, item)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [PKeyList] in reverse order.
+     * @param initial The value of `acc` for the first call to [operation] or return value if
+     * there are no elements in this list.
+     * @param operation function that takes an element and the current accumulator value, and
+     * calculates the next accumulator value.
+     */
+    public inline fun <R> foldRight(initial: R, operation: (element: PKey, acc: R) -> R): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversed { item ->
+            acc = operation(item, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Accumulates values, starting with [initial], and applying [operation] to each element
+     * in the [PKeyList] in reverse order.
+     */
+    public inline fun <R> foldRightIndexed(
+        initial: R,
+        operation: (index: Int, element: PKey, acc: R) -> R
+    ): R {
+        contract { callsInPlace(operation) }
+        var acc = initial
+        forEachReversedIndexed { i, item ->
+            acc = operation(i, item, acc)
+        }
+        return acc
+    }
+
+    /**
+     * Calls [block] for each element in the [PKeyList], in order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEach(block: (element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [PKeyList] along with its index, in order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachIndexed(block: (index: Int, element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in 0 until _size) {
+            block(i, content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [PKeyList] in reverse order.
+     * @param block will be executed for every element in the list, accepting an element from
+     * the list
+     */
+    public inline fun forEachReversed(block: (element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(content[i])
+        }
+    }
+
+    /**
+     * Calls [block] for each element in the [PKeyList] along with its index, in reverse
+     * order.
+     * @param block will be executed for every element in the list, accepting the index and
+     * the element at that index.
+     */
+    public inline fun forEachReversedIndexed(block: (index: Int, element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val content = content
+        for (i in _size - 1 downTo 0) {
+            block(i, content[i])
+        }
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public operator fun get(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the element at the given [index] or throws [IndexOutOfBoundsException] if
+     * the [index] is out of bounds of this collection.
+     */
+    public fun elementAt(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the element at the given [index] or [defaultValue] if [index] is out of bounds
+     * of the collection.
+     * @param index The index of the element whose value should be returned
+     * @param defaultValue A lambda to call with [index] as a parameter to return a value at
+     * an index not in the list.
+     */
+    public inline fun elementAtOrElse(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        defaultValue: (index: Int) -> PKey
+    ): PKey {
+        if (index !in 0 until _size) {
+            return defaultValue(index)
+        }
+        return content[index]
+    }
+
+    /**
+     * Returns the index of [element] in the [PKeyList] or `-1` if [element] is not there.
+     */
+    public fun indexOf(element: PKey): Int {
+        forEachIndexed { i, item ->
+            if (element == item) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the first element in the [PKeyList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfFirst(predicate: (element: PKey) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachIndexed { i, item ->
+            if (predicate(item)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns the index if the last element in the [PKeyList] for which [predicate]
+     * returns `true`.
+     */
+    public inline fun indexOfLast(predicate: (element: PKey) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        forEachReversedIndexed { i, item ->
+            if (predicate(item)) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns `true` if the [PKeyList] has no elements in it or `false` otherwise.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if there are elements in the [PKeyList] or `false` if it is empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the last element in the [PKeyList] or throws a [NoSuchElementException] if
+     * it [isEmpty].
+     */
+    public fun last(): PKey {
+        if (isEmpty()) {
+            throw NoSuchElementException("PKeyList is empty.")
+        }
+        return content[lastIndex]
+    }
+
+    /**
+     * Returns the last element in the [PKeyList] for which [predicate] returns `true` or
+     * throws [NoSuchElementException] if nothing matches.
+     * @see indexOfLast
+     */
+    public inline fun last(predicate: (element: PKey) -> Boolean): PKey {
+        contract { callsInPlace(predicate) }
+        forEachReversed { item ->
+            if (predicate(item)) {
+                return item
+            }
+        }
+        throw NoSuchElementException("PKeyList contains no element matching the predicate.")
+    }
+
+    /**
+     * Returns the index of the last element in the [PKeyList] that is the same as
+     * [element] or `-1` if no elements match.
+     */
+    public fun lastIndexOf(element: PKey): Int {
+        forEachReversedIndexed { i, item ->
+            if (item == element) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Returns a hash code based on the contents of the [PKeyList].
+     */
+    override fun hashCode(): Int {
+        var hashCode = 0
+        forEach { element ->
+            hashCode += 31 * element.hashCode()
+        }
+        return hashCode
+    }
+
+    /**
+     * Returns `true` if [other] is a [PKeyList] and the contents of this and [other] are the
+     * same.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (other !is PKeyList || other._size != _size) {
+            return false
+        }
+        val content = content
+        val otherContent = other.content
+        for (i in indices) {
+            if (content[i] != otherContent[i]) {
+                return false
+            }
+        }
+        return true
+    }
+
+    /**
+     * Returns a String representation of the list, surrounded by "[]" and each element
+     * separated by ", ".
+     */
+    override fun toString(): String {
+        if (isEmpty()) {
+            return "[]"
+        }
+        val last = lastIndex
+        return buildString {
+            append('[')
+            val content = content
+            for (i in 0 until last) {
+                append(content[i])
+                append(',')
+                append(' ')
+            }
+            append(content[last])
+            append(']')
+        }
+    }
+}
+
+/**
+ * [MutablePKeyList] is a [MutableList]-like collection for [PKey] values.
+ * It allows storing and retrieving the elements without boxing. Immutable
+ * access is available through its base class [PKeyList], which has a [List]-like
+ * interface.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the list (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. It is also not safe to mutate during reentrancy --
+ * in the middle of a [forEach], for example. However, concurrent reads are safe.
+ *
+ * @constructor Creates a [MutablePKeyList] with a [capacity] of `initialCapacity`.
+ */
+public class MutablePKeyList(
+    initialCapacity: Int = 16
+) : PKeyList(initialCapacity) {
+    /**
+     * Returns the total number of elements that can be held before the [MutablePKeyList] must
+     * grow.
+     *
+     * @see ensureCapacity
+     */
+    public inline val capacity: Int
+        get() = content.size
+
+    /**
+     * Adds [element] to the [MutablePKeyList] and returns `true`.
+     */
+    public fun add(element: PKey): Boolean {
+        ensureCapacity(_size + 1)
+        content[_size] = element
+        _size++
+        return true
+    }
+
+    /**
+     * Adds [element] to the [MutablePKeyList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun add(@androidx.annotation.IntRange(from = 0) index: Int, element: PKey) {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        ensureCapacity(_size + 1)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + 1,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        content[index] = element
+        _size++
+    }
+
+    /**
+     * Adds all [elements] to the [MutablePKeyList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutablePKeyList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive.
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: PKeyArray
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements.size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements.size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.copyInto(content, index)
+        _size += elements.size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the [MutablePKeyList] at the given [index], shifting over any
+     * elements at [index] and after, if any.
+     * @return `true` if the [MutablePKeyList] was changed or `false` if [elements] was empty
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [size], inclusive
+     */
+    public fun addAll(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        elements: PKeyList
+    ): Boolean {
+        if (index !in 0.._size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$_size")
+        }
+        if (elements.isEmpty()) return false
+        ensureCapacity(_size + elements._size)
+        val content = content
+        if (index != _size) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index + elements._size,
+                startIndex = index,
+                endIndex = _size
+            )
+        }
+        elements.content.copyInto(
+            destination = content,
+            destinationOffset = index,
+            startIndex = 0,
+            endIndex = elements._size
+        )
+        _size += elements._size
+        return true
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutablePKeyList] and returns `true` if the
+     * [MutablePKeyList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: PKeyList): Boolean {
+        return addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutablePKeyList] and returns `true` if the
+     * [MutablePKeyList] was changed or `false` if [elements] was empty.
+     */
+    public fun addAll(elements: PKeyArray): Boolean {
+        return addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutablePKeyList].
+     */
+    public operator fun plusAssign(elements: PKeyList) {
+        addAll(_size, elements)
+    }
+
+    /**
+     * Adds all [elements] to the end of the [MutablePKeyList].
+     */
+    public operator fun plusAssign(elements: PKeyArray) {
+        addAll(_size, elements)
+    }
+
+    /**
+     * Removes all elements in the [MutablePKeyList]. The storage isn't released.
+     * @see trim
+     */
+    public fun clear() {
+        _size = 0
+    }
+
+    /**
+     * Reduces the internal storage. If [capacity] is greater than [minCapacity] and [size], the
+     * internal storage is reduced to the maximum of [size] and [minCapacity].
+     * @see ensureCapacity
+     */
+    public fun trim(minCapacity: Int = _size) {
+        val minSize = maxOf(minCapacity, _size)
+        if (capacity > minSize) {
+            content = content.copyOf(minSize)
+        }
+    }
+
+    /**
+     * Ensures that there is enough space to store [capacity] elements in the [MutablePKeyList].
+     * @see trim
+     */
+    public fun ensureCapacity(capacity: Int) {
+        val oldContent = content
+        if (oldContent.size < capacity) {
+            val newSize = maxOf(capacity, oldContent.size * 3 / 2)
+            content = oldContent.copyOf(newSize)
+        }
+    }
+
+    /**
+     * [add] [element] to the [MutablePKeyList].
+     */
+    public inline operator fun plusAssign(element: PKey) {
+        add(element)
+    }
+
+    /**
+     * [remove] [element] from the [MutablePKeyList]
+     */
+    public inline operator fun minusAssign(element: PKey) {
+        remove(element)
+    }
+
+    /**
+     * Removes [element] from the [MutablePKeyList]. If [element] was in the [MutablePKeyList]
+     * and was removed, `true` will be returned, or `false` will be returned if the element
+     * was not found.
+     */
+    public fun remove(element: PKey): Boolean {
+        val index = indexOf(element)
+        if (index >= 0) {
+            removeAt(index)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Removes all [elements] from the [MutablePKeyList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: PKeyArray): Boolean {
+        val initialSize = _size
+        for (i in elements.indices) {
+            remove(elements[i])
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutablePKeyList] and returns `true` if anything was removed.
+     */
+    public fun removeAll(elements: PKeyList): Boolean {
+        val initialSize = _size
+        for (i in 0..elements.lastIndex) {
+            remove(elements[i])
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Removes all [elements] from the [MutablePKeyList].
+     */
+    public operator fun minusAssign(elements: PKeyArray) {
+        elements.forEach { element ->
+            remove(element)
+        }
+    }
+
+    /**
+     * Removes all [elements] from the [MutablePKeyList].
+     */
+    public operator fun minusAssign(elements: PKeyList) {
+        elements.forEach { element ->
+            remove(element)
+        }
+    }
+
+    /**
+     * Removes the element at the given [index] and returns it.
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public fun removeAt(@androidx.annotation.IntRange(from = 0) index: Int): PKey {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("Index $index must be in 0..$lastIndex")
+        }
+        val content = content
+        val item = content[index]
+        if (index != lastIndex) {
+            content.copyInto(
+                destination = content,
+                destinationOffset = index,
+                startIndex = index + 1,
+                endIndex = _size
+            )
+        }
+        _size--
+        return item
+    }
+
+    /**
+     * Removes items from index [start] (inclusive) to [end] (exclusive).
+     * @throws IndexOutOfBoundsException if [start] or [end] isn't between 0 and [size], inclusive
+     * @throws IllegalArgumentException if [start] is greater than [end]
+     */
+    public fun removeRange(
+        @androidx.annotation.IntRange(from = 0) start: Int,
+        @androidx.annotation.IntRange(from = 0) end: Int
+    ) {
+        if (start !in 0.._size || end !in 0.._size) {
+            throw IndexOutOfBoundsException("Start ($start) and end ($end) must be in 0..$_size")
+        }
+        if (end < start) {
+            throw IllegalArgumentException("Start ($start) is more than end ($end)")
+        }
+        if (end != start) {
+            if (end < _size) {
+                content.copyInto(
+                    destination = content,
+                    destinationOffset = start,
+                    startIndex = end,
+                    endIndex = _size
+                )
+            }
+            _size -= (end - start)
+        }
+    }
+
+    /**
+     * Keeps only [elements] in the [MutablePKeyList] and removes all other values.
+     * @return `true` if the [MutablePKeyList] has changed.
+     */
+    public fun retainAll(elements: PKeyArray): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val item = content[i]
+            if (elements.indexOfFirst { it == item } < 0) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Keeps only [elements] in the [MutablePKeyList] and removes all other values.
+     * @return `true` if the [MutablePKeyList] has changed.
+     */
+    public fun retainAll(elements: PKeyList): Boolean {
+        val initialSize = _size
+        val content = content
+        for (i in lastIndex downTo 0) {
+            val item = content[i]
+            if (item !in elements) {
+                removeAt(i)
+            }
+        }
+        return initialSize != _size
+    }
+
+    /**
+     * Sets the value at [index] to [element].
+     * @return the previous value set at [index]
+     * @throws IndexOutOfBoundsException if [index] isn't between 0 and [lastIndex], inclusive
+     */
+    public operator fun set(
+        @androidx.annotation.IntRange(from = 0) index: Int,
+        element: PKey
+    ): PKey {
+        if (index !in 0 until _size) {
+            throw IndexOutOfBoundsException("set index $index must be between 0 .. $lastIndex")
+        }
+        val content = content
+        val old = content[index]
+        content[index] = element
+        return old
+    }
+
+    /**
+     * Sorts the [MutablePKeyList] elements in ascending order.
+     */
+    public fun sort() {
+        content.sort(fromIndex = 0, toIndex = _size)
+    }
+
+    /**
+     * Sorts the [MutablePKeyList] elements in descending order.
+     */
+    public fun sortDescending() {
+        content.sortDescending(fromIndex = 0, toIndex = _size)
+    }
+}
+
+private val EmptyPKeyList: PKeyList = MutablePKeyList(0)
+
+/**
+ * @return a read-only [PKeyList] with nothing in it.
+ */
+public fun emptyPKeyList(): PKeyList = EmptyPKeyList
+
+/**
+ * @return a read-only [PKeyList] with nothing in it.
+ */
+public fun pKeyListOf(): PKeyList = EmptyPKeyList
+
+/**
+ * @return a new read-only [PKeyList] with [element1] as the only item in the list.
+ */
+public fun pKeyListOf(element1: PKey): PKeyList = mutablePKeyListOf(element1)
+
+/**
+ * @return a new read-only [PKeyList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun pKeyListOf(element1: PKey, element2: PKey): PKeyList =
+    mutablePKeyListOf(element1, element2)
+
+/**
+ * @return a new read-only [PKeyList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun pKeyListOf(element1: PKey, element2: PKey, element3: PKey): PKeyList =
+    mutablePKeyListOf(element1, element2, element3)
+
+/**
+ * @return a new read-only [PKeyList] with [elements] in order.
+ */
+public fun pKeyListOf(vararg elements: PKey): PKeyList =
+    MutablePKeyList(elements.size).apply { plusAssign(elements) }
+
+/**
+ * @return a new empty [MutablePKeyList] with the default capacity.
+ */
+public inline fun mutablePKeyListOf(): MutablePKeyList = MutablePKeyList()
+
+/**
+ * @return a new [MutablePKeyList] with [element1] as the only item in the list.
+ */
+public fun mutablePKeyListOf(element1: PKey): MutablePKeyList {
+    val list = MutablePKeyList(1)
+    list += element1
+    return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with 2 elements, [element1] and [element2], in order.
+ */
+public fun mutablePKeyListOf(element1: PKey, element2: PKey): MutablePKeyList {
+    val list = MutablePKeyList(2)
+    list += element1
+    list += element2
+    return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with 3 elements, [element1], [element2], and [element3],
+ * in order.
+ */
+public fun mutablePKeyListOf(element1: PKey, element2: PKey, element3: PKey): MutablePKeyList {
+    val list = MutablePKeyList(3)
+    list += element1
+    list += element2
+    list += element3
+    return list
+}
+
+/**
+ * @return a new [MutablePKeyList] with the given elements, in order.
+ */
+public inline fun mutablePKeyListOf(vararg elements: PKey): MutablePKeyList =
+    MutablePKeyList(elements.size).apply { plusAssign(elements) }
diff --git a/collection/collection/template/PKeyListTest.kt.template b/collection/collection/template/PKeyListTest.kt.template
new file mode 100644
index 0000000..34915e5
--- /dev/null
+++ b/collection/collection/template/PKeyListTest.kt.template
@@ -0,0 +1,723 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeyListTest {
+    private val list: MutablePKeyList = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+
+    @Test
+    fun emptyConstruction() {
+        val l = mutablePKeyListOf()
+        assertEquals(0, l.size)
+        assertEquals(16, l.capacity)
+    }
+
+    @Test
+    fun sizeConstruction() {
+        val l = MutablePKeyList(4)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun contentConstruction() {
+        val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+        assertEquals(3, l.size)
+        assertEquals(1KeySuffix, l[0])
+        assertEquals(2KeySuffix, l[1])
+        assertEquals(3KeySuffix, l[2])
+        assertEquals(3, l.capacity)
+        repeat(2) {
+            val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+            assertEquals(list, l2)
+            l2.removeAt(0)
+        }
+    }
+
+    @Test
+    fun hashCodeTest() {
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.removeAt(4)
+        assertNotEquals(list.hashCode(), l2.hashCode())
+        l2.add(5KeySuffix)
+        assertEquals(list.hashCode(), l2.hashCode())
+        l2.clear()
+        assertNotEquals(list.hashCode(), l2.hashCode())
+    }
+
+    @Test
+    fun equalsTest() {
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        assertEquals(list, l2)
+        assertNotEquals(list, mutablePKeyListOf())
+        l2.removeAt(4)
+        assertNotEquals(list, l2)
+        l2.add(5KeySuffix)
+        assertEquals(list, l2)
+        l2.clear()
+        assertNotEquals(list, l2)
+    }
+
+    @Test
+    fun string() {
+        assertEquals("[${1KeySuffix}, ${2KeySuffix}, ${3KeySuffix}, ${4KeySuffix}, ${5KeySuffix}]", list.toString())
+        assertEquals("[]", mutablePKeyListOf().toString())
+    }
+
+    @Test
+    fun size() {
+        assertEquals(5, list.size)
+        assertEquals(5, list.count())
+        val l2 = mutablePKeyListOf()
+        assertEquals(0, l2.size)
+        assertEquals(0, l2.count())
+        l2 += 1KeySuffix
+        assertEquals(1, l2.size)
+        assertEquals(1, l2.count())
+    }
+
+    @Test
+    fun get() {
+        assertEquals(1KeySuffix, list[0])
+        assertEquals(5KeySuffix, list[4])
+        assertEquals(1KeySuffix, list.elementAt(0))
+        assertEquals(5KeySuffix, list.elementAt(4))
+    }
+
+    @Test
+    fun getOutOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[5]
+        }
+    }
+
+    @Test
+    fun getOutOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list[-1]
+        }
+    }
+
+    @Test
+    fun elementAtOfBounds() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(5)
+        }
+    }
+
+    @Test
+    fun elementAtOfBoundsNegative() {
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            list.elementAt(-1)
+        }
+    }
+
+    @Test
+    fun elementAtOrElse() {
+        assertEquals(1KeySuffix, list.elementAtOrElse(0) {
+            assertEquals(0, it)
+            0KeySuffix
+        })
+        assertEquals(0KeySuffix, list.elementAtOrElse(-1) {
+            assertEquals(-1, it)
+            0KeySuffix
+        })
+        assertEquals(0KeySuffix, list.elementAtOrElse(5) {
+            assertEquals(5, it)
+            0KeySuffix
+        })
+    }
+
+    @Test
+    fun count() {
+        assertEquals(1, list.count { it < 2KeySuffix })
+        assertEquals(0, list.count { it < 0KeySuffix })
+        assertEquals(5, list.count { it < 10KeySuffix })
+    }
+
+    @Test
+    fun isEmpty() {
+        assertFalse(list.isEmpty())
+        assertFalse(list.none())
+        assertTrue(mutablePKeyListOf().isEmpty())
+        assertTrue(mutablePKeyListOf().none())
+    }
+
+    @Test
+    fun isNotEmpty() {
+        assertTrue(list.isNotEmpty())
+        assertTrue(list.any())
+        assertFalse(mutablePKeyListOf().isNotEmpty())
+    }
+
+    @Test
+    fun indices() {
+        assertEquals(IntRange(0, 4), list.indices)
+        assertEquals(IntRange(0, -1), mutablePKeyListOf().indices)
+    }
+
+    @Test
+    fun any() {
+        assertTrue(list.any { it == 5KeySuffix })
+        assertTrue(list.any { it == 1KeySuffix })
+        assertFalse(list.any { it == 0KeySuffix })
+    }
+
+    @Test
+    fun reversedAny() {
+        val reversedList = mutablePKeyListOf()
+        assertFalse(
+            list.reversedAny {
+                reversedList.add(it)
+                false
+            }
+        )
+        val reversedContent = mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix)
+        assertEquals(reversedContent, reversedList)
+
+        val reversedSublist = mutablePKeyListOf()
+        assertTrue(
+            list.reversedAny {
+                reversedSublist.add(it)
+                reversedSublist.size == 2
+            }
+        )
+        assertEquals(reversedSublist, mutablePKeyListOf(5KeySuffix, 4KeySuffix))
+    }
+
+    @Test
+    fun forEach() {
+        val copy = mutablePKeyListOf()
+        list.forEach { copy += it }
+        assertEquals(list, copy)
+    }
+
+    @Test
+    fun forEachReversed() {
+        val copy = mutablePKeyListOf()
+        list.forEachReversed { copy += it }
+        assertEquals(copy, mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix))
+    }
+
+    @Test
+    fun forEachIndexed() {
+        val copy = mutablePKeyListOf()
+        val indices = mutablePKeyListOf()
+        list.forEachIndexed { index, item ->
+            copy += item
+            indices += index.toPKey()
+        }
+        assertEquals(list, copy)
+        assertEquals(indices, mutablePKeyListOf(0KeySuffix, 1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix))
+    }
+
+    @Test
+    fun forEachReversedIndexed() {
+        val copy = mutablePKeyListOf()
+        val indices = mutablePKeyListOf()
+        list.forEachReversedIndexed { index, item ->
+            copy += item
+            indices += index.toPKey()
+        }
+        assertEquals(copy, mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix))
+        assertEquals(indices, mutablePKeyListOf(4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix, 0KeySuffix))
+    }
+
+    @Test
+    fun indexOfFirst() {
+        assertEquals(0, list.indexOfFirst { it == 1KeySuffix })
+        assertEquals(4, list.indexOfFirst { it == 5KeySuffix })
+        assertEquals(-1, list.indexOfFirst { it == 0KeySuffix })
+        assertEquals(0, mutablePKeyListOf(8KeySuffix, 8KeySuffix).indexOfFirst { it == 8KeySuffix })
+    }
+
+    @Test
+    fun indexOfLast() {
+        assertEquals(0, list.indexOfLast { it == 1KeySuffix })
+        assertEquals(4, list.indexOfLast { it == 5KeySuffix })
+        assertEquals(-1, list.indexOfLast { it == 0KeySuffix })
+        assertEquals(1, mutablePKeyListOf(8KeySuffix, 8KeySuffix).indexOfLast { it == 8KeySuffix })
+    }
+
+    @Test
+    fun contains() {
+        assertTrue(list.contains(5KeySuffix))
+        assertTrue(list.contains(1KeySuffix))
+        assertFalse(list.contains(0KeySuffix))
+    }
+
+    @Test
+    fun containsAllList() {
+        assertTrue(list.containsAll(mutablePKeyListOf(2KeySuffix, 3KeySuffix, 1KeySuffix)))
+        assertFalse(list.containsAll(mutablePKeyListOf(2KeySuffix, 3KeySuffix, 6KeySuffix)))
+    }
+
+    @Test
+    fun lastIndexOf() {
+        assertEquals(4, list.lastIndexOf(5KeySuffix))
+        assertEquals(1, list.lastIndexOf(2KeySuffix))
+        val copy = mutablePKeyListOf()
+        copy.addAll(list)
+        copy.addAll(list)
+        assertEquals(5, copy.lastIndexOf(1KeySuffix))
+    }
+
+    @Test
+    fun first() {
+        assertEquals(1KeySuffix, list.first())
+    }
+
+    @Test
+    fun firstException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutablePKeyListOf().first()
+        }
+    }
+
+    @Test
+    fun firstWithPredicate() {
+        assertEquals(5KeySuffix, list.first { it == 5KeySuffix })
+        assertEquals(1KeySuffix, mutablePKeyListOf(1KeySuffix, 5KeySuffix).first { it != 0KeySuffix })
+    }
+
+    @Test
+    fun firstWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutablePKeyListOf().first { it == 8KeySuffix }
+        }
+    }
+
+    @Test
+    fun last() {
+        assertEquals(5KeySuffix, list.last())
+    }
+
+    @Test
+    fun lastException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutablePKeyListOf().last()
+        }
+    }
+
+    @Test
+    fun lastWithPredicate() {
+        assertEquals(1KeySuffix, list.last { it == 1KeySuffix })
+        assertEquals(5KeySuffix, mutablePKeyListOf(1KeySuffix, 5KeySuffix).last { it != 0KeySuffix })
+    }
+
+    @Test
+    fun lastWithPredicateException() {
+        assertFailsWith(NoSuchElementException::class) {
+            mutablePKeyListOf().last { it == 8KeySuffix }
+        }
+    }
+
+    @Test
+    fun fold() {
+        assertEquals("12345", list.fold("") { acc, i -> acc + i.toInt().toString() })
+    }
+
+    @Test
+    fun foldIndexed() {
+        assertEquals(
+            "01-12-23-34-45-",
+            list.foldIndexed("") { index, acc, i ->
+                "$acc$index${i.toInt()}-"
+            }
+        )
+    }
+
+    @Test
+    fun foldRight() {
+        assertEquals("54321", list.foldRight("") { i, acc -> acc + i.toInt().toString() })
+    }
+
+    @Test
+    fun foldRightIndexed() {
+        assertEquals(
+            "45-34-23-12-01-",
+            list.foldRightIndexed("") { index, i, acc ->
+                "$acc$index${i.toInt()}-"
+            }
+        )
+    }
+
+    @Test
+    fun add() {
+        val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+        l += 4KeySuffix
+        l.add(5KeySuffix)
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun addAtIndex() {
+        val l = mutablePKeyListOf(2KeySuffix, 4KeySuffix)
+        l.add(2, 5KeySuffix)
+        l.add(0, 1KeySuffix)
+        l.add(2, 3KeySuffix)
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(-1, 2KeySuffix)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.add(6, 2KeySuffix)
+        }
+    }
+
+    @Test
+    fun addAllListAtIndex() {
+        val l = mutablePKeyListOf(4KeySuffix)
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        val l3 = mutablePKeyListOf(5KeySuffix)
+        val l4 = mutablePKeyListOf(3KeySuffix)
+        assertTrue(l4.addAll(1, l3))
+        assertTrue(l4.addAll(0, l2))
+        assertTrue(l4.addAll(3, l))
+        assertFalse(l4.addAll(0, mutablePKeyListOf()))
+        assertEquals(list, l4)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(6, mutablePKeyListOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l4.addAll(-1, mutablePKeyListOf())
+        }
+    }
+
+    @Test
+    fun addAllList() {
+        val l = MutablePKeyList()
+        l.add(3KeySuffix)
+        l.add(4KeySuffix)
+        l.add(5KeySuffix)
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        assertTrue(l2.addAll(l))
+        assertEquals(list, l2)
+        assertFalse(l2.addAll(mutablePKeyListOf()))
+    }
+
+    @Test
+    fun plusAssignList() {
+        val l = MutablePKeyList()
+        l.add(3KeySuffix)
+        l.add(4KeySuffix)
+        l.add(5KeySuffix)
+        val l2 = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        l2 += l
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun addAllArrayAtIndex() {
+        val a1 = pKeyArrayOf(4KeySuffix)
+        val a2 = pKeyArrayOf(1KeySuffix, 2KeySuffix)
+        val a3 = pKeyArrayOf(5KeySuffix)
+        val l = mutablePKeyListOf(3KeySuffix)
+        assertTrue(l.addAll(1, a3))
+        assertTrue(l.addAll(0, a2))
+        assertTrue(l.addAll(3, a1))
+        assertFalse(l.addAll(0, pKeyArrayOf()))
+        assertEquals(list, l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(6, pKeyArrayOf())
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.addAll(-1, pKeyArrayOf())
+        }
+    }
+
+    @Test
+    fun addAllArray() {
+        val a = pKeyArrayOf(3KeySuffix, 4KeySuffix, 5KeySuffix)
+        val v = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        v.addAll(a)
+        assertEquals(5, v.size)
+        assertEquals(3KeySuffix, v[2])
+        assertEquals(4KeySuffix, v[3])
+        assertEquals(5KeySuffix, v[4])
+    }
+
+    @Test
+    fun plusAssignArray() {
+        val a = pKeyArrayOf(3KeySuffix, 4KeySuffix, 5KeySuffix)
+        val v = mutablePKeyListOf(1KeySuffix, 2KeySuffix)
+        v += a
+        assertEquals(list, v)
+    }
+
+    @Test
+    fun clear() {
+        val l = mutablePKeyListOf()
+        l.addAll(list)
+        assertTrue(l.isNotEmpty())
+        l.clear()
+        assertTrue(l.isEmpty())
+    }
+
+    @Test
+    fun trim() {
+        val l = mutablePKeyListOf(1KeySuffix)
+        l.trim()
+        assertEquals(1, l.capacity)
+        l += pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l.trim()
+        assertEquals(6, l.capacity)
+        assertEquals(6, l.size)
+        l.clear()
+        l.trim()
+        assertEquals(0, l.capacity)
+        l.trim(100)
+        assertEquals(0, l.capacity)
+        l += pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l -= 5KeySuffix
+        l.trim(5)
+        assertEquals(5, l.capacity)
+        l.trim(4)
+        assertEquals(4, l.capacity)
+        l.trim(3)
+        assertEquals(4, l.capacity)
+    }
+
+    @Test
+    fun remove() {
+        val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l.remove(3KeySuffix)
+        assertEquals(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 4KeySuffix, 5KeySuffix), l)
+    }
+
+    @Test
+    fun removeAt() {
+        val l = mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l.removeAt(2)
+        assertEquals(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 4KeySuffix, 5KeySuffix), l)
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(6)
+        }
+        assertFailsWith(IndexOutOfBoundsException::class) {
+            l.removeAt(-1)
+        }
+    }
+
+    @Test
+    fun set() {
+        val l = mutablePKeyListOf(0KeySuffix, 0KeySuffix, 0KeySuffix, 0KeySuffix, 0KeySuffix)
+        l[0] = 1KeySuffix
+        l[4] = 5KeySuffix
+        l[2] = 3KeySuffix
+        l[1] = 2KeySuffix
+        l[3] = 4KeySuffix
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.set(-1, 1KeySuffix)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.set(6, 1KeySuffix)
+        }
+        assertEquals(4KeySuffix, l.set(3, 1KeySuffix));
+    }
+
+    @Test
+    fun ensureCapacity() {
+        val l = mutablePKeyListOf(1KeySuffix)
+        assertEquals(1, l.capacity)
+        l.ensureCapacity(5)
+        assertEquals(5, l.capacity)
+    }
+
+    @Test
+    fun removeAllList() {
+        assertFalse(list.removeAll(mutablePKeyListOf(0KeySuffix, 10KeySuffix, 15KeySuffix)))
+        val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+        assertTrue(l.removeAll(mutablePKeyListOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeAllPKeyArray() {
+        assertFalse(list.removeAll(pKeyArrayOf(0KeySuffix, 10KeySuffix, 15KeySuffix)))
+        val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+        assertTrue(l.removeAll(pKeyArrayOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun minusAssignList() {
+        val l = mutablePKeyListOf().also { it += list }
+        l -= mutablePKeyListOf(0KeySuffix, 10KeySuffix, 15KeySuffix)
+        assertEquals(list, l)
+        val l2 = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+        l2 -= mutablePKeyListOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun minusAssignPKeyArray() {
+        val l = mutablePKeyListOf().also { it += list }
+        l -= pKeyArrayOf(0KeySuffix, 10KeySuffix, 15KeySuffix)
+        assertEquals(list, l)
+        val l2 = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix, 5KeySuffix)
+        l2 -= pKeyArrayOf(20KeySuffix, 0KeySuffix, 15KeySuffix, 10KeySuffix, 5KeySuffix)
+        assertEquals(list, l2)
+    }
+
+    @Test
+    fun retainAll() {
+        assertFalse(list.retainAll(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+        val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix)
+        assertTrue(l.retainAll(mutablePKeyListOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun retainAllPKeyArray() {
+        assertFalse(list.retainAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+        val l = mutablePKeyListOf(0KeySuffix, 1KeySuffix, 15KeySuffix, 10KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 20KeySuffix)
+        assertTrue(l.retainAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 6KeySuffix)))
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun removeRange() {
+        val l = mutablePKeyListOf(1KeySuffix, 9KeySuffix, 7KeySuffix, 6KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix)
+        l.removeRange(1, 4)
+        assertEquals(list, l)
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(6, 6)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(100, 200)
+        }
+        assertFailsWith<IndexOutOfBoundsException> {
+            l.removeRange(-1, 0)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            l.removeRange(3, 2)
+        }
+    }
+
+    @Test
+    fun sort() {
+        val l = mutablePKeyListOf(1KeySuffix, 4KeySuffix, 2KeySuffix, 5KeySuffix, 3KeySuffix)
+        l.sort()
+        assertEquals(list, l)
+    }
+
+    @Test
+    fun sortDescending() {
+        val l = mutablePKeyListOf(1KeySuffix, 4KeySuffix, 2KeySuffix, 5KeySuffix, 3KeySuffix)
+        l.sortDescending()
+        assertEquals(mutablePKeyListOf(5KeySuffix, 4KeySuffix, 3KeySuffix, 2KeySuffix, 1KeySuffix), l)
+    }
+
+    @Test
+    fun testEmptyPKeyList() {
+        val l = emptyPKeyList()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun pKeyListOfEmpty() {
+        val l = pKeyListOf()
+        assertEquals(0, l.size)
+    }
+
+    @Test
+    fun pKeyListOfOneValue() {
+        val l = pKeyListOf(2KeySuffix)
+        assertEquals(1, l.size)
+        assertEquals(2KeySuffix, l[0])
+    }
+
+    @Test
+    fun pKeyListOfTwoValues() {
+        val l = pKeyListOf(2KeySuffix, 1KeySuffix)
+        assertEquals(2, l.size)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(1KeySuffix, l[1])
+    }
+
+    @Test
+    fun pKeyListOfThreeValues() {
+        val l = pKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix)
+        assertEquals(3, l.size)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(10KeySuffix, l[1])
+        assertEquals(-1KeySuffix, l[2])
+    }
+
+    @Test
+    fun pKeyListOfFourValues() {
+        val l = pKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix, 10KeySuffix)
+        assertEquals(4, l.size)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(10KeySuffix, l[1])
+        assertEquals(-1KeySuffix, l[2])
+        assertEquals(10KeySuffix, l[3])
+    }
+
+    @Test
+    fun mutablePKeyListOfOneValue() {
+        val l = mutablePKeyListOf(2KeySuffix)
+        assertEquals(1, l.size)
+        assertEquals(1, l.capacity)
+        assertEquals(2KeySuffix, l[0])
+    }
+
+    @Test
+    fun mutablePKeyListOfTwoValues() {
+        val l = mutablePKeyListOf(2KeySuffix, 1KeySuffix)
+        assertEquals(2, l.size)
+        assertEquals(2, l.capacity)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(1KeySuffix, l[1])
+    }
+
+    @Test
+    fun mutablePKeyListOfThreeValues() {
+        val l = mutablePKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix)
+        assertEquals(3, l.size)
+        assertEquals(3, l.capacity)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(10KeySuffix, l[1])
+        assertEquals(-1KeySuffix, l[2])
+    }
+
+    @Test
+    fun mutablePKeyListOfFourValues() {
+        val l = mutablePKeyListOf(2KeySuffix, 10KeySuffix, -1KeySuffix, 10KeySuffix)
+        assertEquals(4, l.size)
+        assertEquals(4, l.capacity)
+        assertEquals(2KeySuffix, l[0])
+        assertEquals(10KeySuffix, l[1])
+        assertEquals(-1KeySuffix, l[2])
+        assertEquals(10KeySuffix, l[3])
+    }
+}
diff --git a/collection/collection/template/PKeyObjectMap.kt.template b/collection/collection/template/PKeyObjectMap.kt.template
new file mode 100644
index 0000000..2bb53a2b
--- /dev/null
+++ b/collection/collection/template/PKeyObjectMap.kt.template
@@ -0,0 +1,847 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import androidx.collection.internal.EMPTY_OBJECTS
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyPKeyObjectMap = MutablePKeyObjectMap<Nothing>(0)
+
+/**
+ * Returns an empty, read-only [PKeyObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> emptyPKeyObjectMap(): PKeyObjectMap<V> = EmptyPKeyObjectMap as PKeyObjectMap<V>
+
+/**
+ * Returns an empty, read-only [PKeyObjectMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun <V> pKeyObjectMapOf(): PKeyObjectMap<V> = EmptyPKeyObjectMap as PKeyObjectMap<V>
+
+/**
+ * Returns a new [PKeyObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PKey] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> pKeyObjectMapOf(vararg pairs: Pair<PKey, V>): PKeyObjectMap<V> =
+    MutablePKeyObjectMap<V>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutablePKeyObjectMap].
+ */
+public fun <V> mutablePKeyObjectMapOf(): MutablePKeyObjectMap<V> = MutablePKeyObjectMap()
+
+/**
+ * Returns a new [MutablePKeyObjectMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * the [PKey] key is boxed. Use [set] for each entry instead when it is
+ * important to reduce allocations.
+ */
+public fun <V> mutablePKeyObjectMapOf(vararg pairs: Pair<PKey, V>): MutablePKeyObjectMap<V> =
+    MutablePKeyObjectMap<V>(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [PKeyObjectMap] is a container with a [Map]-like interface for keys with
+ * [PKey] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutablePKeyObjectMap].
+ *
+ * @see [MutablePKeyObjectMap]
+ */
+public sealed class PKeyObjectMap<V> {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: PKeyArray = EmptyPKeyArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: Array<Any?> = EMPTY_OBJECTS
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key], or `null` if such
+     * a key is not present in the map.
+     */
+    public operator fun get(key: PKey): V? {
+        val index = findKeyIndex(key)
+        @Suppress("UNCHECKED_CAST")
+        return if (index >= 0) values[index] as V? else null
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: PKey, defaultValue: V): V {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            @Suppress("UNCHECKED_CAST")
+            return values[index] as V
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: PKey, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue()
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: PKey, value: V) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(k[index], v[index] as V)
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: PKey) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: V) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            block(v[index] as V)
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (PKey, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (PKey, V) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (PKey, V) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: V): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [PKeyObjectMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is PKeyObjectMap<*>) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value == null) {
+                if (other[key] != null || !other.containsKey(key)) {
+                    return false
+                }
+            } else if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(if (value === this) "(this)" else value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    internal inline fun findKeyIndex(key: PKey): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutablePKeyObjectMap] is a container with a [MutableMap]-like interface for keys with
+ * [PKey] primitives and reference type values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutablePKeyObjectMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see ScatterMap
+ */
+public class MutablePKeyObjectMap<V>(
+    initialCapacity: Int = DefaultScatterCapacity
+) : PKeyObjectMap<V>() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = PKeyArray(newCapacity)
+        values = arrayOfNulls(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: PKey, defaultValue: () -> V): V {
+        return get(key) ?: defaultValue().also { set(key, it) }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: PKey, value: V) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: PKey, value: V): V? {
+        val index = findAbsoluteInsertIndex(key)
+        val oldValue = values[index]
+        keys[index] = key
+        values[index] = value
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [PKey] key is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<out Pair<PKey, V>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: PKeyObjectMap<V>) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that the [Pair] is allocated and the [PKey] key is boxed.
+     * Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<PKey, V>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * the [PKey] key is boxed. Use [set] for each entry instead when it is
+     * important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<out Pair<PKey, V>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: PKeyObjectMap<V>): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map. If the
+     * [key] was present in the map, this function returns the value that was
+     * present before removal.
+     */
+    public fun remove(key: PKey): V? {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return removeValueAt(index)
+        }
+        return null
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: PKey, value: V): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (PKey, V) -> Boolean) {
+        forEachIndexed { index ->
+            @Suppress("UNCHECKED_CAST")
+            if (predicate(keys[index], values[index] as V)) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: PKey) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: PKeyArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: PKeySet) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: PKeyList) {
+        keys.forEach { key ->
+            minusAssign(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int): V? {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+        val oldValue = values[index]
+        values[index] = null
+
+        @Suppress("UNCHECKED_CAST")
+        return oldValue as V?
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        values.fill(null, 0, _capacity)
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: PKey): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutablePKeyObjectMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/template/PKeyObjectMapTest.kt.template b/collection/collection/template/PKeyObjectMapTest.kt.template
new file mode 100644
index 0000000..d04a330
--- /dev/null
+++ b/collection/collection/template/PKeyObjectMapTest.kt.template
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeyObjectMapTest {
+    @Test
+    fun pKeyObjectMap() {
+        val map = MutablePKeyObjectMap<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyPKeyObjectMap() {
+        val map = emptyPKeyObjectMap<String>()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyPKeyObjectMap<String>(), map)
+    }
+
+    @Test
+    fun pKeyObjectMapFunction() {
+        val map = mutablePKeyObjectMapOf<String>()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutablePKeyObjectMap<String>(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun pKeyObjectMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutablePKeyObjectMap<String>(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun pKeyObjectMapPairsFunction() {
+        val map = mutablePKeyObjectMapOf(
+            1KeySuffix to "World",
+            2KeySuffix to "Monde"
+        )
+        assertEquals(2, map.size)
+        assertEquals("World", map[1KeySuffix])
+        assertEquals("Monde", map[2KeySuffix])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun insertIndex0() {
+        val map = MutablePKeyObjectMap<String>()
+        map.put(1KeySuffix, "World")
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutablePKeyObjectMap<String>(12)
+        map[1KeySuffix] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutablePKeyObjectMap<String>(2)
+        map[1KeySuffix] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutablePKeyObjectMap<String>(0)
+        map[1KeySuffix] = "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[1KeySuffix] = "Monde"
+
+        assertEquals(1, map.size)
+        assertEquals("Monde", map[1KeySuffix])
+    }
+
+    @Test
+    fun put() {
+        val map = MutablePKeyObjectMap<String?>()
+
+        assertNull(map.put(1KeySuffix, "World"))
+        assertEquals("World", map.put(1KeySuffix, "Monde"))
+        assertNull(map.put(2KeySuffix, null))
+        assertNull(map.put(2KeySuffix, "Monde"))
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        map.putAll(arrayOf(3KeySuffix to "Welt", 7KeySuffix to "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3KeySuffix])
+        assertEquals("Mundo", map[7KeySuffix])
+    }
+
+    @Test
+    fun putAllMap() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        map.putAll(mutablePKeyObjectMapOf(3KeySuffix to "Welt", 7KeySuffix to "Mundo"))
+
+        assertEquals(4, map.size)
+        assertEquals("Welt", map[3KeySuffix])
+        assertEquals("Mundo", map[7KeySuffix])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutablePKeyObjectMap<String>()
+        map += 1KeySuffix to "World"
+
+        assertEquals(1, map.size)
+        assertEquals("World", map[1KeySuffix])
+    }
+
+    @Test
+    fun plusMap() {
+        val map = MutablePKeyObjectMap<String>()
+        map += pKeyObjectMapOf(3KeySuffix to "Welt", 7KeySuffix to "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3KeySuffix])
+        assertEquals("Mundo", map[7KeySuffix])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutablePKeyObjectMap<String>()
+        map += arrayOf(3KeySuffix to "Welt", 7KeySuffix to "Mundo")
+
+        assertEquals(2, map.size)
+        assertEquals("Welt", map[3KeySuffix])
+        assertEquals("Mundo", map[7KeySuffix])
+    }
+
+    @Test
+    fun nullValue() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = null
+
+        assertEquals(1, map.size)
+        assertNull(map[1KeySuffix])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+
+        assertNull(map[2KeySuffix])
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+
+        assertEquals("Monde", map.getOrDefault(2KeySuffix, "Monde"))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertEquals("Monde", map.getOrElse(2KeySuffix) { "Monde" })
+        assertEquals("Welt", map.getOrElse(3KeySuffix) { "Welt" })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+
+        var counter = 0
+        map.getOrPut(1KeySuffix) {
+            counter++
+            "Monde"
+        }
+        assertEquals("World", map[1KeySuffix])
+        assertEquals(0, counter)
+
+        map.getOrPut(2KeySuffix) {
+            counter++
+            "Monde"
+        }
+        assertEquals("Monde", map[2KeySuffix])
+        assertEquals(1, counter)
+
+        map.getOrPut(2KeySuffix) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Monde", map[2KeySuffix])
+        assertEquals(1, counter)
+
+        map.getOrPut(3KeySuffix) {
+            counter++
+            null
+        }
+        assertNull(map[3KeySuffix])
+        assertEquals(2, counter)
+
+        map.getOrPut(3KeySuffix) {
+            counter++
+            "Welt"
+        }
+        assertEquals("Welt", map[3KeySuffix])
+        assertEquals(3, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutablePKeyObjectMap<String?>()
+        assertNull(map.remove(1KeySuffix))
+
+        map[1KeySuffix] = "World"
+        assertEquals("World", map.remove(1KeySuffix))
+        assertEquals(0, map.size)
+
+        map[1KeySuffix] = null
+        assertNull(map.remove(1KeySuffix))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutablePKeyObjectMap<String>(6)
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1KeySuffix)
+        map.remove(2KeySuffix)
+        map.remove(3KeySuffix)
+        map.remove(4KeySuffix)
+        map.remove(5KeySuffix)
+        map.remove(6KeySuffix)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7KeySuffix] = "Mundo"
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        map.removeIf { key, value ->
+            key == 1KeySuffix || key == 3KeySuffix || value.startsWith('S')
+        }
+
+        assertEquals(2, map.size)
+        assertEquals("Monde", map[2KeySuffix])
+        assertEquals("Mondo", map[5KeySuffix])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+
+        map -= 1KeySuffix
+
+        assertEquals(2, map.size)
+        assertNull(map[1KeySuffix])
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+
+        map -= pKeyArrayOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertNull(map[3KeySuffix])
+        assertNull(map[2KeySuffix])
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+
+        map -= pKeySetOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertNull(map[3KeySuffix])
+        assertNull(map[2KeySuffix])
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+
+        map -= pKeyListOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertNull(map[3KeySuffix])
+        assertNull(map[2KeySuffix])
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutablePKeyObjectMap<String?>()
+        assertFalse(map.remove(1KeySuffix, "World"))
+
+        map[1KeySuffix] = "World"
+        assertTrue(map.remove(1KeySuffix, "World"))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutablePKeyObjectMap<String>()
+
+        for (i in 0 until 1700) {
+            map[i.toPKey()] = i.toString()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutablePKeyObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key.toInt().toString(), value)
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutablePKeyObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachKey { key ->
+                assertEquals(key.toInt().toString(), map[key])
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutablePKeyObjectMap<String>()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toString()
+            }
+
+            var counter = 0
+            map.forEachValue { value ->
+                assertNotNull(value.toIntOrNull())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutablePKeyObjectMap<String>()
+
+        for (i in 0 until 32) {
+            map[i.toPKey()] = i.toString()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutablePKeyObjectMap<String?>()
+        assertEquals("{}", map.toString())
+
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        val oneKey = 1KeySuffix.toString()
+        val twoKey = 2KeySuffix.toString()
+        assertTrue(
+            "{$oneKey=World, $twoKey=Monde}" == map.toString() ||
+                "{$twoKey=Monde, $oneKey=World}" == map.toString()
+        )
+
+        map.clear()
+        map[1KeySuffix] = null
+        assertEquals("{$oneKey=null}", map.toString())
+
+        val selfAsValueMap = MutablePKeyObjectMap<Any>()
+        selfAsValueMap[1KeySuffix] = selfAsValueMap
+        assertEquals("{$oneKey=(this)}", selfAsValueMap.toString())
+    }
+
+    @Test
+    fun equals() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutablePKeyObjectMap<String?>()
+        map2[2KeySuffix] = null
+
+        assertNotEquals(map, map2)
+
+        map2[1KeySuffix] = "World"
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertTrue(map.containsKey(1KeySuffix))
+        assertFalse(map.containsKey(3KeySuffix))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertTrue(1KeySuffix in map)
+        assertFalse(3KeySuffix in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutablePKeyObjectMap<String?>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = null
+
+        assertTrue(map.containsValue("World"))
+        assertTrue(map.containsValue(null))
+        assertFalse(map.containsValue("Monde"))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutablePKeyObjectMap<String?>()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1KeySuffix] = "World"
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutablePKeyObjectMap<String>()
+        assertEquals(0, map.count())
+
+        map[1KeySuffix] = "World"
+        assertEquals(1, map.count())
+
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        assertEquals(2, map.count { key, _ -> key < 3KeySuffix })
+        assertEquals(0, map.count { key, _ -> key < 0KeySuffix })
+    }
+
+    @Test
+    fun any() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        assertTrue(map.any { key, _ -> key > 5KeySuffix })
+        assertFalse(map.any { key, _ -> key < 0KeySuffix })
+    }
+
+    @Test
+    fun all() {
+        val map = MutablePKeyObjectMap<String>()
+        map[1KeySuffix] = "World"
+        map[2KeySuffix] = "Monde"
+        map[3KeySuffix] = "Welt"
+        map[4KeySuffix] = "Sekai"
+        map[5KeySuffix] = "Mondo"
+        map[6KeySuffix] = "Sesang"
+
+        assertTrue(map.all { key, value -> key < 7KeySuffix && value.length > 0 })
+        assertFalse(map.all { key, _ -> key < 6KeySuffix })
+    }
+}
diff --git a/collection/collection/template/PKeyPValueMap.kt.template b/collection/collection/template/PKeyPValueMap.kt.template
new file mode 100644
index 0000000..0d0a6fe
--- /dev/null
+++ b/collection/collection/template/PKeyPValueMap.kt.template
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// Default empty map to avoid allocations
+private val EmptyPKeyPValueMap = MutablePKeyPValueMap(0)
+
+/**
+ * Returns an empty, read-only [PKeyPValueMap].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun emptyPKeyPValueMap(): PKeyPValueMap = EmptyPKeyPValueMap
+
+/**
+ * Returns a new [MutablePKeyPValueMap].
+ */
+public fun pKeyPValueMapOf(): PKeyPValueMap = EmptyPKeyPValueMap
+
+/**
+ * Returns a new [PKeyPValueMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [PKey] key and [PValue] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun pKeyPValueMapOf(vararg pairs: Pair<PKey, PValue>): PKeyPValueMap =
+    MutablePKeyPValueMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * Returns a new [MutablePKeyPValueMap].
+ */
+public fun mutablePKeyPValueMapOf(): MutablePKeyPValueMap = MutablePKeyPValueMap()
+
+/**
+ * Returns a new [MutablePKeyPValueMap] with the specified contents, given as
+ * a list of pairs where the first component is the key and the second
+ * is the value. If multiple pairs have the same key, the resulting map
+ * will contain the value from the last of those pairs.
+ *
+ * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+ * both the [PKey] key and [PValue] value are boxed. Use [set] for each
+ * entry instead when it is important to reduce allocations.
+ */
+public fun mutablePKeyPValueMapOf(vararg pairs: Pair<PKey, PValue>): MutablePKeyPValueMap =
+    MutablePKeyPValueMap(pairs.size).also { map ->
+        pairs.forEach { (key, value) -> map[key] = value }
+    }
+
+/**
+ * [PKeyPValueMap] is a container with a [Map]-like interface for
+ * [PKey] primitive keys and [PValue] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * This implementation is read-only and only allows data to be queried. A
+ * mutable implementation is provided by [MutablePKeyPValueMap].
+ *
+ * @see [MutablePKeyPValueMap]
+ * @see [ScatterMap]
+ */
+public sealed class PKeyPValueMap {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` entries, including when
+    // the table is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var keys: PKeyArray = EmptyPKeyArray
+
+    @PublishedApi
+    @JvmField
+    internal var values: PValueArray = EmptyPValueArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of key-value pairs that can be stored in this map
+     * without requiring internal storage reallocation.
+     */
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @Suppress("PropertyName")
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of key-value pairs in this map.
+     */
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this map has at least one entry.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this map has no entries.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this map is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this map is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the value corresponding to the given [key].
+     * @throws NoSuchElementException if [key] is not in the map
+     */
+    public operator fun get(key: PKey): PValue {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            throw NoSuchElementException("Cannot find value for key $key")
+        }
+        return values[index]
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * or [defaultValue] if this map contains no mapping for the key.
+     */
+    public fun getOrDefault(key: PKey, defaultValue: PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            return values[index]
+        }
+        return defaultValue
+    }
+
+    /**
+     * Returns the value for the given [key] if the value is present
+     * and not null. Otherwise, returns the result of the [defaultValue]
+     * function.
+     */
+    public inline fun getOrElse(key: PKey, defaultValue: () -> PValue): PValue {
+        val index = findKeyIndex(key)
+        if (index < 0) {
+            return defaultValue()
+        }
+        return values[index]
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndexed(block: (index: Int) -> Unit) {
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 entries
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every key/value pair stored in this map by invoking
+     * the specified [block] lambda.
+     */
+    public inline fun forEach(block: (key: PKey, value: PValue) -> Unit) {
+        val k = keys
+        val v = values
+
+        forEachIndexed { index ->
+            block(k[index], v[index])
+        }
+    }
+
+    /**
+     * Iterates over every key stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachKey(block: (key: PKey) -> Unit) {
+        val k = keys
+
+        forEachIndexed { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Iterates over every value stored in this map by invoking the specified
+     * [block] lambda.
+     */
+    public inline fun forEachValue(block: (value: PValue) -> Unit) {
+        val v = values
+
+        forEachIndexed { index ->
+            block(v[index])
+        }
+    }
+
+    /**
+     * Returns true if all entries match the given [predicate].
+     */
+    public inline fun all(predicate: (PKey, PValue) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (!predicate(key, value)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one entry matches the given [predicate].
+     */
+    public inline fun any(predicate: (PKey, PValue) -> Boolean): Boolean {
+        forEach { key, value ->
+            if (predicate(key, value)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     */
+    public fun count(): Int = size
+
+    /**
+     * Returns the number of entries matching the given [predicate].
+     */
+    public inline fun count(predicate: (PKey, PValue) -> Boolean): Int {
+        var count = 0
+        forEach { key, value ->
+            if (predicate(key, value)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public operator fun contains(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [key] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsKey(key: PKey): Boolean = findKeyIndex(key) >= 0
+
+    /**
+     * Returns true if the specified [value] is present in this hash map, false
+     * otherwise.
+     */
+    public fun containsValue(value: PValue): Boolean {
+        forEachValue { v ->
+            if (value == v) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the hash code value for this map. The hash code the sum of the hash
+     * codes of each key/value pair.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { key, value ->
+            hash += key.hashCode() xor value.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this hash map for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [PKeyPValueMap]
+     * - Has the same [size] as this map
+     * - Contains key/value pairs equal to this map's pair
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is PKeyPValueMap) {
+            return false
+        }
+        if (other.size != size) {
+            return false
+        }
+
+        forEach { key, value ->
+            if (value != other[key]) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this map. The map is denoted in the
+     * string by the `{}`. Each key/value pair present in the map is represented
+     * inside '{}` by a substring of the form `key=value`, and pairs are
+     * separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "{}"
+        }
+
+        val s = StringBuilder().append('{')
+        var i = 0
+        forEach { key, value ->
+            s.append(key)
+            s.append("=")
+            s.append(value)
+            i++
+            if (i < _size) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append('}').toString()
+    }
+
+    /**
+     * Scans the hash table to find the index in the backing arrays of the
+     * specified [key]. Returns -1 if the key is not present.
+     */
+    @PublishedApi
+    internal fun findKeyIndex(key: PKey): Int {
+        val hash = hash(key)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutablePKeyPValueMap] is a container with a [MutableMap]-like interface for
+ * [PKey] primitive keys and [PValue] primitive values.
+ *
+ * The underlying implementation is designed to avoid allocations from boxing,
+ * and insertion, removal, retrieval, and iteration operations. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added entries to the table. In addition, this implementation
+ * minimizes memory usage by avoiding the use of separate objects to hold
+ * key/value pairs.
+ *
+ * This implementation makes no guarantee as to the order of the keys and
+ * values stored, nor does it make guarantees that the order remains constant
+ * over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the map (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Multiple threads are safe to read from this
+ * map concurrently if no write is happening.
+ *
+ * @constructor Creates a new [MutablePKeyPValueMap]
+ * @param initialCapacity The initial desired capacity for this container.
+ * the container will honor this value by guaranteeing its internal structures
+ * can hold that many entries without requiring any allocations. The initial
+ * capacity can be set to 0.
+ *
+ * @see MutableScatterMap
+ */
+public class MutablePKeyPValueMap(
+    initialCapacity: Int = DefaultScatterCapacity
+) : PKeyPValueMap() {
+    // Number of entries we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        keys = PKeyArray(newCapacity)
+        values = PValueArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Returns the value to which the specified [key] is mapped,
+     * if the value is present in the map and not `null`. Otherwise,
+     * calls `defaultValue()` and puts the result in the map associated
+     * with [key].
+     */
+    public inline fun getOrPut(key: PKey, defaultValue: () -> PValue): PValue {
+        val index = findKeyIndex(key)
+        return if (index < 0) {
+            val defValue = defaultValue()
+            put(key, defValue)
+            defValue
+        } else {
+            values[index]
+        }
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations.
+     */
+    public operator fun set(key: PKey, value: PValue) {
+        val index = findAbsoluteInsertIndex(key)
+        keys[index] = key
+        values[index] = value
+    }
+
+    /**
+     * Creates a new mapping from [key] to [value] in this map. If [key] is
+     * already present in the map, the association is modified and the previously
+     * associated value is replaced with [value]. If [key] is not present, a new
+     * entry is added to the map, which may require to grow the underlying storage
+     * and cause allocations. Return the previous value associated with the [key],
+     * or `null` if the key was not present in the map.
+     */
+    public fun put(key: PKey, value: PValue) {
+        set(key, value)
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [PKey] key and [PValue] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public fun putAll(@Suppress("ArrayReturn") pairs: Array<Pair<PKey, PValue>>) {
+        for ((key, value) in pairs) {
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public fun putAll(from: PKeyPValueMap) {
+        from.forEach { key, value ->
+            this[key] = value
+        }
+    }
+
+    /**
+     * Puts the key/value mapping from the [pair] in this map, using the first
+     * element as the key, and the second element as the value.
+     *
+     * Note that [pair] allocated and both the [PKey] key and [PValue] value are
+     * boxed. Use [set] instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(pair: Pair<PKey, PValue>) {
+        this[pair.first] = pair.second
+    }
+
+    /**
+     * Puts all the [pairs] into this map, using the first component of the pair
+     * as the key, and the second component as the value.
+     *
+     * Note that [pairs] is an allocated array, and each [Pair] is allocated and
+     * both the [PKey] key and [PValue] value are boxed. Use [set] for each
+     * entry instead when it is important to reduce allocations.
+     */
+    public inline operator fun plusAssign(
+        @Suppress("ArrayReturn") pairs: Array<Pair<PKey, PValue>>
+    ): Unit = putAll(pairs)
+
+    /**
+     * Puts all the key/value mappings in the [from] map into this map.
+     */
+    public inline operator fun plusAssign(from: PKeyPValueMap): Unit = putAll(from)
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public fun remove(key: PKey) {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            removeValueAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map if the
+     * associated value equals [value]. Returns whether the removal happened.
+     */
+    public fun remove(key: PKey, value: PValue): Boolean {
+        val index = findKeyIndex(key)
+        if (index >= 0) {
+            if (values[index] == value) {
+                removeValueAt(index)
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Removes any mapping for which the specified [predicate] returns true.
+     */
+    public fun removeIf(predicate: (PKey, PValue) -> Boolean) {
+        forEachIndexed { index ->
+            if (predicate(keys[index], values[index])) {
+                removeValueAt(index)
+            }
+        }
+    }
+
+    /**
+     * Removes the specified [key] and its associated value from the map.
+     */
+    public inline operator fun minusAssign(key: PKey) {
+        remove(key)
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(@Suppress("ArrayReturn") keys: PKeyArray) {
+        for (key in keys) {
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: PKeySet) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    /**
+     * Removes the specified [keys] and their associated value from the map.
+     */
+    public inline operator fun minusAssign(keys: PKeyList) {
+        keys.forEach { key ->
+            remove(key)
+        }
+    }
+
+    private fun removeValueAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the entry as empty if there's a group
+        //       window around this entry that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all mappings from this map.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the hash table to find the index at which we can store a value
+     * for the give [key]. If the key already exists in the table, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the table is full.
+     */
+    private fun findAbsoluteInsertIndex(key: PKey): Int {
+        val hash = hash(key)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (keys[index] == key) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the table in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutablePKeyPValueMap]'s storage so it is sized appropriately
+     * to hold the current mappings.
+     *
+     * Returns the number of empty entries removed from this map's storage.
+     * Returns be 0 if no trimming is necessary or possible.
+     */
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted entries from the table to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the table capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_set`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousKeys = keys
+        val previousValues = values
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newKeys = keys
+        val newValues = values
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousKey = previousKeys[i]
+                val hash = hash(previousKey)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newKeys[index] = previousKey
+                newValues[index] = previousValues[i]
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
diff --git a/collection/collection/template/PKeyPValueMapTest.kt.template b/collection/collection/template/PKeyPValueMapTest.kt.template
new file mode 100644
index 0000000..c8f3cd5
--- /dev/null
+++ b/collection/collection/template/PKeyPValueMapTest.kt.template
@@ -0,0 +1,602 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+//
+// Note that there are 3 templates for maps, one for object-to-primitive, one
+// for primitive-to-object and one for primitive-to-primitive. Also, the
+// object-to-object is ScatterMap.kt, which doesn't have a template.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+@Suppress("RemoveRedundantCallsOfConversionMethods")
+class PKeyPValueMapTest {
+    @Test
+    fun pKeyPValueMap() {
+        val map = MutablePKeyPValueMap()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun testEmptyPKeyPValueMap() {
+        val map = emptyPKeyPValueMap()
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+
+        assertSame(emptyPKeyPValueMap(), map)
+    }
+
+    @Test
+    fun pKeyPValueMapFunction() {
+        val map = mutablePKeyPValueMapOf()
+        assertEquals(7, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun zeroCapacityHashMap() {
+        val map = MutablePKeyPValueMap(0)
+        assertEquals(0, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun pKeyPValueMapWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val map = MutablePKeyPValueMap(1800)
+        assertEquals(4095, map.capacity)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun pKeyPValueMapPairsFunction() {
+        val map = mutablePKeyPValueMapOf(
+            1KeySuffix to 1ValueSuffix,
+            2KeySuffix to 2ValueSuffix
+        )
+        assertEquals(2, map.size)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+        assertEquals(2ValueSuffix, map[2KeySuffix])
+    }
+
+    @Test
+    fun addToMap() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun addToSizedMap() {
+        val map = MutablePKeyPValueMap(12)
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun addToSmallMap() {
+        val map = MutablePKeyPValueMap(2)
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(7, map.capacity)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun addToZeroCapacityMap() {
+        val map = MutablePKeyPValueMap(0)
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun replaceExistingKey() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[1KeySuffix] = 2ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(2ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun put() {
+        val map = MutablePKeyPValueMap()
+
+        map.put(1KeySuffix, 1ValueSuffix)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+        map.put(1KeySuffix, 2ValueSuffix)
+        assertEquals(2ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun putAllArray() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+
+        map.putAll(arrayOf(3KeySuffix to 3ValueSuffix, 7KeySuffix to 7ValueSuffix))
+
+        assertEquals(4, map.size)
+        assertEquals(3ValueSuffix, map[3KeySuffix])
+        assertEquals(7ValueSuffix, map[7KeySuffix])
+    }
+
+    @Test
+    fun plus() {
+        val map = MutablePKeyPValueMap()
+        map += 1KeySuffix to 1ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+    }
+
+    @Test
+    fun plusArray() {
+        val map = MutablePKeyPValueMap()
+        map += arrayOf(3KeySuffix to 3ValueSuffix, 7KeySuffix to 7ValueSuffix)
+
+        assertEquals(2, map.size)
+        assertEquals(3ValueSuffix, map[3KeySuffix])
+        assertEquals(7ValueSuffix, map[7KeySuffix])
+    }
+
+    @Test
+    fun findNonExistingKey() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertFailsWith<NoSuchElementException> {
+            map[2KeySuffix]
+        }
+    }
+
+    @Test
+    fun getOrDefault() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(2ValueSuffix, map.getOrDefault(2KeySuffix, 2ValueSuffix))
+    }
+
+    @Test
+    fun getOrElse() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertEquals(3ValueSuffix, map.getOrElse(3KeySuffix) { 3ValueSuffix })
+    }
+
+    @Test
+    fun getOrPut() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        var counter = 0
+        map.getOrPut(1KeySuffix) {
+            counter++
+            2ValueSuffix
+        }
+        assertEquals(1ValueSuffix, map[1KeySuffix])
+        assertEquals(0, counter)
+
+        map.getOrPut(2KeySuffix) {
+            counter++
+            2ValueSuffix
+        }
+        assertEquals(2ValueSuffix, map[2KeySuffix])
+        assertEquals(1, counter)
+
+        map.getOrPut(2KeySuffix) {
+            counter++
+            3ValueSuffix
+        }
+        assertEquals(2ValueSuffix, map[2KeySuffix])
+        assertEquals(1, counter)
+
+        map.getOrPut(3KeySuffix) {
+            counter++
+            3ValueSuffix
+        }
+        assertEquals(3ValueSuffix, map[3KeySuffix])
+        assertEquals(2, counter)
+    }
+
+    @Test
+    fun remove() {
+        val map = MutablePKeyPValueMap()
+        map.remove(1KeySuffix)
+
+        map[1KeySuffix] = 1ValueSuffix
+        map.remove(1KeySuffix)
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val map = MutablePKeyPValueMap(6)
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        // Removing all the entries will mark the medata as deleted
+        map.remove(1KeySuffix)
+        map.remove(2KeySuffix)
+        map.remove(3KeySuffix)
+        map.remove(4KeySuffix)
+        map.remove(5KeySuffix)
+        map.remove(6KeySuffix)
+
+        assertEquals(0, map.size)
+
+        val capacity = map.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        map[7KeySuffix] = 7ValueSuffix
+
+        assertEquals(1, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun removeIf() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        map.removeIf { key, _ -> key == 1KeySuffix || key == 3KeySuffix }
+
+        assertEquals(4, map.size)
+        assertEquals(2ValueSuffix, map[2KeySuffix])
+        assertEquals(4ValueSuffix, map[4KeySuffix])
+        assertEquals(5ValueSuffix, map[5KeySuffix])
+        assertEquals(6ValueSuffix, map[6KeySuffix])
+    }
+
+    @Test
+    fun minus() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        map -= 1KeySuffix
+
+        assertEquals(2, map.size)
+        assertFalse(1KeySuffix in map)
+    }
+
+    @Test
+    fun minusArray() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        map -= pKeyArrayOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertFalse(3KeySuffix in map)
+        assertFalse(2KeySuffix in map)
+    }
+
+    @Test
+    fun minusSet() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        map -= pKeySetOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertFalse(3KeySuffix in map)
+        assertFalse(2KeySuffix in map)
+    }
+
+    @Test
+    fun minusList() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        map -= pKeyListOf(3KeySuffix, 2KeySuffix)
+
+        assertEquals(1, map.size)
+        assertFalse(3KeySuffix in map)
+        assertFalse(2KeySuffix in map)
+    }
+
+    @Test
+    fun conditionalRemove() {
+        val map = MutablePKeyPValueMap()
+        assertFalse(map.remove(1KeySuffix, 1ValueSuffix))
+
+        map[1KeySuffix] = 1ValueSuffix
+        assertTrue(map.remove(1KeySuffix, 1ValueSuffix))
+        assertEquals(0, map.size)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val map = MutablePKeyPValueMap()
+
+        for (i in 0 until 1700) {
+            map[i.toPKey()] = i.toPValue()
+        }
+
+        assertEquals(1700, map.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val map = MutablePKeyPValueMap()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toPValue()
+            }
+
+            var counter = 0
+            map.forEach { key, value ->
+                assertEquals(key, value.toPKey())
+                counter++
+            }
+
+            assertEquals(i, counter)
+        }
+    }
+
+    @Test
+    fun forEachKey() {
+        for (i in 0..48) {
+            val map = MutablePKeyPValueMap()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toPValue()
+            }
+
+            var counter = 0
+            val keys = BooleanArray(map.size)
+            map.forEachKey { key ->
+                keys[key.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            keys.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun forEachValue() {
+        for (i in 0..48) {
+            val map = MutablePKeyPValueMap()
+
+            for (j in 0 until i) {
+                map[j.toPKey()] = j.toPValue()
+            }
+
+            var counter = 0
+            val values = BooleanArray(map.size)
+            map.forEachValue { value ->
+                values[value.toInt()] = true
+                counter++
+            }
+
+            assertEquals(i, counter)
+            values.forEach { assertTrue(it) }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val map = MutablePKeyPValueMap()
+
+        for (i in 0 until 32) {
+            map[i.toPKey()] = i.toPValue()
+        }
+
+        val capacity = map.capacity
+        map.clear()
+
+        assertEquals(0, map.size)
+        assertEquals(capacity, map.capacity)
+    }
+
+    @Test
+    fun string() {
+        val map = MutablePKeyPValueMap()
+        assertEquals("{}", map.toString())
+
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        val oneValueString = 1ValueSuffix.toString()
+        val twoValueString = 2ValueSuffix.toString()
+        val oneKeyString = 1KeySuffix.toString()
+        val twoKeyString = 2KeySuffix.toString()
+        assertTrue(
+            "{$oneKeyString=$oneValueString, $twoKeyString=$twoValueString}" == map.toString() ||
+                "{$twoKeyString=$twoValueString, $oneKeyString=$oneValueString}" == map.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertFalse(map.equals(null))
+        assertEquals(map, map)
+
+        val map2 = MutablePKeyPValueMap()
+        assertNotEquals(map, map2)
+
+        map2[1KeySuffix] = 1ValueSuffix
+        assertEquals(map, map2)
+    }
+
+    @Test
+    fun containsKey() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertTrue(map.containsKey(1KeySuffix))
+        assertFalse(map.containsKey(2KeySuffix))
+    }
+
+    @Test
+    fun contains() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertTrue(1KeySuffix in map)
+        assertFalse(2KeySuffix in map)
+    }
+
+    @Test
+    fun containsValue() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertTrue(map.containsValue(1ValueSuffix))
+        assertFalse(map.containsValue(3ValueSuffix))
+    }
+
+    @Test
+    fun empty() {
+        val map = MutablePKeyPValueMap()
+        assertTrue(map.isEmpty())
+        assertFalse(map.isNotEmpty())
+        assertTrue(map.none())
+        assertFalse(map.any())
+
+        map[1KeySuffix] = 1ValueSuffix
+
+        assertFalse(map.isEmpty())
+        assertTrue(map.isNotEmpty())
+        assertTrue(map.any())
+        assertFalse(map.none())
+    }
+
+    @Test
+    fun count() {
+        val map = MutablePKeyPValueMap()
+        assertEquals(0, map.count())
+
+        map[1KeySuffix] = 1ValueSuffix
+        assertEquals(1, map.count())
+
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        assertEquals(2, map.count { key, _ -> key <= 2KeySuffix })
+        assertEquals(0, map.count { key, _ -> key < 0KeySuffix })
+    }
+
+    @Test
+    fun any() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        assertTrue(map.any { key, _ -> key == 4KeySuffix })
+        assertFalse(map.any { key, _ -> key < 0KeySuffix })
+    }
+
+    @Test
+    fun all() {
+        val map = MutablePKeyPValueMap()
+        map[1KeySuffix] = 1ValueSuffix
+        map[2KeySuffix] = 2ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+        map[4KeySuffix] = 4ValueSuffix
+        map[5KeySuffix] = 5ValueSuffix
+        map[6KeySuffix] = 6ValueSuffix
+
+        assertTrue(map.all { key, value -> key > 0KeySuffix && value >= 1ValueSuffix })
+        assertFalse(map.all { key, _ -> key < 6KeySuffix })
+    }
+
+    @Test
+    fun trim() {
+        val map = MutablePKeyPValueMap()
+        assertEquals(7, map.trim())
+
+        map[1KeySuffix] = 1ValueSuffix
+        map[3KeySuffix] = 3ValueSuffix
+
+        assertEquals(0, map.trim())
+
+        for (i in 0 until 1700) {
+            map[i.toPKey()] = i.toPValue()
+        }
+
+        assertEquals(2047, map.capacity)
+
+        // After removing these items, our capacity needs should go
+        // from 2047 down to 1023
+        for (i in 0 until 1700) {
+            if (i and 0x1 == 0x0) {
+                val s = i.toPKey()
+                map.remove(s)
+            }
+        }
+
+        assertEquals(1024, map.trim())
+        assertEquals(0, map.trim())
+    }
+}
diff --git a/collection/collection/template/PKeySet.kt.template b/collection/collection/template/PKeySet.kt.template
new file mode 100644
index 0000000..f950f03
--- /dev/null
+++ b/collection/collection/template/PKeySet.kt.template
@@ -0,0 +1,784 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+    "RedundantVisibilityModifier",
+    "KotlinRedundantDiagnosticSuppress",
+    "KotlinConstantConditions",
+    "PropertyName",
+    "ConstPropertyName",
+    "PrivatePropertyName",
+    "NOTHING_TO_INLINE"
+)
+
+package androidx.collection
+
+import kotlin.contracts.contract
+import kotlin.jvm.JvmField
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+// This is a copy of ScatterSet, but with primitive elements
+
+// Default empty set to avoid allocations
+private val EmptyPKeySet = MutablePKeySet(0)
+
+// An empty array of pKeys
+internal val EmptyPKeyArray = PKeyArray(0)
+
+/**
+ * Returns an empty, read-only [PKeySet].
+ */
+public fun emptyPKeySet(): PKeySet = EmptyPKeySet
+
+/**
+ * Returns an empty, read-only [ScatterSet].
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(): PKeySet = EmptyPKeySet
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey): PKeySet = mutablePKeySetOf(element1)
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1] and [element2] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey, element2: PKey): PKeySet =
+    mutablePKeySetOf(element1, element2)
+
+/**
+ * Returns a new read-only [PKeySet] with only [element1], [element2], and [element3] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(element1: PKey, element2: PKey, element3: PKey): PKeySet =
+    mutablePKeySetOf(element1, element2, element3)
+
+/**
+ * Returns a new read-only [PKeySet] with only [elements] in it.
+ */
+@Suppress("UNCHECKED_CAST")
+public fun pKeySetOf(vararg elements: PKey): PKeySet =
+    MutablePKeySet(elements.size).apply { plusAssign(elements) }
+
+/**
+ * Returns a new [MutablePKeySet].
+ */
+public fun mutablePKeySetOf(): MutablePKeySet = MutablePKeySet()
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey): MutablePKeySet =
+    MutablePKeySet(1).apply {
+        plusAssign(element1)
+    }
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1] and [element2] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey, element2: PKey): MutablePKeySet =
+    MutablePKeySet(2).apply {
+        plusAssign(element1)
+        plusAssign(element2)
+    }
+
+/**
+ * Returns a new [MutablePKeySet] with only [element1], [element2], and [element3] in it.
+ */
+public fun mutablePKeySetOf(element1: PKey, element2: PKey, element3: PKey): MutablePKeySet =
+    MutablePKeySet(3).apply {
+        plusAssign(element1)
+        plusAssign(element2)
+        plusAssign(element3)
+    }
+
+/**
+ * Returns a new [MutablePKeySet] with the specified elements.
+ */
+public fun mutablePKeySetOf(vararg elements: PKey): MutablePKeySet =
+    MutablePKeySet(elements.size).apply { plusAssign(elements) }
+
+/**
+ * [PKeySet] is a container with a [Set]-like interface designed to avoid
+ * allocations, including boxing.
+ *
+ * This implementation makes no guarantee as to the order of the elements,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * Though [PKeySet] offers a read-only interface, it is always backed
+ * by a [MutablePKeySet]. Read operations alone are thread-safe. However,
+ * any mutations done through the backing [MutablePKeySet] while reading
+ * on another thread are not safe and the developer must protect the set
+ * from such changes during read operations.
+ *
+ * @see [MutablePKeySet]
+ */
+public sealed class PKeySet {
+    // NOTE: Our arrays are marked internal to implement inlined forEach{}
+    // The backing array for the metadata bytes contains
+    // `capacity + 1 + ClonedMetadataCount` elements, including when
+    // the set is empty (see [EmptyGroup]).
+    @PublishedApi
+    @JvmField
+    internal var metadata: LongArray = EmptyGroup
+
+    @PublishedApi
+    @JvmField
+    internal var elements: PKeyArray = EmptyPKeyArray
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the capacity
+    @JvmField
+    internal var _capacity: Int = 0
+
+    /**
+     * Returns the number of elements that can be stored in this set
+     * without requiring internal storage reallocation.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val capacity: Int
+        get() = _capacity
+
+    // We use a backing field for capacity to avoid invokevirtual calls
+    // every time we need to look at the size
+    @JvmField
+    internal var _size: Int = 0
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @get:androidx.annotation.IntRange(from = 0)
+    public val size: Int
+        get() = _size
+
+    /**
+     * Returns `true` if this set has at least one element.
+     */
+    public fun any(): Boolean = _size != 0
+
+    /**
+     * Returns `true` if this set has no elements.
+     */
+    public fun none(): Boolean = _size == 0
+
+    /**
+     * Indicates whether this set is empty.
+     */
+    public fun isEmpty(): Boolean = _size == 0
+
+    /**
+     * Returns `true` if this set is not empty.
+     */
+    public fun isNotEmpty(): Boolean = _size != 0
+
+    /**
+     * Returns the first element in the collection.
+     * @throws NoSuchElementException if the collection is empty
+     */
+    public inline fun first(): PKey {
+        forEach { return it }
+        throw NoSuchElementException("The PKeySet is empty")
+    }
+
+    /**
+     * Returns the first element in the collection for which [predicate] returns `true`.
+     *
+     * **Note** There is no mechanism for both determining if there is an element that matches
+     * [predicate] _and_ returning it if it exists. Developers should use [forEach] to achieve
+     * this behavior.
+     *
+     * @param predicate Called on elements of the set, returning `true` for an element that matches
+     * or `false` if it doesn't
+     * @return An element in the set for which [predicate] returns `true`.
+     * @throws NoSuchElementException if [predicate] returns `false` for all elements or the
+     * collection is empty.
+     */
+    public inline fun first(predicate: (element: PKey) -> Boolean): PKey {
+        contract { callsInPlace(predicate) }
+        forEach { if (predicate(it)) return it }
+        throw NoSuchElementException("Could not find a match")
+    }
+
+    /**
+     * Iterates over every element stored in this set by invoking
+     * the specified [block] lambda.
+     */
+    @PublishedApi
+    internal inline fun forEachIndex(block: (index: Int) -> Unit) {
+        contract { callsInPlace(block) }
+        val m = metadata
+        val lastIndex = m.size - 2 // We always have 0 or at least 2 elements
+
+        for (i in 0..lastIndex) {
+            var slot = m[i]
+            if (slot.maskEmptyOrDeleted() != BitmaskMsb) {
+                // Branch-less if (i == lastIndex) 7 else 8
+                // i - lastIndex returns a negative value when i < lastIndex,
+                // so 1 is set as the MSB. By inverting and shifting we get
+                // 0 when i < lastIndex, 1 otherwise.
+                val bitCount = 8 - ((i - lastIndex).inv() ushr 31)
+                for (j in 0 until bitCount) {
+                    if (isFull(slot and 0xFFL)) {
+                        val index = (i shl 3) + j
+                        block(index)
+                    }
+                    slot = slot shr 8
+                }
+                if (bitCount != 8) return
+            }
+        }
+    }
+
+    /**
+     * Iterates over every element stored in this set by invoking
+     * the specified [block] lambda.
+     * @param block called with each element in the set
+     */
+    public inline fun forEach(block: (element: PKey) -> Unit) {
+        contract { callsInPlace(block) }
+        val k = elements
+
+        forEachIndex { index ->
+            block(k[index])
+        }
+    }
+
+    /**
+     * Returns true if all elements match the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns return `true` for
+     * all elements.
+     */
+    public inline fun all(predicate: (element: PKey) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach { element ->
+            if (!predicate(element)) return false
+        }
+        return true
+    }
+
+    /**
+     * Returns true if at least one element matches the given [predicate].
+     * @param predicate called for elements in the set to determine if it returns `true` for any
+     * elements.
+     */
+    public inline fun any(predicate: (element: PKey) -> Boolean): Boolean {
+        contract { callsInPlace(predicate) }
+        forEach { element ->
+            if (predicate(element)) return true
+        }
+        return false
+    }
+
+    /**
+     * Returns the number of elements in this set.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public fun count(): Int = _size
+
+    /**
+     * Returns the number of elements matching the given [predicate].
+     * @param predicate Called for all elements in the set to count the number for which it returns
+     * `true`.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public inline fun count(predicate: (element: PKey) -> Boolean): Int {
+        contract { callsInPlace(predicate) }
+        var count = 0
+        forEach { element ->
+            if (predicate(element)) count++
+        }
+        return count
+    }
+
+    /**
+     * Returns `true` if the specified [element] is present in this set, `false`
+     * otherwise.
+     * @param element The element to look for in this set
+     */
+    public operator fun contains(element: PKey): Boolean = findElementIndex(element) >= 0
+
+    /**
+     * Returns the hash code value for this set. The hash code of a set is defined to be the
+     * sum of the hash codes of the elements in the set.
+     */
+    public override fun hashCode(): Int {
+        var hash = 0
+
+        forEach { element ->
+            hash += element.hashCode()
+        }
+
+        return hash
+    }
+
+    /**
+     * Compares the specified object [other] with this set for equality.
+     * The two objects are considered equal if [other]:
+     * - Is a [PKeySet]
+     * - Has the same [size] as this set
+     * - Contains elements equal to this set's elements
+     */
+    public override fun equals(other: Any?): Boolean {
+        if (other === this) {
+            return true
+        }
+
+        if (other !is PKeySet) {
+            return false
+        }
+        if (other._size != _size) {
+            return false
+        }
+
+        forEach { element ->
+            if (element !in other) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    /**
+     * Returns a string representation of this set. The set is denoted in the
+     * string by the `{}`. Each element is separated by `, `.
+     */
+    public override fun toString(): String {
+        if (isEmpty()) {
+            return "[]"
+        }
+
+        val s = StringBuilder().append('[')
+        val last = _size - 1
+        var index = 0
+        forEach { element ->
+            s.append(element)
+            if (index++ < last) {
+                s.append(',').append(' ')
+            }
+        }
+
+        return s.append(']').toString()
+    }
+
+    /**
+     * Scans the set to find the index in the backing arrays of the
+     * specified [element]. Returns -1 if the element is not present.
+     */
+    internal inline fun findElementIndex(element: PKey): Int {
+        val hash = hash(element)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = h1(hash) and probeMask
+        var probeIndex = 0
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (elements[index] == element) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        return -1
+    }
+}
+
+/**
+ * [MutablePKeySet] is a container with a [MutableSet]-like interface based on a flat
+ * hash table implementation. The underlying implementation is designed to avoid
+ * all allocations on insertion, removal, retrieval, and iteration. Allocations
+ * may still happen on insertion when the underlying storage needs to grow to
+ * accommodate newly added elements to the set.
+ *
+ * This implementation makes no guarantee as to the order of the elements stored,
+ * nor does it make guarantees that the order remains constant over time.
+ *
+ * This implementation is not thread-safe: if multiple threads access this
+ * container concurrently, and one or more threads modify the structure of
+ * the set (insertion or removal for instance), the calling code must provide
+ * the appropriate synchronization. Concurrent reads are however safe.
+ *
+ * @constructor Creates a new [MutablePKeySet]
+ * @param initialCapacity The initial desired capacity for this container.
+ * The container will honor this value by guaranteeing its internal structures
+ * can hold that many elements without requiring any allocations. The initial
+ * capacity can be set to 0.
+ */
+public class MutablePKeySet(
+    initialCapacity: Int = DefaultScatterCapacity
+) : PKeySet() {
+    // Number of elements we can add before we need to grow
+    private var growthLimit = 0
+
+    init {
+        require(initialCapacity >= 0) { "Capacity must be a positive value." }
+        initializeStorage(unloadedCapacity(initialCapacity))
+    }
+
+    private fun initializeStorage(initialCapacity: Int) {
+        val newCapacity = if (initialCapacity > 0) {
+            // Since we use longs for storage, our capacity is never < 7, enforce
+            // it here. We do have a special case for 0 to create small empty maps
+            maxOf(7, normalizeCapacity(initialCapacity))
+        } else {
+            0
+        }
+        _capacity = newCapacity
+        initializeMetadata(newCapacity)
+        elements = PKeyArray(newCapacity)
+    }
+
+    private fun initializeMetadata(capacity: Int) {
+        metadata = if (capacity == 0) {
+            EmptyGroup
+        } else {
+            // Round up to the next multiple of 8 and find how many longs we need
+            val size = (((capacity + 1 + ClonedMetadataCount) + 7) and 0x7.inv()) shr 3
+            LongArray(size).apply {
+                fill(AllEmpty)
+            }
+        }
+        writeRawMetadata(metadata, capacity, Sentinel)
+        initializeGrowth()
+    }
+
+    private fun initializeGrowth() {
+        growthLimit = loadedCapacity(capacity) - _size
+    }
+
+    /**
+     * Adds the specified element to the set.
+     * @param element The element to add to the set.
+     * @return `true` if the element has been added or `false` if the element is already
+     * contained within the set.
+     */
+    public fun add(element: PKey): Boolean {
+        val oldSize = _size
+        val index = findAbsoluteInsertIndex(element)
+        elements[index] = element
+        return _size != oldSize
+    }
+
+    /**
+     * Adds the specified element to the set.
+     * @param element The element to add to the set.
+     */
+    public operator fun plusAssign(element: PKey) {
+        val index = findAbsoluteInsertIndex(element)
+        elements[index] = element
+    }
+
+    /**
+     * Adds all the [elements] into this set.
+     * @param elements An array of elements to add to the set.
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public fun addAll(@Suppress("ArrayReturn") elements: PKeyArray): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all the [elements] into this set.
+     * @param elements An array of elements to add to the set.
+     */
+    public operator fun plusAssign(@Suppress("ArrayReturn") elements: PKeyArray) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [PKeySet] of elements to add to this set.
+     * @return `true` if any of the specified elements were added to the collection,
+     * `false` if the collection was not modified.
+     */
+    public fun addAll(elements: PKeySet): Boolean {
+        val oldSize = _size
+        plusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Adds all the elements in the [elements] set into this set.
+     * @param elements A [PKeySet] of elements to add to this set.
+     */
+    public operator fun plusAssign(elements: PKeySet) {
+        elements.forEach { element ->
+            plusAssign(element)
+        }
+    }
+
+    /**
+     * Removes the specified [element] from the set.
+     * @param element The element to remove from the set.
+     * @return `true` if the [element] was present in the set, or `false` if it wasn't
+     * present before removal.
+     */
+    public fun remove(element: PKey): Boolean {
+        val index = findElementIndex(element)
+        val exists = index >= 0
+        if (exists) {
+            removeElementAt(index)
+        }
+        return exists
+    }
+
+    /**
+     * Removes the specified [element] from the set if it is present.
+     * @param element The element to remove from the set.
+     */
+    public operator fun minusAssign(element: PKey) {
+        val index = findElementIndex(element)
+        if (index >= 0) {
+            removeElementAt(index)
+        }
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An array of elements to be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public fun removeAll(@Suppress("ArrayReturn") elements: PKeyArray): Boolean {
+        val oldSize = _size
+        minusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An array of elements to be removed from the set.
+     */
+    public operator fun minusAssign(@Suppress("ArrayReturn") elements: PKeyArray) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [PKeySet] of elements to be removed from the set.
+     * @return `true` if the set was changed or `false` if none of the elements were present.
+     */
+    public fun removeAll(elements: PKeySet): Boolean {
+        val oldSize = _size
+        minusAssign(elements)
+        return oldSize != _size
+    }
+
+    /**
+     * Removes the specified [elements] from the set, if present.
+     * @param elements An [PKeySet] of elements to be removed from the set.
+     */
+    public operator fun minusAssign(elements: PKeySet) {
+        elements.forEach { element ->
+            minusAssign(element)
+        }
+    }
+
+    private fun removeElementAt(index: Int) {
+        _size -= 1
+
+        // TODO: We could just mark the element as empty if there's a group
+        //       window around this element that was already empty
+        writeMetadata(index, Deleted)
+    }
+
+    /**
+     * Removes all elements from this set.
+     */
+    public fun clear() {
+        _size = 0
+        if (metadata !== EmptyGroup) {
+            metadata.fill(AllEmpty)
+            writeRawMetadata(metadata, _capacity, Sentinel)
+        }
+        initializeGrowth()
+    }
+
+    /**
+     * Scans the set to find the index at which we can store a given [element].
+     * If the element already exists in the set, its index
+     * will be returned, otherwise the index of an empty slot will be returned.
+     * Calling this function may cause the internal storage to be reallocated
+     * if the set is full.
+     */
+    private fun findAbsoluteInsertIndex(element: PKey): Int {
+        val hash = hash(element)
+        val hash1 = h1(hash)
+        val hash2 = h2(hash)
+
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+
+        while (true) {
+            val g = group(metadata, probeOffset)
+            var m = g.match(hash2)
+            while (m.hasNext()) {
+                val index = (probeOffset + m.get()) and probeMask
+                if (elements[index] == element) {
+                    return index
+                }
+                m = m.next()
+            }
+
+            if (g.maskEmpty() != 0L) {
+                break
+            }
+
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+
+        var index = findFirstAvailableSlot(hash1)
+        if (growthLimit == 0 && !isDeleted(metadata, index)) {
+            adjustStorage()
+            index = findFirstAvailableSlot(hash1)
+        }
+
+        _size += 1
+        growthLimit -= if (isEmpty(metadata, index)) 1 else 0
+        writeMetadata(index, hash2.toLong())
+
+        return index
+    }
+
+    /**
+     * Finds the first empty or deleted slot in the set in which we can
+     * store a value without resizing the internal storage.
+     */
+    private fun findFirstAvailableSlot(hash1: Int): Int {
+        val probeMask = _capacity
+        var probeOffset = hash1 and probeMask
+        var probeIndex = 0
+        while (true) {
+            val g = group(metadata, probeOffset)
+            val m = g.maskEmptyOrDeleted()
+            if (m != 0L) {
+                return (probeOffset + m.lowestBitSet()) and probeMask
+            }
+            probeIndex += GroupWidth
+            probeOffset = (probeOffset + probeIndex) and probeMask
+        }
+    }
+
+    /**
+     * Trims this [MutablePKeySet]'s storage so it is sized appropriately
+     * to hold the current elements.
+     *
+     * Returns the number of empty elements removed from this set's storage.
+     * Returns 0 if no trimming is necessary or possible.
+     */
+    @androidx.annotation.IntRange(from = 0)
+    public fun trim(): Int {
+        val previousCapacity = _capacity
+        val newCapacity = normalizeCapacity(unloadedCapacity(_size))
+        if (newCapacity < previousCapacity) {
+            resizeStorage(newCapacity)
+            return previousCapacity - _capacity
+        }
+        return 0
+    }
+
+    /**
+     * Grow internal storage if necessary. This function can instead opt to
+     * remove deleted elements from the set to avoid an expensive reallocation
+     * of the underlying storage. This "rehash in place" occurs when the
+     * current size is <= 25/32 of the set capacity. The choice of 25/32 is
+     * detailed in the implementation of abseil's `raw_hash_map`.
+     */
+    private fun adjustStorage() {
+        if (_capacity > GroupWidth && _size.toULong() * 32UL <= _capacity.toULong() * 25UL) {
+            // TODO: Avoid resize and drop deletes instead
+            resizeStorage(nextCapacity(_capacity))
+        } else {
+            resizeStorage(nextCapacity(_capacity))
+        }
+    }
+
+    private fun resizeStorage(newCapacity: Int) {
+        val previousMetadata = metadata
+        val previousElements = elements
+        val previousCapacity = _capacity
+
+        initializeStorage(newCapacity)
+
+        val newElements = elements
+
+        for (i in 0 until previousCapacity) {
+            if (isFull(previousMetadata, i)) {
+                val previousElement = previousElements[i]
+                val hash = hash(previousElement)
+                val index = findFirstAvailableSlot(h1(hash))
+
+                writeMetadata(index, h2(hash).toLong())
+                newElements[index] = previousElement
+            }
+        }
+    }
+
+    /**
+     * Writes the "H2" part of an entry into the metadata array at the specified
+     * [index]. The index must be a valid index. This function ensures the
+     * metadata is also written in the clone area at the end.
+     */
+    private inline fun writeMetadata(index: Int, value: Long) {
+        val m = metadata
+        writeRawMetadata(m, index, value)
+
+        // Mirroring
+        val c = _capacity
+        val cloneIndex = ((index - ClonedMetadataCount) and c) +
+            (ClonedMetadataCount and c)
+        writeRawMetadata(m, cloneIndex, value)
+    }
+}
+
+/**
+ * Returns the hash code of [k]. This follows the [HashSet] default behavior on Android
+ * of returning [Object.hashcode()] with the higher bits of hash spread to the lower bits.
+ */
+internal inline fun hash(k: PKey): Int {
+    val hash = k.hashCode()
+    return hash xor (hash ushr 16)
+}
diff --git a/collection/collection/template/PKeySetTest.kt.template b/collection/collection/template/PKeySetTest.kt.template
new file mode 100644
index 0000000..cd5a422
--- /dev/null
+++ b/collection/collection/template/PKeySetTest.kt.template
@@ -0,0 +1,534 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+// DO NOT MAKE CHANGES to the kotlin source file.
+//
+// This file was generated from a template in the template directory.
+// Make a change to the original template and run the generateCollections.sh script
+// to ensure the change is available on all versions of the map.
+// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+class PKeySetTest {
+    @Test
+    fun emptyPKeySetConstructor() {
+        val set = MutablePKeySet()
+        assertEquals(7, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun immutableEmptyPKeySet() {
+        val set: PKeySet = emptyPKeySet()
+        assertEquals(0, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun zeroCapacityPKeySet() {
+        val set = MutablePKeySet(0)
+        assertEquals(0, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun emptyPKeySetWithCapacity() {
+        // When unloading the suggested capacity, we'll fall outside of the
+        // expected bucket of 2047 entries, and we'll get 4095 instead
+        val set = MutablePKeySet(1800)
+        assertEquals(4095, set.capacity)
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun mutablePKeySetBuilder() {
+        val empty = mutablePKeySetOf()
+        assertEquals(0, empty.size)
+
+        val withElements = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, withElements.size)
+        assertTrue(1KeySuffix in withElements)
+        assertTrue(2KeySuffix in withElements)
+    }
+
+    @Test
+    fun addToPKeySet() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        assertTrue(set.add(2KeySuffix))
+
+        assertEquals(2, set.size)
+        val elements = PKeyArray(2)
+        var index = 0
+        set.forEach { element ->
+            elements[index++] = element
+        }
+        elements.sort()
+        assertEquals(1KeySuffix, elements[0])
+        assertEquals(2KeySuffix, elements[1])
+    }
+
+    @Test
+    fun addToSizedPKeySet() {
+        val set = MutablePKeySet(12)
+        set += 1KeySuffix
+
+        assertEquals(1, set.size)
+        assertEquals(1KeySuffix, set.first())
+    }
+
+    @Test
+    fun addExistingElement() {
+        val set = MutablePKeySet(12)
+        set += 1KeySuffix
+        assertFalse(set.add(1KeySuffix))
+        set += 1KeySuffix
+
+        assertEquals(1, set.size)
+        assertEquals(1KeySuffix, set.first())
+    }
+
+    @Test
+    fun addAllArray() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        assertFalse(set.addAll(pKeyArrayOf(1KeySuffix)))
+        assertEquals(1, set.size)
+        assertTrue(set.addAll(pKeyArrayOf(1KeySuffix, 2KeySuffix)))
+        assertEquals(2, set.size)
+        assertTrue(2KeySuffix in set)
+    }
+
+    @Test
+    fun addAllPKeySet() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        assertFalse(set.addAll(mutablePKeySetOf(1KeySuffix)))
+        assertEquals(1, set.size)
+        assertTrue(set.addAll(mutablePKeySetOf(1KeySuffix, 2KeySuffix)))
+        assertEquals(2, set.size)
+        assertTrue(2KeySuffix in set)
+    }
+
+    @Test
+    fun plusAssignArray() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        set += pKeyArrayOf(1KeySuffix)
+        assertEquals(1, set.size)
+        set += pKeyArrayOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, set.size)
+        assertTrue(2KeySuffix in set)
+    }
+
+    @Test
+    fun plusAssignPKeySet() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        set += mutablePKeySetOf(1KeySuffix)
+        assertEquals(1, set.size)
+        set += mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, set.size)
+        assertTrue(2KeySuffix in set)
+    }
+
+    @Test
+    fun firstWithValue() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 2KeySuffix
+        var element: PKey = -1KeySuffix
+        var otherElement: PKey = -1KeySuffix
+        set.forEach { if (element == -1KeySuffix) element = it else otherElement = it }
+        assertEquals(element, set.first())
+        set -= element
+        assertEquals(otherElement, set.first())
+    }
+
+    @Test
+    fun firstEmpty() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutablePKeySet()
+            set.first()
+        }
+    }
+
+    @Test
+    fun firstMatching() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 2KeySuffix
+        assertEquals(1KeySuffix, set.first { it < 2KeySuffix })
+        assertEquals(2KeySuffix, set.first { it > 1KeySuffix })
+    }
+
+    @Test
+    fun firstMatchingEmpty() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutablePKeySet()
+            set.first { it > 0KeySuffix }
+        }
+    }
+
+    @Test
+    fun firstMatchingNoMatch() {
+        assertFailsWith(NoSuchElementException::class) {
+            val set = MutablePKeySet()
+            set += 1KeySuffix
+            set += 2KeySuffix
+            set.first { it < 0KeySuffix }
+        }
+    }
+
+    @Test
+    fun remove() {
+        val set = MutablePKeySet()
+        assertFalse(set.remove(1KeySuffix))
+
+        set += 1KeySuffix
+        assertTrue(set.remove(1KeySuffix))
+        assertEquals(0, set.size)
+
+        set += 1KeySuffix
+        set -= 1KeySuffix
+        assertEquals(0, set.size)
+    }
+
+    @Test
+    fun removeThenAdd() {
+        // Use a size of 6 to fit in a single entry in the metadata table
+        val set = MutablePKeySet(6)
+        set += 1KeySuffix
+        set += 5KeySuffix
+        set += 6KeySuffix
+        set += 9KeySuffix
+        set += 11KeySuffix
+        set += 13KeySuffix
+
+        // Removing all the entries will mark the medata as deleted
+        set.remove(1KeySuffix)
+        set.remove(5KeySuffix)
+        set.remove(6KeySuffix)
+        set.remove(9KeySuffix)
+        set.remove(11KeySuffix)
+        set.remove(13KeySuffix)
+
+        assertEquals(0, set.size)
+
+        val capacity = set.capacity
+
+        // Make sure reinserting an entry after filling the table
+        // with "Deleted" markers works
+        set += 3KeySuffix
+
+        assertEquals(1, set.size)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun removeAllArray() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertFalse(set.removeAll(pKeyArrayOf(3KeySuffix, 5KeySuffix)))
+        assertEquals(2, set.size)
+        assertTrue(set.removeAll(pKeyArrayOf(3KeySuffix, 1KeySuffix, 5KeySuffix)))
+        assertEquals(1, set.size)
+        assertFalse(1KeySuffix in set)
+    }
+
+    @Test
+    fun removeAllPKeySet() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertFalse(set.removeAll(mutablePKeySetOf(3KeySuffix, 5KeySuffix)))
+        assertEquals(2, set.size)
+        assertTrue(set.removeAll(mutablePKeySetOf(3KeySuffix, 1KeySuffix, 5KeySuffix)))
+        assertEquals(1, set.size)
+        assertFalse(1KeySuffix in set)
+    }
+
+    @Test
+    fun minusAssignArray() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        set -= pKeyArrayOf(3KeySuffix, 5KeySuffix)
+        assertEquals(2, set.size)
+        set -= pKeyArrayOf(3KeySuffix, 1KeySuffix, 5KeySuffix)
+        assertEquals(1, set.size)
+        assertFalse(1KeySuffix in set)
+    }
+
+    @Test
+    fun minusAssignPKeySet() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        set -= mutablePKeySetOf(3KeySuffix, 5KeySuffix)
+        assertEquals(2, set.size)
+        set -= mutablePKeySetOf(3KeySuffix, 1KeySuffix, 5KeySuffix)
+        assertEquals(1, set.size)
+        assertFalse(1KeySuffix in set)
+    }
+
+    @Test
+    fun insertManyEntries() {
+        val set = MutablePKeySet()
+
+        for (i in 0 until 1700) {
+            set += i.toPKey()
+        }
+
+        assertEquals(1700, set.size)
+    }
+
+    @Test
+    fun forEach() {
+        for (i in 0..48) {
+            val set = MutablePKeySet()
+
+            for (j in 0 until i) {
+                set += j.toPKey()
+            }
+
+            val elements = PKeyArray(i)
+            var index = 0
+            set.forEach { element ->
+                elements[index++] = element
+            }
+            elements.sort()
+
+            index = 0
+            elements.forEach { element ->
+                assertEquals(element, index.toPKey())
+                index++
+            }
+        }
+    }
+
+    @Test
+    fun clear() {
+        val set = MutablePKeySet()
+
+        for (i in 0 until 32) {
+            set += i.toPKey()
+        }
+
+        val capacity = set.capacity
+        set.clear()
+
+        assertEquals(0, set.size)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun string() {
+        val set = MutablePKeySet()
+        assertEquals("[]", set.toString())
+
+        set += 1KeySuffix
+        set += 5KeySuffix
+        assertTrue(
+            "[${1KeySuffix}, ${5KeySuffix}]" == set.toString() ||
+                "[${5KeySuffix}, ${1KeySuffix}]" == set.toString()
+        )
+    }
+
+    @Test
+    fun equals() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 5KeySuffix
+
+        assertFalse(set.equals(null))
+        assertEquals(set, set)
+
+        val set2 = MutablePKeySet()
+        set2 += 5KeySuffix
+
+        assertNotEquals(set, set2)
+
+        set2 += 1KeySuffix
+        assertEquals(set, set2)
+    }
+
+    @Test
+    fun contains() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 5KeySuffix
+
+        assertTrue(set.contains(1KeySuffix))
+        assertTrue(set.contains(5KeySuffix))
+        assertFalse(set.contains(2KeySuffix))
+    }
+
+    @Test
+    fun empty() {
+        val set = MutablePKeySet()
+        assertTrue(set.isEmpty())
+        assertFalse(set.isNotEmpty())
+        assertTrue(set.none())
+        assertFalse(set.any())
+
+        set += 1KeySuffix
+
+        assertFalse(set.isEmpty())
+        assertTrue(set.isNotEmpty())
+        assertTrue(set.any())
+        assertFalse(set.none())
+    }
+
+    @Test
+    fun count() {
+        val set = MutablePKeySet()
+        assertEquals(0, set.count())
+
+        set += 1KeySuffix
+        assertEquals(1, set.count())
+
+        set += 5KeySuffix
+        set += 6KeySuffix
+        set += 9KeySuffix
+        set += 11KeySuffix
+        set += 13KeySuffix
+
+        assertEquals(2, set.count { it < 6KeySuffix })
+        assertEquals(0, set.count { it < 0KeySuffix })
+    }
+
+    @Test
+    fun any() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 5KeySuffix
+        set += 6KeySuffix
+        set += 9KeySuffix
+        set += 11KeySuffix
+        set += 13KeySuffix
+
+        assertTrue(set.any { it >= 11KeySuffix })
+        assertFalse(set.any { it < 0KeySuffix })
+    }
+
+    @Test
+    fun all() {
+        val set = MutablePKeySet()
+        set += 1KeySuffix
+        set += 5KeySuffix
+        set += 6KeySuffix
+        set += 9KeySuffix
+        set += 11KeySuffix
+        set += 13KeySuffix
+
+        assertTrue(set.all { it > 0KeySuffix })
+        assertFalse(set.all { it < 0KeySuffix })
+    }
+
+    @Test
+    fun trim() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 7KeySuffix)
+        val capacity = set.capacity
+        assertEquals(0, set.trim())
+        set.clear()
+        assertEquals(capacity, set.trim())
+        assertEquals(0, set.capacity)
+        set.addAll(pKeyArrayOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix, 5KeySuffix, 7KeySuffix, 6KeySuffix, 8KeySuffix,
+            9KeySuffix, 10KeySuffix, 11KeySuffix, 12KeySuffix, 13KeySuffix, 14KeySuffix))
+        set.removeAll(pKeyArrayOf(6KeySuffix, 8KeySuffix, 9KeySuffix, 10KeySuffix, 11KeySuffix, 12KeySuffix, 13KeySuffix, 14KeySuffix))
+        assertTrue(set.trim() > 0)
+        assertEquals(capacity, set.capacity)
+    }
+
+    @Test
+    fun pKeySetOfEmpty() {
+        assertSame(emptyPKeySet(), pKeySetOf())
+        assertEquals(0, pKeySetOf().size)
+    }
+
+    @Test
+    fun pKeySetOfOne() {
+        val set = pKeySetOf(1KeySuffix)
+        assertEquals(1, set.size)
+        assertEquals(1KeySuffix, set.first())
+    }
+
+    @Test
+    fun pKeySetOfTwo() {
+        val set = pKeySetOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun pKeySetOfThree() {
+        val set = pKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+        assertEquals(3, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertTrue(3KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun pKeySetOfFour() {
+        val set = pKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix)
+        assertEquals(4, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertTrue(3KeySuffix in set)
+        assertTrue(4KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun mutablePKeySetOfOne() {
+        val set = mutablePKeySetOf(1KeySuffix)
+        assertEquals(1, set.size)
+        assertEquals(1KeySuffix, set.first())
+    }
+
+    @Test
+    fun mutablePKeySetOfTwo() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix)
+        assertEquals(2, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun mutablePKeySetOfThree() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix)
+        assertEquals(3, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertTrue(3KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+
+    @Test
+    fun mutablePKeySetOfFour() {
+        val set = mutablePKeySetOf(1KeySuffix, 2KeySuffix, 3KeySuffix, 4KeySuffix)
+        assertEquals(4, set.size)
+        assertTrue(1KeySuffix in set)
+        assertTrue(2KeySuffix in set)
+        assertTrue(3KeySuffix in set)
+        assertTrue(4KeySuffix in set)
+        assertFalse(5KeySuffix in set)
+    }
+}
diff --git a/collection/collection/template/generateCollections.sh b/collection/collection/template/generateCollections.sh
new file mode 100755
index 0000000..39db416d
--- /dev/null
+++ b/collection/collection/template/generateCollections.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+primitives=("Float" "Long" "Int")
+suffixes=("f" "L" "")
+
+scriptDir=`dirname ${PWD}/${0}`
+
+for index in ${!primitives[@]}
+do
+  primitive=${primitives[$index]}
+  firstLower=`echo ${primitive:0:1} | tr '[:upper:]' '[:lower:]'`
+  lower="${firstLower}${primitive:1}"
+  echo "generating ${primitive}ObjectMap.kt"
+  sed -e "s/PKey/${primitive}/g" -e "s/pKey/${lower}/g" ${scriptDir}/PKeyObjectMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}ObjectMap.kt
+  echo "generating ${primitive}ObjectMapTest.kt"
+  sed -e "s/PValue/${primitive}/g" ${scriptDir}/ObjectPValueMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/Object${primitive}Map.kt
+
+  suffix=${suffixes[$index]}
+  echo "generating Object${primitive}Map.kt"
+  sed -e "s/PValue/${primitive}/g" -e "s/ValueSuffix/${suffix}/g" ${scriptDir}/ObjectPValueMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/Object${primitive}MapTest.kt
+  echo "generating Object${primitive}MapTest.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeyObjectMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}ObjectMapTest.kt
+
+  echo "generating ${primitive}Set.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" ${scriptDir}/PKeySet.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}Set.kt
+  echo "generating ${primitive}SetTest.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeySetTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}SetTest.kt
+
+  echo "generating ${primitive}List.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" ${scriptDir}/PKeyList.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${primitive}List.kt
+  echo "generating ${primitive}ListTest.kt"
+  sed -e "s/PKey/${primitive}/g" -e"s/pKey/${lower}/g" -e "s/KeySuffix/${suffix}/g" ${scriptDir}/PKeyListTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${primitive}ListTest.kt
+done
+
+for keyIndex in ${!primitives[@]}
+do
+  key=${primitives[$keyIndex]}
+  firstLower=`echo ${key:0:1} | tr '[:upper:]' '[:lower:]'`
+  lowerKey="${firstLower}${key:1}"
+  keySuffix=${suffixes[$keyIndex]}
+  for valueIndex in ${!primitives[@]}
+  do
+    value=${primitives[$valueIndex]}
+    valueSuffix=${suffixes[$valueIndex]}
+    echo "generating ${key}${value}Map.kt"
+    sed -e "s/PKey/${key}/g" -e "s/pKey/${lowerKey}/g" -e "s/PValue/${value}/g" ${scriptDir}/PKeyPValueMap.kt.template > ${scriptDir}/../src/commonMain/kotlin/androidx/collection/${key}${value}Map.kt
+    echo "generating ${key}${value}MapTest.kt"
+    sed -e "s/PKey/${key}/g" -e "s/pKey/${lowerKey}/g" -e "s/PValue/${value}/g" -e "s/ValueSuffix/${valueSuffix}/g" -e "s/KeySuffix/${keySuffix}/g" ${scriptDir}/PKeyPValueMapTest.kt.template > ${scriptDir}/../src/commonTest/kotlin/androidx/collection/${key}${value}MapTest.kt
+  done
+done
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
index 2767e5aa..67921dc 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
@@ -28,11 +28,13 @@
 import androidx.compose.animation.core.updateTransition
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Surface
 import androidx.compose.material3.TabRow
@@ -1115,6 +1117,77 @@
     }
 
     @Test
+    fun testRightEnterExitTransitionIsChosenDuringInterruption() {
+        var flag by mutableStateOf(false)
+        var fixedPosition: Offset? = null
+        var slidePosition: Offset? = null
+        rule.setContent {
+            AnimatedContent(
+                targetState = flag,
+                label = "",
+                transitionSpec = {
+                    if (false isTransitioningTo true) {
+                        ContentTransform(
+                            targetContentEnter = EnterTransition.None,
+                            initialContentExit = slideOutOfContainer(
+                                AnimatedContentTransitionScope.SlideDirection.Start,
+                                animationSpec = tween(durationMillis = 500)
+                            ),
+                            targetContentZIndex = -1.0f,
+                            sizeTransform = SizeTransform(clip = false)
+                        )
+                    } else {
+                        ContentTransform(
+                            targetContentEnter = slideIntoContainer(
+                                AnimatedContentTransitionScope.SlideDirection.End
+                            ),
+                            initialContentExit = ExitTransition.Hold,
+                            targetContentZIndex = 0.0f,
+                            sizeTransform = SizeTransform(clip = false)
+                        )
+                    }
+                },
+                modifier = Modifier.fillMaxSize()
+            ) { flag ->
+                Spacer(
+                    modifier = Modifier
+                        .wrapContentSize(Alignment.Center)
+                        .size(256.dp)
+                        .onGloballyPositioned {
+                            if (flag) {
+                                fixedPosition = it.positionInRoot()
+                            } else {
+                                slidePosition = it.positionInRoot()
+                            }
+                        }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            flag = true
+        }
+        rule.waitUntil { fixedPosition != null }
+        val initialFixedPosition = fixedPosition
+        // Advance 10 frames
+        repeat(10) {
+            val lastSlidePos = slidePosition
+            rule.waitUntil { slidePosition != lastSlidePos }
+            assertEquals(initialFixedPosition, fixedPosition)
+        }
+
+        // Change the target state amid transition, creating an interruption
+        flag = false
+        // Advance 10 frames
+        repeat(10) {
+            val lastSlidePos = slidePosition
+            rule.waitUntil { slidePosition != lastSlidePos }
+            assertEquals(initialFixedPosition, fixedPosition)
+        }
+        rule.waitForIdle()
+    }
+
+    @Test
     fun testScaleToFitWithFitHeight() {
         var target by mutableStateOf(true)
         var box1Coords: LayoutCoordinates? = null
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
index b82c044..e557851 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedContent.kt
@@ -820,13 +820,14 @@
         }
         if (!contentMap.containsKey(targetState) || !contentMap.containsKey(currentState)) {
             contentMap.clear()
-            val enter = transitionSpec(rootScope).targetContentEnter
-            val exit = rootScope.transitionSpec().initialContentExit
-            val zIndex = transitionSpec(rootScope).targetContentZIndex
             currentlyVisible.fastForEach { stateForContent ->
                 contentMap[stateForContent] = {
+                    // Only update content transform when enter/exit _direction_ changes.
+                    val contentTransform = remember(stateForContent == targetState) {
+                        rootScope.transitionSpec()
+                    }
                     PopulateContentFor(
-                        stateForContent, rootScope, enter, exit, zIndex, currentlyVisible, content
+                        stateForContent, rootScope, contentTransform, currentlyVisible, content
                     )
                 }
             }
@@ -871,33 +872,32 @@
 private inline fun <S> Transition<S>.PopulateContentFor(
     stateForContent: S,
     rootScope: AnimatedContentRootScope<S>,
-    enter: EnterTransition,
-    exit: ExitTransition,
-    zIndex: Float,
+    contentTransform: ContentTransform,
     currentlyVisible: SnapshotStateList<S>,
     crossinline content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
 ) {
-    var activeEnter by remember { mutableStateOf(enter) }
+    var activeEnter by remember { mutableStateOf(contentTransform.targetContentEnter) }
     var activeExit by remember { mutableStateOf(ExitTransition.None) }
-    val targetZIndex = remember { zIndex }
+    val targetZIndex = remember { contentTransform.targetContentZIndex }
 
     val isEntering = targetState == stateForContent
     if (targetState == currentState) {
         // Transition finished, reset active enter & exit.
-        activeEnter = androidx.compose.animation.EnterTransition.None
-        activeExit = androidx.compose.animation.ExitTransition.None
+        activeEnter = EnterTransition.None
+        activeExit = ExitTransition.None
     } else if (isEntering) {
         // If the previous enter transition never finishes when multiple
         // interruptions happen, avoid adding new enter transitions for simplicity.
-        if (activeEnter == androidx.compose.animation.EnterTransition.None)
-            activeEnter += enter
+        if (activeEnter == EnterTransition.None)
+            activeEnter += contentTransform.targetContentEnter
     } else {
         // If the previous exit transition never finishes when multiple
         // interruptions happen, avoid adding new enter transitions for simplicity.
-        if (activeExit == androidx.compose.animation.ExitTransition.None) {
-            activeExit += exit
+        if (activeExit == ExitTransition.None) {
+            activeExit += contentTransform.initialContentExit
         }
     }
+
     val childData = remember { AnimatedContentRootScope.ChildData(stateForContent) }
     AnimatedEnterExitImpl(
         this,
@@ -915,16 +915,15 @@
             .then(
                 if (isEntering) {
                     activeEnter[ScaleToFitTransitionKey]
-                        ?: activeExit[ScaleToFitTransitionKey] ?: androidx.compose.ui.Modifier
+                        ?: activeExit[ScaleToFitTransitionKey] ?: Modifier
                 } else {
                     activeExit[ScaleToFitTransitionKey]
-                        ?: activeEnter[ScaleToFitTransitionKey] ?: androidx.compose.ui.Modifier
+                        ?: activeEnter[ScaleToFitTransitionKey] ?: Modifier
                 }
             ),
         shouldDisposeBlock = { currentState, targetState ->
-            currentState == androidx.compose.animation.EnterExitState.PostExit &&
-                targetState == androidx.compose.animation.EnterExitState.PostExit &&
-                !activeExit.data.hold
+            currentState == EnterExitState.PostExit &&
+                targetState == EnterExitState.PostExit && !activeExit.data.hold
         },
         onLookaheadMeasured = {
             if (isEntering) rootScope.targetSizeMap.getOrPut(targetState) {
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 66ef5ba..361d875 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -2,13 +2,13 @@
 package androidx.compose.foundation {
 
   public final class BackgroundKt {
-    method public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shape shape, optional @FloatRange(from=0.0, to=1.0) float alpha);
-    method public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, long color, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shape shape, optional @FloatRange(from=0.0, to=1.0) float alpha);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   public final class BasicMarqueeKt {
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.MarqueeSpacing MarqueeSpacing(float spacing);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier basicMarquee(androidx.compose.ui.Modifier, optional int iterations, optional int animationMode, optional int delayMillis, optional int initialDelayMillis, optional androidx.compose.foundation.MarqueeSpacing spacing, optional float velocity);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier basicMarquee(androidx.compose.ui.Modifier, optional int iterations, optional int animationMode, optional int delayMillis, optional int initialDelayMillis, optional androidx.compose.foundation.MarqueeSpacing spacing, optional float velocity);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static int getDefaultMarqueeDelayMillis();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static int getDefaultMarqueeIterations();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.MarqueeSpacing getDefaultMarqueeSpacing();
@@ -28,7 +28,7 @@
 
   public final class BasicTooltipKt {
     method @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
+    method @androidx.compose.runtime.Stable public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.BasicTooltipState rememberBasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
   }
 
@@ -47,9 +47,9 @@
   }
 
   public final class BorderKt {
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   @androidx.compose.runtime.Immutable public final class BorderStroke {
@@ -83,7 +83,7 @@
   }
 
   public final class ClipScrollableContainerKt {
-    method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface CombinedClickableNode extends androidx.compose.ui.node.PointerInputModifierNode {
@@ -103,7 +103,7 @@
   }
 
   public final class FocusableKt {
-    method public static androidx.compose.ui.Modifier focusGroup(androidx.compose.ui.Modifier);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier focusGroup(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier focusable(androidx.compose.ui.Modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
@@ -111,6 +111,30 @@
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier onFocusedBoundsChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onPositioned);
   }
 
+  public final class GraphicsSurfaceKt {
+    method @androidx.compose.runtime.Composable public static void EmbeddedGraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional long surfaceSize, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+    method @androidx.compose.runtime.Composable public static void GraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional int zOrder, optional long surfaceSize, optional boolean isSecure, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+  }
+
+  public interface GraphicsSurfaceScope {
+    method public void onSurface(kotlin.jvm.functions.Function5<? super androidx.compose.foundation.SurfaceCoroutineScope,? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onSurface);
+  }
+
+  @kotlin.jvm.JvmInline public final value class GraphicsSurfaceZOrder {
+    method public int getZOrder();
+    property public final int zOrder;
+    field public static final androidx.compose.foundation.GraphicsSurfaceZOrder.Companion Companion;
+  }
+
+  public static final class GraphicsSurfaceZOrder.Companion {
+    method public int getBehind();
+    method public int getMediaOverlay();
+    method public int getOnTop();
+    property public final int Behind;
+    property public final int MediaOverlay;
+    property public final int OnTop;
+  }
+
   public final class HoverableKt {
     method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
   }
@@ -246,6 +270,14 @@
     property public final androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.ScrollState,?> Saver;
   }
 
+  public interface SurfaceCoroutineScope extends androidx.compose.foundation.SurfaceScope kotlinx.coroutines.CoroutineScope {
+  }
+
+  public interface SurfaceScope {
+    method public void onChanged(android.view.Surface, kotlin.jvm.functions.Function3<? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> onChanged);
+    method public void onDestroyed(android.view.Surface, kotlin.jvm.functions.Function1<? super android.view.Surface,kotlin.Unit> onDestroyed);
+  }
+
   public final class SystemGestureExclusionKt {
     method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,androidx.compose.ui.geometry.Rect> exclusion);
@@ -402,8 +434,8 @@
   }
 
   public final class ScrollableKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
-    method public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ScrollableState {
@@ -1211,7 +1243,7 @@
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
-    method public static androidx.compose.ui.Modifier selectableGroup(androidx.compose.ui.Modifier);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier selectableGroup(androidx.compose.ui.Modifier);
   }
 
   public final class SelectableKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 3118943..5a3ef8a 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -2,13 +2,13 @@
 package androidx.compose.foundation {
 
   public final class BackgroundKt {
-    method public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shape shape, optional @FloatRange(from=0.0, to=1.0) float alpha);
-    method public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, long color, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Brush brush, optional androidx.compose.ui.graphics.Shape shape, optional @FloatRange(from=0.0, to=1.0) float alpha);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier background(androidx.compose.ui.Modifier, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   public final class BasicMarqueeKt {
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.MarqueeSpacing MarqueeSpacing(float spacing);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier basicMarquee(androidx.compose.ui.Modifier, optional int iterations, optional int animationMode, optional int delayMillis, optional int initialDelayMillis, optional androidx.compose.foundation.MarqueeSpacing spacing, optional float velocity);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier basicMarquee(androidx.compose.ui.Modifier, optional int iterations, optional int animationMode, optional int delayMillis, optional int initialDelayMillis, optional androidx.compose.foundation.MarqueeSpacing spacing, optional float velocity);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static int getDefaultMarqueeDelayMillis();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static int getDefaultMarqueeIterations();
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.MarqueeSpacing getDefaultMarqueeSpacing();
@@ -28,7 +28,7 @@
 
   public final class BasicTooltipKt {
     method @androidx.compose.runtime.Composable public static void BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider positionProvider, kotlin.jvm.functions.Function0<kotlin.Unit> tooltip, androidx.compose.foundation.BasicTooltipState state, optional androidx.compose.ui.Modifier modifier, optional boolean focusable, optional boolean enableUserInput, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
+    method @androidx.compose.runtime.Stable public static androidx.compose.foundation.BasicTooltipState BasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.BasicTooltipState rememberBasicTooltipState(optional boolean initialIsVisible, optional boolean isPersistent, optional androidx.compose.foundation.MutatorMutex mutatorMutex);
   }
 
@@ -47,9 +47,9 @@
   }
 
   public final class BorderKt {
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
-    method public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, optional androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, androidx.compose.ui.graphics.Brush brush, androidx.compose.ui.graphics.Shape shape);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier border(androidx.compose.ui.Modifier, float width, long color, optional androidx.compose.ui.graphics.Shape shape);
   }
 
   @androidx.compose.runtime.Immutable public final class BorderStroke {
@@ -83,7 +83,7 @@
   }
 
   public final class ClipScrollableContainerKt {
-    method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface CombinedClickableNode extends androidx.compose.ui.node.PointerInputModifierNode {
@@ -103,7 +103,7 @@
   }
 
   public final class FocusableKt {
-    method public static androidx.compose.ui.Modifier focusGroup(androidx.compose.ui.Modifier);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier focusGroup(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier focusable(androidx.compose.ui.Modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
@@ -111,6 +111,30 @@
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier onFocusedBoundsChanged(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,kotlin.Unit> onPositioned);
   }
 
+  public final class GraphicsSurfaceKt {
+    method @androidx.compose.runtime.Composable public static void EmbeddedGraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional long surfaceSize, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+    method @androidx.compose.runtime.Composable public static void GraphicsSurface(optional androidx.compose.ui.Modifier modifier, optional boolean isOpaque, optional int zOrder, optional long surfaceSize, optional boolean isSecure, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.GraphicsSurfaceScope,kotlin.Unit> onInit);
+  }
+
+  public interface GraphicsSurfaceScope {
+    method public void onSurface(kotlin.jvm.functions.Function5<? super androidx.compose.foundation.SurfaceCoroutineScope,? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onSurface);
+  }
+
+  @kotlin.jvm.JvmInline public final value class GraphicsSurfaceZOrder {
+    method public int getZOrder();
+    property public final int zOrder;
+    field public static final androidx.compose.foundation.GraphicsSurfaceZOrder.Companion Companion;
+  }
+
+  public static final class GraphicsSurfaceZOrder.Companion {
+    method public int getBehind();
+    method public int getMediaOverlay();
+    method public int getOnTop();
+    property public final int Behind;
+    property public final int MediaOverlay;
+    property public final int OnTop;
+  }
+
   public final class HoverableKt {
     method public static androidx.compose.ui.Modifier hoverable(androidx.compose.ui.Modifier, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean enabled);
   }
@@ -248,6 +272,14 @@
     property public final androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.ScrollState,?> Saver;
   }
 
+  public interface SurfaceCoroutineScope extends androidx.compose.foundation.SurfaceScope kotlinx.coroutines.CoroutineScope {
+  }
+
+  public interface SurfaceScope {
+    method public void onChanged(android.view.Surface, kotlin.jvm.functions.Function3<? super android.view.Surface,? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> onChanged);
+    method public void onDestroyed(android.view.Surface, kotlin.jvm.functions.Function1<? super android.view.Surface,kotlin.Unit> onDestroyed);
+  }
+
   public final class SystemGestureExclusionKt {
     method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier);
     method public static androidx.compose.ui.Modifier systemGestureExclusion(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,androidx.compose.ui.geometry.Rect> exclusion);
@@ -404,8 +436,8 @@
   }
 
   public final class ScrollableKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
-    method public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ScrollableState {
@@ -1213,7 +1245,7 @@
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
-    method public static androidx.compose.ui.Modifier selectableGroup(androidx.compose.ui.Modifier);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier selectableGroup(androidx.compose.ui.Modifier);
   }
 
   public final class SelectableKt {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index b50584f..4a07721 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -66,6 +66,7 @@
         ComposableDemo("Vertical scroll") { VerticalScrollExample() },
         ComposableDemo("Controlled Scrollable Row") { ControlledScrollableRowSample() },
         ComposableDemo("Draw Modifiers") { DrawModifiersDemo() },
+        ComposableDemo("Graphics Surfaces") { GraphicsSurfaceDemo() },
         DemoCategory("Lazy lists", LazyListDemos),
         DemoCategory("Snapping", SnappingDemos),
         DemoCategory("Pagers", PagerDemos),
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt
new file mode 100644
index 0000000..4cc8c77
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/GraphicsSurfaceDemo.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 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.foundation.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.samples.EmbeddedGraphicsSurfaceColors
+import androidx.compose.foundation.samples.GraphicsSurfaceColors
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun GraphicsSurfaceDemo() {
+    Column(Modifier.verticalScroll(rememberScrollState())) {
+        Text("GraphicsSurface:")
+        GraphicsSurfaceColors()
+        Spacer(Modifier.height(50.dp))
+        Text("EmbeddedGraphicsSurface:")
+        EmbeddedGraphicsSurfaceColors()
+    }
+}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt
new file mode 100644
index 0000000..dc63c5f
--- /dev/null
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/GraphicsSurfaceSamples.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.samples
+
+import android.graphics.Rect
+import androidx.annotation.Sampled
+import androidx.compose.foundation.EmbeddedGraphicsSurface
+import androidx.compose.foundation.GraphicsSurface
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.dp
+import kotlin.math.sin
+
+@Sampled
+@Composable
+fun GraphicsSurfaceColors() {
+    GraphicsSurface(
+        modifier = Modifier.fillMaxWidth().height(400.dp)
+    ) {
+        // Resources can be initialized/cached here
+
+        // A surface is available, we can start rendering
+        onSurface { surface, width, height ->
+            var w = width
+            var h = height
+
+            // Initial draw to avoid a black frame
+            surface.lockCanvas(Rect(0, 0, w, h)).apply {
+                drawColor(Color.Blue.toArgb())
+                surface.unlockCanvasAndPost(this)
+            }
+
+            // React to surface dimension changes
+            surface.onChanged { newWidth, newHeight ->
+                w = newWidth
+                h = newHeight
+            }
+
+            // Cleanup if needed
+            surface.onDestroyed {
+            }
+
+            // Render loop, automatically cancelled by GraphicsSurface
+            // on surface destruction
+            while (true) {
+                withFrameNanos { time ->
+                    surface.lockCanvas(Rect(0, 0, w, h)).apply {
+                        val timeMs = time / 1_000_000L
+                        val t = 0.5f + 0.5f * sin(timeMs / 1_000.0f)
+                        drawColor(lerp(Color.Blue, Color.Green, t).toArgb())
+                        surface.unlockCanvasAndPost(this)
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Sampled
+@Composable
+fun EmbeddedGraphicsSurfaceColors() {
+    EmbeddedGraphicsSurface(
+        modifier = Modifier.fillMaxWidth().height(400.dp)
+    ) {
+        // Resources can be initialized/cached here
+
+        // A surface is available, we can start rendering
+        onSurface { surface, width, height ->
+            var w = width
+            var h = height
+
+            // Initial draw to avoid a black frame
+            surface.lockCanvas(Rect(0, 0, w, h)).apply {
+                drawColor(Color.Yellow.toArgb())
+                surface.unlockCanvasAndPost(this)
+            }
+
+            // React to surface dimension changes
+            surface.onChanged { newWidth, newHeight ->
+                w = newWidth
+                h = newHeight
+            }
+
+            // Cleanup if needed
+            surface.onDestroyed {
+            }
+
+            // Render loop, automatically cancelled by EmbeddedGraphicsSurface
+            // on surface destruction
+            while (true) {
+                withFrameNanos { time ->
+                    surface.lockCanvas(Rect(0, 0, w, h)).apply {
+                        val timeMs = time / 1_000_000L
+                        val t = 0.5f + 0.5f * sin(timeMs / 1_000.0f)
+                        drawColor(lerp(Color.Yellow, Color.Red, t).toArgb())
+                        surface.unlockCanvasAndPost(this)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt
new file mode 100644
index 0000000..6a10cd1
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/EmbeddedGraphicsSurfaceTest.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2020 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.foundation
+
+import android.graphics.PorterDuff
+import android.os.Build
+import android.view.Surface
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.dp
+import androidx.core.graphics.ColorUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import kotlin.math.roundToInt
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class EmbeddedGraphicsSurfaceTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    val size = 48.dp
+
+    @Test
+    fun testOnSurface() {
+        var surfaceRef: Surface? = null
+        var surfaceWidth = 0
+        var surfaceHeight = 0
+        var expectedSize = 0
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                size.toPx().roundToInt()
+            }
+
+            EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, width, height ->
+                    surfaceRef = surface
+                    surfaceWidth = width
+                    surfaceHeight = height
+                }
+            }
+        }
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(size)
+            .assertHeightIsEqualTo(size)
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceChanged() {
+        var surfaceWidth = 0
+        var surfaceHeight = 0
+        var expectedSize = 0
+
+        var desiredSize by mutableStateOf(size)
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                desiredSize.toPx().roundToInt()
+            }
+
+            EmbeddedGraphicsSurface(modifier = Modifier.size(desiredSize)) {
+                onSurface { surface, initWidth, initHeight ->
+                    surfaceWidth = initWidth
+                    surfaceHeight = initHeight
+
+                    surface.onChanged { newWidth, newHeight ->
+                        surfaceWidth = newWidth
+                        surfaceHeight = newHeight
+                    }
+                }
+            }
+        }
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(desiredSize)
+            .assertHeightIsEqualTo(desiredSize)
+
+        rule.runOnIdle {
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+
+        desiredSize = size * 2
+        val prevSurfaceWidth = surfaceWidth
+        val prevSurfaceHeight = surfaceHeight
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(desiredSize)
+            .assertHeightIsEqualTo(desiredSize)
+
+        rule.runOnIdle {
+            assertNotEquals(prevSurfaceWidth, surfaceWidth)
+            assertNotEquals(prevSurfaceHeight, surfaceHeight)
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceDestroyed() {
+        var surfaceRef: Surface? = null
+        var visible by mutableStateOf(true)
+
+        rule.setContent {
+            if (visible) {
+                EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+                    onSurface { surface, _, _ ->
+                        surfaceRef = surface
+
+                        surface.onDestroyed {
+                            surfaceRef = null
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+        }
+
+        visible = false
+
+        rule.runOnIdle {
+            assertNull(surfaceRef)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceRecreated() {
+        var surfaceCreatedCount = 0
+        var surfaceDestroyedCount = 0
+        var visible by mutableStateOf(true)
+
+        // NOTE: TextureView only destroys the surface when TextureView is detached from
+        // the window, and only creates when it gets attached to the window
+        rule.setContent {
+            if (visible) {
+                EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+                    onSurface { surface, _, _ ->
+                        surfaceCreatedCount++
+                        surface.onDestroyed {
+                            surfaceDestroyedCount++
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, surfaceCreatedCount)
+            assertEquals(0, surfaceDestroyedCount)
+            visible = false
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, surfaceCreatedCount)
+            assertEquals(1, surfaceDestroyedCount)
+            visible = true
+        }
+
+        rule.runOnIdle {
+            assertEquals(2, surfaceCreatedCount)
+            assertEquals(1, surfaceDestroyedCount)
+        }
+    }
+
+    @Test
+    fun testRender() {
+        var surfaceRef: Surface? = null
+        var expectedSize = 0
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                size.toPx().roundToInt()
+            }
+            EmbeddedGraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, _, _ ->
+                    surfaceRef = surface
+                    surface.lockHardwareCanvas().apply {
+                        drawColor(Color.Blue.toArgb())
+                        surface.unlockCanvasAndPost(this)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+        }
+
+        surfaceRef!!
+            .captureToImage(expectedSize, expectedSize)
+            .assertPixels { Color.Blue }
+    }
+
+    @Test
+    fun testNotOpaque() {
+        val translucentRed = Color(1.0f, 0.0f, 0.0f, 0.5f).toArgb()
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.White)
+                }
+                EmbeddedGraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("EmbeddedGraphicSurface"),
+                    isOpaque = false
+                ) {
+                    onSurface { surface, _, _ ->
+                        surface.lockHardwareCanvas().apply {
+                            drawColor(0x00000000, PorterDuff.Mode.CLEAR)
+                            drawColor(translucentRed)
+                            surface.unlockCanvasAndPost(this)
+                        }
+                    }
+                }
+            }
+        }
+
+        val expectedColor = Color(ColorUtils.compositeColors(translucentRed, Color.White.toArgb()))
+
+        rule
+            .onNodeWithTag("EmbeddedGraphicSurface")
+            .captureToImage()
+            .assertPixels { expectedColor }
+    }
+
+    @Test
+    fun testOpaque() {
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.Green)
+                }
+                EmbeddedGraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("EmbeddedGraphicSurface")
+                ) {
+                    onSurface { surface, _, _ ->
+                        surface.lockHardwareCanvas().apply {
+                            drawColor(Color.Red.toArgb())
+                            surface.unlockCanvasAndPost(this)
+                        }
+                    }
+                }
+            }
+        }
+
+        rule
+            .onNodeWithTag("EmbeddedGraphicSurface")
+            .captureToImage()
+            .assertPixels { Color.Red }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt
new file mode 100644
index 0000000..6fc417c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/GraphicsSurfaceTest.kt
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2020 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.foundation
+
+import android.graphics.Bitmap
+import android.graphics.PorterDuff
+import android.graphics.Rect
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.view.Choreographer
+import android.view.PixelCopy
+import android.view.Surface
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.dp
+import androidx.concurrent.futures.ResolvableFuture
+import androidx.core.graphics.ColorUtils
+import androidx.core.graphics.createBitmap
+import androidx.test.core.internal.os.HandlerExecutor
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+@RunWith(AndroidJUnit4::class)
+class GraphicsSurfaceTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    val size = 48.dp
+
+    @Test
+    fun testOnSurface() {
+        var surfaceRef: Surface? = null
+        var surfaceWidth = 0
+        var surfaceHeight = 0
+        var expectedSize = 0
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                size.toPx().roundToInt()
+            }
+
+            GraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, width, height ->
+                    surfaceRef = surface
+                    surfaceWidth = width
+                    surfaceHeight = height
+                }
+            }
+        }
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(size)
+            .assertHeightIsEqualTo(size)
+            .assertIsDisplayed()
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceChanged() {
+        var surfaceWidth = 0
+        var surfaceHeight = 0
+        var expectedSize = 0
+
+        var desiredSize by mutableStateOf(size)
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                desiredSize.toPx().roundToInt()
+            }
+
+            GraphicsSurface(modifier = Modifier.size(desiredSize)) {
+                onSurface { surface, _, _ ->
+                    surface.onChanged { newWidth, newHeight ->
+                        surfaceWidth = newWidth
+                        surfaceHeight = newHeight
+                    }
+                }
+            }
+        }
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(desiredSize)
+            .assertHeightIsEqualTo(desiredSize)
+
+        // onChanged() hasn't been called yet
+        rule.runOnIdle {
+            assertEquals(0, surfaceWidth)
+            assertEquals(0, surfaceHeight)
+        }
+
+        desiredSize = size * 2
+        val prevSurfaceWidth = surfaceWidth
+        val prevSurfaceHeight = surfaceHeight
+
+        rule.onRoot()
+            .assertWidthIsEqualTo(desiredSize)
+            .assertHeightIsEqualTo(desiredSize)
+
+        rule.runOnIdle {
+            assertNotEquals(prevSurfaceWidth, surfaceWidth)
+            assertNotEquals(prevSurfaceHeight, surfaceHeight)
+            assertEquals(expectedSize, surfaceWidth)
+            assertEquals(expectedSize, surfaceHeight)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceDestroyed() {
+        var surfaceRef: Surface? = null
+        var visible by mutableStateOf(true)
+
+        rule.setContent {
+            if (visible) {
+                GraphicsSurface(modifier = Modifier.size(size)) {
+                    onSurface { surface, _, _ ->
+                        surfaceRef = surface
+
+                        surface.onDestroyed {
+                            surfaceRef = null
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+        }
+
+        visible = false
+
+        rule.runOnIdle {
+            assertNull(surfaceRef)
+        }
+    }
+
+    @Test
+    fun testOnSurfaceRecreated() {
+        var surfaceCreatedCount = 0
+        var surfaceDestroyedCount = 0
+
+        var view: View? = null
+
+        rule.setContent {
+            view = LocalView.current
+            GraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, _, _ ->
+                    surfaceCreatedCount++
+                    surface.onDestroyed {
+                        surfaceDestroyedCount++
+                    }
+                }
+            }
+        }
+
+        // NOTE: SurfaceView only triggers a Surface destroy/create cycle on visibility
+        // change if its *own* visibility or the visibility of the window changes. Here
+        // we change the visibility of the window by setting the visibility of the root
+        // view (the host view in ViewRootImpl).
+        rule.runOnIdle {
+            assertEquals(1, surfaceCreatedCount)
+            assertEquals(0, surfaceDestroyedCount)
+            view?.rootView?.visibility = View.INVISIBLE
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, surfaceCreatedCount)
+            assertEquals(1, surfaceDestroyedCount)
+            view?.rootView?.visibility = View.VISIBLE
+        }
+
+        rule.runOnIdle {
+            assertEquals(2, surfaceCreatedCount)
+            assertEquals(1, surfaceDestroyedCount)
+        }
+    }
+
+    @Test
+    fun testRender() {
+        var surfaceRef: Surface? = null
+        var expectedSize = 0
+
+        rule.setContent {
+            expectedSize = with(LocalDensity.current) {
+                size.toPx().roundToInt()
+            }
+            GraphicsSurface(modifier = Modifier.size(size)) {
+                onSurface { surface, _, _ ->
+                    surfaceRef = surface
+                    surface.lockHardwareCanvas().apply {
+                        drawColor(Color.Blue.toArgb())
+                        surface.unlockCanvasAndPost(this)
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertNotNull(surfaceRef)
+        }
+
+        surfaceRef!!
+            .captureToImage(expectedSize, expectedSize)
+            .assertPixels { Color.Blue }
+    }
+
+    @Test
+    fun testZOrderDefault() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                GraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("GraphicSurface")
+                ) {
+                    onSurface { surface, _, _ ->
+                        // Draw > 3 frames to make sure the screenshot copy will pick up
+                        // a SurfaceFlinger composition that includes our Surface
+                        repeat(frameCount) {
+                            withFrameNanos {
+                                surface.lockHardwareCanvas().apply {
+                                    drawColor(Color.Blue.toArgb())
+                                    surface.unlockCanvasAndPost(this)
+                                }
+                                latch.countDown()
+                            }
+                        }
+                    }
+                }
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.Green)
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage()!!
+            .assertPixels { Color.Green }
+    }
+
+    @Test
+    fun testZOrderMediaOverlay() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                GraphicsSurface(
+                    modifier = Modifier.size(size),
+                    zOrder = GraphicsSurfaceZOrder.Behind
+                ) {
+                    onSurface { surface, _, _ ->
+                        // Draw > 3 frames to make sure the screenshot copy will pick up
+                        // a SurfaceFlinger composition that includes our Surface
+                        repeat(frameCount) {
+                            withFrameNanos {
+                                surface.lockHardwareCanvas().apply {
+                                    drawColor(Color.Blue.toArgb())
+                                    surface.unlockCanvasAndPost(this)
+                                }
+                                latch.countDown()
+                            }
+                        }
+                    }
+                }
+                GraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("GraphicSurface"),
+                    zOrder = GraphicsSurfaceZOrder.MediaOverlay
+                ) {
+                    onSurface { surface, _, _ ->
+                        surface.lockHardwareCanvas().apply {
+                            drawColor(Color.Red.toArgb())
+                            surface.unlockCanvasAndPost(this)
+                        }
+                        latch.countDown()
+                    }
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage()!!
+            .assertPixels { Color.Red }
+    }
+
+    @Test
+    fun testZOrderOnTop() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                GraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("GraphicSurface"),
+                    zOrder = GraphicsSurfaceZOrder.OnTop
+                ) {
+                    onSurface { surface, _, _ ->
+                        // Draw > 3 frames to make sure the screenshot copy will pick up
+                        // a SurfaceFlinger composition that includes our Surface
+                        repeat(frameCount) {
+                            withFrameNanos {
+                                surface.lockHardwareCanvas().apply {
+                                    drawColor(Color.Blue.toArgb())
+                                    surface.unlockCanvasAndPost(this)
+                                }
+                                latch.countDown()
+                            }
+                        }
+                    }
+                }
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.Green)
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage()!!
+            .assertPixels { Color.Blue }
+    }
+
+    @Test
+    fun testNotOpaque() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+        val translucentRed = Color(1.0f, 0.0f, 0.0f, 0.5f).toArgb()
+
+        rule.setContent {
+            Box(modifier = Modifier.size(size)) {
+                GraphicsSurface(
+                    modifier = Modifier
+                        .size(size)
+                        .testTag("GraphicSurface"),
+                    isOpaque = false,
+                    zOrder = GraphicsSurfaceZOrder.OnTop
+                ) {
+                    onSurface { surface, _, _ ->
+                        // Draw > 3 frames to make sure the screenshot copy will pick up
+                        // a SurfaceFlinger composition that includes our Surface
+                        repeat(frameCount) {
+                            withFrameNanos {
+                                surface.lockHardwareCanvas().apply {
+                                    // Since we are drawing a translucent color we need to
+                                    // clear first
+                                    drawColor(0x00000000, PorterDuff.Mode.CLEAR)
+                                    drawColor(translucentRed)
+                                    surface.unlockCanvasAndPost(this)
+                                }
+                                latch.countDown()
+                            }
+                        }
+                    }
+                }
+                Canvas(modifier = Modifier.size(size)) {
+                    drawRect(Color.White)
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        val expectedColor = Color(ColorUtils.compositeColors(translucentRed, Color.White.toArgb()))
+
+        rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage()!!
+            .assertPixels { expectedColor }
+    }
+
+    @Test
+    fun testSecure() {
+        val frameCount = 4
+        val latch = CountDownLatch(frameCount)
+
+        rule.setContent {
+            GraphicsSurface(
+                modifier = Modifier
+                    .size(size)
+                    .testTag("GraphicSurface"),
+                isSecure = true
+            ) {
+                onSurface { surface, _, _ ->
+                    // Draw > 3 frames to make sure the screenshot copy will pick up
+                    // a SurfaceFlinger composition that includes our Surface
+                    repeat(frameCount) {
+                        withFrameNanos {
+                            surface.lockHardwareCanvas().apply {
+                                drawColor(Color.Blue.toArgb())
+                                surface.unlockCanvasAndPost(this)
+                            }
+                            latch.countDown()
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!latch.await(5, TimeUnit.SECONDS)) {
+            throw AssertionError("Failed waiting for render")
+        }
+
+        val screen = rule
+            .onNodeWithTag("GraphicSurface")
+            .screenshotToImage(true)
+
+        // Before API 33 taking a screenshot with a secure surface returns null
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            assertNull(screen)
+        } else {
+            screen!!.assertPixels { Color.Black }
+        }
+    }
+}
+
+/**
+ * Returns an ImageBitmap containing a screenshot of the device. On API < 33,
+ * a secure surface present on screen can cause this function to return null.
+ */
+private fun SemanticsNodeInteraction.screenshotToImage(
+    hasSecureSurfaces: Boolean = false
+): ImageBitmap? {
+    val instrumentation = InstrumentationRegistry.getInstrumentation()
+    instrumentation.waitForIdleSync()
+
+    val uiAutomation = instrumentation.uiAutomation
+
+    val node = fetchSemanticsNode()
+    val view = (node.root as ViewRootForTest).view
+
+    val bitmapFuture: ResolvableFuture<Bitmap> = ResolvableFuture.create()
+
+    val mainExecutor = HandlerExecutor(Handler(Looper.getMainLooper()))
+    mainExecutor.execute {
+        Choreographer.getInstance().postFrameCallback {
+            val location = IntArray(2)
+            view.getLocationOnScreen(location)
+
+            val bounds = node.boundsInRoot.translate(
+                location[0].toFloat(),
+                location[1].toFloat()
+            )
+
+            // do multiple retries of uiAutomation.takeScreenshot because it is known to return null
+            // on API 31+ b/257274080
+            var bitmap: Bitmap? = null
+            var i = 0
+            while (i < 3 && bitmap == null) {
+                bitmap = uiAutomation.takeScreenshot()
+                i++
+            }
+
+            if (bitmap != null) {
+                bitmap = Bitmap.createBitmap(
+                    bitmap,
+                    bounds.left.toInt(),
+                    bounds.top.toInt(),
+                    bounds.width.toInt(),
+                    bounds.height.toInt()
+                )
+                bitmapFuture.set(bitmap)
+            } else {
+                if (hasSecureSurfaces) {
+                    // may be null on older API levels when a secure surface is showing
+                    bitmapFuture.set(null)
+                }
+                // if we don't show secure surfaces, let the future timeout on get()
+            }
+        }
+    }
+
+    return try {
+        bitmapFuture.get(5, TimeUnit.SECONDS)?.asImageBitmap()
+    } catch (e: ExecutionException) {
+        null
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+internal fun Surface.captureToImage(width: Int, height: Int): ImageBitmap {
+    val bitmap = createBitmap(width, height)
+
+    val latch = CountDownLatch(1)
+    var copyResult = 0
+    val onCopyFinished = PixelCopy.OnPixelCopyFinishedListener { result ->
+        copyResult = result
+        latch.countDown()
+        android.util.Log.d("Test", Thread.currentThread().toString())
+    }
+
+    PixelCopy.request(
+        this,
+        Rect(0, 0, width, height),
+        bitmap,
+        onCopyFinished,
+        Handler(Looper.getMainLooper())
+    )
+
+    if (!latch.await(1, TimeUnit.SECONDS)) {
+        throw AssertionError("Failed waiting for PixelCopy!")
+    }
+
+    if (copyResult != PixelCopy.SUCCESS) {
+        throw AssertionError("PixelCopy failed!")
+    }
+
+    return bitmap.asImageBitmap()
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt
new file mode 100644
index 0000000..17a6823
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/GraphicsSurface.kt
@@ -0,0 +1,438 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import android.graphics.PixelFormat
+import android.graphics.SurfaceTexture
+import android.view.Surface
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import android.view.TextureView
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.viewinterop.AndroidView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.launch
+
+/**
+ * [SurfaceScope] is a scoped environment provided by [GraphicsSurface] and
+ * [EmbeddedGraphicsSurface] to handle [Surface] lifecycle events.
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+interface SurfaceScope {
+    /**
+     * Invokes [onChanged] when the surface's geometry (width and height) changes.
+     * Always invoked on the main thread.
+     */
+    @Suppress("PrimitiveInLambda")
+    fun Surface.onChanged(onChanged: Surface.(width: Int, height: Int) -> Unit)
+
+    /**
+     * Invokes [onDestroyed] when the surface is destroyed. All rendering into
+     * the surface should stop immediately after [onDestroyed] is invoked.
+     * Always invoked on the main thread.
+     */
+    fun Surface.onDestroyed(onDestroyed: Surface.() -> Unit)
+}
+
+/**
+ * [SurfaceCoroutineScope] is a scoped environment provided by
+ * [GraphicsSurface] and [EmbeddedGraphicsSurface] when a new [Surface] is
+ * created. This environment is a coroutine scope that also provides access to
+ * a [SurfaceScope] environment which can itself be used to handle other [Surface]
+ * lifecycle events.
+ *
+ * @see SurfaceScope
+ * @see GraphicsSurfaceScope
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+interface SurfaceCoroutineScope : SurfaceScope, CoroutineScope
+
+/**
+ * [GraphicsSurfaceScope] is a scoped environment provided when a [GraphicsSurface]
+ * or [EmbeddedGraphicsSurface] is first initialized. This environment can be
+ * used to register a lambda to invoke when a new [Surface] associated with the
+ * [GraphicsSurface]/[EmbeddedGraphicsSurface] is created.
+ */
+interface GraphicsSurfaceScope {
+    /**
+     * Invokes [onSurface] when a new [Surface] is created. The [onSurface] lambda
+     * is invoked on the main thread as part of a [SurfaceCoroutineScope] to provide
+     * a coroutine context.
+     *
+     * @param onSurface Callback invoked when a new [Surface] is created. The initial
+     *                  dimensions of the surface are provided.
+     */
+    @Suppress("PrimitiveInLambda")
+    fun onSurface(
+        onSurface: suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit
+    )
+}
+
+/**
+ * Base class for [GraphicsSurface] and [EmbeddedGraphicsSurface] state. This class
+ * provides methods to properly dispatch lifecycle events on [Surface] creation,
+ * change, and destruction. Surface creation is treated as a coroutine launch,
+ * using the specified [scope] as the parent. This scope must be the main thread scope.
+ */
+private abstract class BaseGraphicsSurfaceState(val scope: CoroutineScope) :
+    GraphicsSurfaceScope, SurfaceScope {
+
+    private var onSurface:
+        (suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit)? = null
+    private var onSurfaceChanged: (Surface.(width: Int, height: Int) -> Unit)? = null
+    private var onSurfaceDestroyed: (Surface.() -> Unit)? = null
+
+    private var job: Job? = null
+
+    override fun onSurface(
+        onSurface: suspend SurfaceCoroutineScope.(surface: Surface, width: Int, height: Int) -> Unit
+    ) {
+        this.onSurface = onSurface
+    }
+
+    override fun Surface.onChanged(onChanged: Surface.(width: Int, height: Int) -> Unit) {
+        onSurfaceChanged = onChanged
+    }
+
+    override fun Surface.onDestroyed(onDestroyed: Surface.() -> Unit) {
+        onSurfaceDestroyed = onDestroyed
+    }
+
+    /**
+     * Dispatch a surface creation event by launching a new coroutine in [scope].
+     * Any previous job from a previous surface creation dispatch is cancelled.
+     */
+    fun dispatchSurfaceCreated(surface: Surface, width: Int, height: Int) {
+        if (onSurface != null) {
+            job = scope.launch(start = CoroutineStart.UNDISPATCHED) {
+                job?.cancelAndJoin()
+                val receiver =
+                    object : SurfaceCoroutineScope, SurfaceScope by this@BaseGraphicsSurfaceState,
+                        CoroutineScope by this {}
+                onSurface?.invoke(receiver, surface, width, height)
+            }
+        }
+    }
+
+    /**
+     * Dispatch a surface change event, providing the surface's new width and height.
+     * Must be invoked from the main thread.
+     */
+    fun dispatchSurfaceChanged(surface: Surface, width: Int, height: Int) {
+        onSurfaceChanged?.invoke(surface, width, height)
+    }
+
+    /**
+     * Dispatch a surface destruction event. Any pending job from [dispatchSurfaceCreated]
+     * is cancelled before dispatching the event. Must be invoked from the main thread.
+     */
+    fun dispatchSurfaceDestroyed(surface: Surface) {
+        onSurfaceDestroyed?.invoke(surface)
+        job?.cancel()
+        job = null
+    }
+}
+
+private class GraphicsSurfaceState(scope: CoroutineScope) : BaseGraphicsSurfaceState(scope),
+    SurfaceHolder.Callback {
+
+    var lastWidth = -1
+    var lastHeight = -1
+
+    override fun surfaceCreated(holder: SurfaceHolder) {
+        val frame = holder.surfaceFrame
+        lastWidth = frame.width()
+        lastHeight = frame.height()
+
+        dispatchSurfaceCreated(holder.surface, lastWidth, lastHeight)
+    }
+
+    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+        if (lastWidth != width || lastHeight != height) {
+            lastWidth = width
+            lastHeight = height
+
+            dispatchSurfaceChanged(holder.surface, width, height)
+        }
+    }
+
+    override fun surfaceDestroyed(holder: SurfaceHolder) {
+        dispatchSurfaceDestroyed(holder.surface)
+    }
+}
+
+@Composable
+private fun rememberGraphicsSurfaceState(): GraphicsSurfaceState {
+    val scope = rememberCoroutineScope()
+    return remember { GraphicsSurfaceState(scope) }
+}
+
+@JvmInline
+value class GraphicsSurfaceZOrder private constructor(val zOrder: Int) {
+    companion object {
+        val Behind = GraphicsSurfaceZOrder(0)
+        val MediaOverlay = GraphicsSurfaceZOrder(1)
+        val OnTop = GraphicsSurfaceZOrder(2)
+    }
+}
+
+/**
+ * Provides a dedicated drawing [Surface] as a separate layer positioned by default behind
+ * the window holding the [GraphicsSurface] composable. Because [GraphicsSurface] uses
+ * a separate window layer, graphics composition is handled by the system compositor which
+ * can bypass the GPU and provide better performance and power usage characteristics compared
+ * to [EmbeddedGraphicsSurface]. It is therefore recommended to use [GraphicsSurface]
+ * whenever possible.
+ *
+ * The z-ordering of the surface can be controlled using the [zOrder] parameter:
+ *
+ * - [GraphicsSurfaceZOrder.Behind]: positions the surface behind the window
+ * - [GraphicsSurfaceZOrder.MediaOverlay]: positions the surface behind the window but
+ *   above other [GraphicsSurfaceZOrder.Behind] surfaces
+ * - [GraphicsSurfaceZOrder.OnTop]: positions the surface above the window
+ *
+ * The drawing surface is opaque by default, which can be controlled with the [isOpaque]
+ * parameter. When the surface is transparent, you may need to change the z-order to
+ * see something behind the surface.
+ *
+ * To start rendering, the caller must first acquire the [Surface] when it's created.
+ * This is achieved by providing the [onInit] lambda, which allows the caller to
+ * register an appropriate [GraphicsSurfaceScope.onSurface] callback. The [onInit]
+ * lambda can also be used to initialize/cache resources needed once a surface is
+ * available.
+ *
+ * After acquiring a surface, the caller can start rendering into it. Rendering into a
+ * surface can be done from any thread.
+ *
+ * It is recommended to register the [SurfaceScope.onChanged] and [SurfaceScope.onDestroyed]
+ * callbacks to properly handle the lifecycle of the surface and react to dimension
+ * changes. You must ensure that the rendering thread stops interacting with the surface
+ * when the [SurfaceScope.onDestroyed] callback is invoked.
+ *
+ * If a [surfaceSize] is specified (set to non-[IntSize.Zero]), the surface will use
+ * the specified size instead of the layout size of this composable. The surface will
+ * be stretched at render time to fit the layout size. This can be used for instance to
+ * render at a lower resolution for performance reasons.
+ *
+ * @param modifier Modifier to be applied to the [GraphicsSurface]
+ * @param isOpaque Whether the managed surface should be opaque or transparent.
+ * @param zOrder Sets the z-order of the surface relative to its parent window.
+ * @param surfaceSize Sets the surface size independently of the layout size of
+ *                    this [GraphicsSurface]. If set to [IntSize.Zero], the surface
+ *                    size will be equal to the [GraphicsSurface] layout size.
+ * @param isSecure Control whether the surface view's content should be treated as
+ *                 secure, preventing it from appearing in screenshots or from being
+ *                 viewed on non-secure displays.
+ * @param onInit Lambda invoked on first composition. This lambda can be used to
+ *               declare a [GraphicsSurfaceScope.onSurface] callback that will be
+ *               invoked when a surface is available.
+ *
+ * @sample androidx.compose.foundation.samples.GraphicsSurfaceColors
+ */
+@Composable
+fun GraphicsSurface(
+    modifier: Modifier = Modifier,
+    isOpaque: Boolean = true,
+    zOrder: GraphicsSurfaceZOrder = GraphicsSurfaceZOrder.Behind,
+    surfaceSize: IntSize = IntSize.Zero,
+    isSecure: Boolean = false,
+    onInit: GraphicsSurfaceScope.() -> Unit
+) {
+    val state = rememberGraphicsSurfaceState()
+
+    AndroidView(
+        factory = { context ->
+            SurfaceView(context).apply {
+                state.onInit()
+                holder.addCallback(state)
+            }
+        },
+        modifier = modifier,
+        onReset = { },
+        update = { view ->
+            if (surfaceSize != IntSize.Zero) {
+                view.holder.setFixedSize(surfaceSize.width, surfaceSize.height)
+            } else {
+                view.holder.setSizeFromLayout()
+            }
+
+            view.holder.setFormat(
+                if (isOpaque) {
+                    PixelFormat.OPAQUE
+                } else {
+                    PixelFormat.TRANSLUCENT
+                }
+            )
+
+            when (zOrder) {
+                GraphicsSurfaceZOrder.Behind -> view.setZOrderOnTop(false)
+                GraphicsSurfaceZOrder.MediaOverlay -> view.setZOrderMediaOverlay(true)
+                GraphicsSurfaceZOrder.OnTop -> view.setZOrderOnTop(true)
+            }
+
+            view.setSecure(isSecure)
+        }
+    )
+}
+
+private class EmbeddedGraphicsSurfaceState(scope: CoroutineScope) : BaseGraphicsSurfaceState(scope),
+    TextureView.SurfaceTextureListener {
+
+    var surfaceSize = IntSize.Zero
+
+    private var surfaceTextureSurface: Surface? = null
+
+    override fun onSurfaceTextureAvailable(
+        surfaceTexture: SurfaceTexture,
+        width: Int,
+        height: Int
+    ) {
+        var w = width
+        var h = height
+
+        if (surfaceSize != IntSize.Zero) {
+            w = surfaceSize.width
+            h = surfaceSize.height
+            surfaceTexture.setDefaultBufferSize(w, h)
+        }
+
+        val surface = Surface(surfaceTexture)
+        surfaceTextureSurface = surface
+
+        dispatchSurfaceCreated(surface, w, h)
+    }
+
+    override fun onSurfaceTextureSizeChanged(
+        surfaceTexture: SurfaceTexture,
+        width: Int,
+        height: Int
+    ) {
+        var w = width
+        var h = height
+
+        if (surfaceSize != IntSize.Zero) {
+            w = surfaceSize.width
+            h = surfaceSize.height
+            surfaceTexture.setDefaultBufferSize(w, h)
+        }
+
+        dispatchSurfaceChanged(surfaceTextureSurface!!, w, h)
+    }
+
+    override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
+        dispatchSurfaceDestroyed(surfaceTextureSurface!!)
+        surfaceTextureSurface = null
+        return true
+    }
+
+    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
+        // onSurfaceTextureUpdated is called when the content of the SurfaceTexture
+        // has changed, which is not relevant to us since we are the producer here
+    }
+}
+
+@Composable
+private fun rememberEmbeddedGraphicsSurfaceState(): EmbeddedGraphicsSurfaceState {
+    val scope = rememberCoroutineScope()
+    return remember { EmbeddedGraphicsSurfaceState(scope) }
+}
+
+/**
+ * Provides a dedicated drawing [Surface] embedded directly in the UI hierarchy.
+ * Unlike [GraphicsSurface], [EmbeddedGraphicsSurface] positions its surface as a
+ * regular element inside the composable hierarchy. This means that graphics
+ * composition is handled like any other UI widget, using the GPU. This can lead
+ * to increased power and memory bandwidth usage compared to [GraphicsSurface]. It
+ * is therefore recommended to use [GraphicsSurface] when possible.
+ *
+ * [EmbeddedGraphicsSurface] can however be useful when interactions with other widgets
+ * is necessary, for instance if the surface needs to be "sandwiched" between two
+ * other widgets, or if it must participate in visual effects driven by
+ * a `Modifier.graphicsLayer{}`.
+ *
+ * The drawing surface is opaque by default, which can be controlled with the [isOpaque]
+ * parameter.
+ *
+ * To start rendering, the caller must first acquire the [Surface] when it's created.
+ * This is achieved by providing the [onInit] lambda, which allows the caller to
+ * register an appropriate [GraphicsSurfaceScope.onSurface] callback. The [onInit]
+ * lambda can also be used to initialize/cache resources needed once a surface is
+ * available.
+ *
+ * After acquiring a surface, the caller can start rendering into it. Rendering into a
+ * surface can be done from any thread.
+ *
+ * It is recommended to register the [SurfaceScope.onChanged] and [SurfaceScope.onDestroyed]
+ * callbacks to properly handle the lifecycle of the surface and react to dimension
+ * changes. You must ensure that the rendering thread stops interacting with the surface
+ * when the [SurfaceScope.onDestroyed] callback is invoked.
+ *
+ * If a [surfaceSize] is specified (set to non-[IntSize.Zero]), the surface will use
+ * the specified size instead of the layout size of this composable. The surface will
+ * be stretched at render time to fit the layout size. This can be used for instance to
+ * render at a lower resolution for performance reasons.
+ *
+ * @param modifier Modifier to be applied to the [GraphicsSurface]
+ * @param isOpaque Whether the managed surface should be opaque or transparent. If
+ *                 transparent and [isMediaOverlay] is `false`, the surface will
+ *                 be positioned above the parent window.
+ * @param surfaceSize Sets the surface size independently of the layout size of
+ *                    this [GraphicsSurface]. If set to [IntSize.Zero], the surface
+ *                    size will be equal to the [GraphicsSurface] layout size.
+ * @param onInit Lambda invoked on first composition. This lambda can be used to
+ *               declare a [GraphicsSurfaceScope.onSurface] callback that will be
+ *               invoked when a surface is available.
+ *
+ * @sample androidx.compose.foundation.samples.EmbeddedGraphicsSurfaceColors
+ */
+@Composable
+fun EmbeddedGraphicsSurface(
+    modifier: Modifier = Modifier,
+    isOpaque: Boolean = true,
+    surfaceSize: IntSize = IntSize.Zero,
+    onInit: GraphicsSurfaceScope.() -> Unit
+) {
+    val state = rememberEmbeddedGraphicsSurfaceState()
+
+    AndroidView(
+        factory = { context ->
+            TextureView(context).apply {
+                state.surfaceSize = surfaceSize
+                state.onInit()
+                surfaceTextureListener = state
+            }
+        },
+        modifier = modifier,
+        onReset = { },
+        update = { view ->
+            if (surfaceSize != IntSize.Zero) {
+                view.surfaceTexture?.setDefaultBufferSize(surfaceSize.width, surfaceSize.height)
+            }
+            state.surfaceSize = surfaceSize
+            view.isOpaque = isOpaque
+        }
+    )
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 2a770bc..622afb5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.annotation.FloatRange
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Brush
@@ -40,6 +41,7 @@
  * @param color color to paint background with
  * @param shape desired shape of the background
  */
+@Stable
 fun Modifier.background(
     color: Color,
     shape: Shape = RectangleShape
@@ -70,6 +72,7 @@
  * @param alpha Opacity to be applied to the [brush], with `0` being completely transparent and
  * `1` being completely opaque. The value must be between `0` and `1`.
  */
+@Stable
 fun Modifier.background(
     brush: Brush,
     shape: Shape = RectangleShape,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
index 4b77a1d..d173f4a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
@@ -135,6 +135,7 @@
  * Note: this modifier and corresponding APIs are experimental pending some refinements in the API
  * surface, mostly related to customisation params.
  */
+@Stable
 @ExperimentalFoundationApi
 fun Modifier.basicMarquee(
     iterations: Int = DefaultMarqueeIterations,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
index 26deed4..dd0593b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicTooltip.kt
@@ -104,6 +104,7 @@
  * @param mutatorMutex [MutatorMutex] used to ensure that for all of the tooltips associated
  * with the mutator mutex, only one will be shown on the screen at any time.
  */
+@Stable
 fun BasicTooltipState(
     initialIsVisible: Boolean = false,
     isPersistent: Boolean = true,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
index 0bae435..330d630 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation
 
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.CacheDrawModifierNode
 import androidx.compose.ui.draw.CacheDrawScope
@@ -63,6 +64,7 @@
  * @param border [BorderStroke] class that specifies border appearance, such as size and color
  * @param shape shape of the border
  */
+@Stable
 fun Modifier.border(border: BorderStroke, shape: Shape = RectangleShape) =
     border(width = border.width, brush = border.brush, shape = shape)
 
@@ -76,6 +78,7 @@
  * @param color color to paint the border with
  * @param shape shape of the border
  */
+@Stable
 fun Modifier.border(width: Dp, color: Color, shape: Shape = RectangleShape) =
     border(width, SolidColor(color), shape)
 
@@ -90,6 +93,7 @@
  * @param brush brush to paint the border with
  * @param shape shape of the border
  */
+@Stable
 fun Modifier.border(width: Dp, brush: Brush, shape: Shape) =
     this then BorderModifierNodeElement(width, brush, shape)
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
index d817e37..5a4da47 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.geometry.Rect
@@ -33,6 +34,7 @@
  *
  * @param orientation orientation of the scrolling
  */
+@Stable
 fun Modifier.clipScrollableContainer(orientation: Orientation) =
     then(
         if (orientation == Orientation.Vertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 0548c70..88c8618 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.BringIntoViewRequesterNode
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusEventModifierNode
 import androidx.compose.ui.focus.FocusProperties
@@ -101,6 +102,7 @@
  *
  * @sample androidx.compose.foundation.samples.FocusableFocusGroupSample
  */
+@Stable
 fun Modifier.focusGroup(): Modifier {
     return this
         .then(focusGroupInspectorInfo)
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 db49a38..47afcb9 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
@@ -110,6 +110,7 @@
  * @param interactionSource [MutableInteractionSource] that will be used to emit
  * drag events when this scrollable is being dragged.
  */
+@Stable
 @OptIn(ExperimentalFoundationApi::class)
 fun Modifier.scrollable(
     state: ScrollableState,
@@ -163,6 +164,7 @@
  * Note: This API is experimental as it brings support for some experimental features:
  * [overscrollEffect] and [bringIntoViewScroller].
  */
+@Stable
 @ExperimentalFoundationApi
 fun Modifier.scrollable(
     state: ScrollableState,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/SelectableGroup.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/SelectableGroup.kt
index 913d91f..b6ab655 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/SelectableGroup.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/SelectableGroup.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.selection
 
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.selectableGroup
 import androidx.compose.ui.semantics.semantics
@@ -26,6 +27,7 @@
  *
  * @see selectableGroup
  */
+@Stable
 fun Modifier.selectableGroup() = this.semantics {
     selectableGroup()
 }
diff --git a/compose/lint/internal-lint-checks/build.gradle b/compose/lint/internal-lint-checks/build.gradle
index 3039214..f2132be 100644
--- a/compose/lint/internal-lint-checks/build.gradle
+++ b/compose/lint/internal-lint-checks/build.gradle
@@ -26,6 +26,7 @@
     compileOnly(libs.androidLintApi)
     compileOnly(libs.kotlinStdlib)
     implementation(project(":compose:lint:common"))
+    implementation(project(":collection:collection"))
 
     testImplementation(project(":compose:lint:common-test"))
     testImplementation(libs.kotlinStdlib)
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt
new file mode 100644
index 0000000..2d470e0
--- /dev/null
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/AsCollectionDetector.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.lint
+
+import androidx.collection.MutableObjectList
+import androidx.collection.MutableScatterMap
+import androidx.collection.MutableScatterSet
+import androidx.collection.ObjectList
+import androidx.collection.ScatterMap
+import androidx.collection.ScatterSet
+import androidx.collection.scatterSetOf
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.impl.source.PsiClassReferenceType
+import java.util.EnumSet
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+
+/**
+ * Using [ScatterMap.asMap], [ScatterSet.asSet], [ObjectList.asList], or their mutable
+ * counterparts indicates that the developer may be using the collection incorrectly.
+ * Using the interfaces is slower access. It is best to use those only for when it touches
+ * public API.
+ */
+class AsCollectionDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableUastTypes() = listOf<Class<out UElement>>(
+        UCallExpression::class.java
+    )
+
+    override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
+        override fun visitCallExpression(node: UCallExpression) {
+            val methodName = node.methodName ?: return
+            if (methodName in MethodNames) {
+                val receiverType = node.receiverType as? PsiClassReferenceType ?: return
+                val qualifiedName = receiverType.reference.qualifiedName ?: return
+                val indexOfAngleBracket = qualifiedName.indexOf('<')
+                if (indexOfAngleBracket > 0 &&
+                    qualifiedName.substring(0, indexOfAngleBracket) in CollectionClasses
+                ) {
+                    context.report(
+                        ISSUE,
+                        node,
+                        context.getLocation(node),
+                        "Use method $methodName() only for public API usage"
+                    )
+                }
+            }
+        }
+    }
+
+    companion object {
+        private val MethodNames = scatterSetOf(
+            "asMap",
+            "asMutableMap",
+            "asSet",
+            "asMutableSet",
+            "asList",
+            "asMutableList"
+        )
+        private val CollectionClasses = scatterSetOf(
+            ScatterMap::class.qualifiedName,
+            MutableScatterMap::class.qualifiedName,
+            ScatterSet::class.qualifiedName,
+            MutableScatterSet::class.qualifiedName,
+            ObjectList::class.qualifiedName,
+            MutableObjectList::class.qualifiedName,
+        )
+
+        private val AsCollectionDetectorId = "AsCollectionCall"
+
+        val ISSUE = Issue.create(
+            id = AsCollectionDetectorId,
+            briefDescription = "High performance collections don't implement standard collection " +
+                "interfaces so that they can remain high performance. Converting to standard " +
+                "collections wraps the classes with another object. Use these interface " +
+                "wrappers only for exposing to public API.",
+            explanation = "ScatterMap, ScatterSet, and AnyList are written for high " +
+                "performance access. Using the standard collection interfaces for these classes " +
+                "forces slower performance access to these collections. The methods returning " +
+                "these interfaces should be limited to public API, where standard collection " +
+                "interfaces are expected.",
+            category = Category.PERFORMANCE, priority = 3, severity = Severity.ERROR,
+            implementation = Implementation(
+                AsCollectionDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE)
+            )
+        )
+    }
+}
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
index 7e43f93..f13628f 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ComposeIssueRegistry.kt
@@ -28,6 +28,7 @@
     override val api = 14
     override val issues get(): List<Issue> {
         return listOf(
+            AsCollectionDetector.ISSUE,
             ExceptionMessageDetector.ISSUE,
             ListIteratorDetector.ISSUE,
             SteppedForLoopDetector.ISSUE,
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt
new file mode 100644
index 0000000..39ce2a1
--- /dev/null
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/AsCollectionDetectorTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/* ktlint-disable max-line-length */
+@RunWith(Parameterized::class)
+class AsCollectionDetectorTest(
+    val types: CollectionType
+) : LintDetectorTest() {
+
+    override fun getDetector(): Detector = AsCollectionDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(AsCollectionDetector.ISSUE)
+
+    private val collectionTilde = "~".repeat(types.collection.length)
+
+    @Test
+    fun immutableAsImmutable() {
+        lint().files(
+            ScatterMapClass,
+            ScatterSetClass,
+            ObjectListClass,
+            kotlin(
+                """
+                        package androidx.compose.lint
+
+                        import androidx.collection.${types.immutable}
+
+                        fun foo(collection: ${types.immutable}${types.params}): ${types.collection}${types.params} =
+                            collection.as${types.collection}()
+                        """
+            )
+        ).run().expect(
+            """
+src/androidx/compose/lint/test.kt:7: Error: Use method as${types.collection}() only for public API usage [AsCollectionCall]
+                            collection.as${types.collection}()
+                            ~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+            """
+        )
+    }
+
+    @Test
+    fun mutableAsImmutable() {
+        lint().files(
+            ScatterMapClass,
+            ScatterSetClass,
+            ObjectListClass,
+            kotlin(
+                """
+                        package androidx.compose.lint
+
+                        import androidx.collection.Mutable${types.immutable}
+
+                        fun foo(collection: Mutable${types.immutable}${types.params}): ${types.collection}${types.params} =
+                            collection.as${types.collection}()
+                        """
+            )
+        ).run().expect(
+            """
+src/androidx/compose/lint/test.kt:7: Error: Use method as${types.collection}() only for public API usage [AsCollectionCall]
+                            collection.as${types.collection}()
+                            ~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+            """
+        )
+    }
+
+    @Test
+    fun mutableAsMutable() {
+        lint().files(
+            ScatterMapClass,
+            ScatterSetClass,
+            ObjectListClass,
+            kotlin(
+                """
+                        package androidx.compose.lint
+
+                        import androidx.collection.Mutable${types.immutable}
+
+                        fun foo(collection: Mutable${types.immutable}${types.params}): Mutable${types.collection}${types.params} =
+                            collection.asMutable${types.collection}()
+                        """
+            )
+        ).run().expect(
+            """
+src/androidx/compose/lint/test.kt:7: Error: Use method asMutable${types.collection}() only for public API usage [AsCollectionCall]
+                            collection.asMutable${types.collection}()
+                            ~~~~~~~~~~~~~~~~~~~~$collectionTilde~~
+1 errors, 0 warnings
+            """
+        )
+    }
+
+    @Test
+    fun nonCollectionAs() {
+        lint().files(
+            kotlin(
+                """
+                        package androidx.compose.lint
+
+                        fun foo(): ${types.collection}${types.params} =
+                            WeirdCollection().as${types.collection}()
+
+                        class WeirdCollection {
+                            fun asList(): List<String>? = null
+                            fun asSet(): Set<String>? = null
+                            fun asMap(): Map<String, String>? = null
+                        }
+                        """
+            )
+        ).run().expectClean()
+    }
+
+    class CollectionType(
+        val immutable: String,
+        val collection: String,
+        val params: String
+    )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters() = listOf(
+            CollectionType("ScatterMap", "Map", "<String, String>"),
+            CollectionType("ScatterSet", "Set", "<String>"),
+            CollectionType("ObjectList", "List", "<String>")
+        )
+
+        val ScatterMapClass = kotlin(
+            """
+            package androidx.collection
+            sealed class ScatterMap<K, V> {
+                fun asMap(): Map<K, V> = mapOf()
+            }
+
+            class MutableScatterMap<K, V> : ScatterMap<K, V>() {
+                fun asMutableMap(): MutableMap<K, V> = mutableMapOf()
+            }
+            """.trimIndent()
+        )
+
+        val ScatterSetClass = kotlin(
+            """
+            package androidx.collection
+            sealed class ScatterSet<E> {
+                fun asSet(): Set<E> = setOf()
+            }
+
+            class MutableScatterSet<E> : ScatterSet<E>() {
+                fun asMutableSet(): MutableSet<E> = mutableSetOf()
+            }
+            """.trimIndent()
+        )
+
+        val ObjectListClass = kotlin(
+            """
+            package androidx.collection
+            sealed class ObjectList<E> {
+                fun asList(): List<E> = listOf()
+            }
+
+            class MutableObjectList<E> : ObjectList<E>() {
+                fun asMutableList(): MutableList<E> = mutableListOf()
+            }
+            """.trimIndent()
+        )
+    }
+}
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index d319950..989002c 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -660,7 +660,10 @@
   public final class ScaffoldKt {
     method @androidx.compose.runtime.Composable public static void Scaffold(androidx.compose.foundation.layout.WindowInsets contentWindowInsets, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional boolean isFloatingActionButtonDocked, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional boolean isFloatingActionButtonDocked, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static boolean getScaffoldSubcomposeInMeasureFix();
     method @androidx.compose.runtime.Composable public static androidx.compose.material.ScaffoldState rememberScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static void setScaffoldSubcomposeInMeasureFix(boolean);
+    property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final boolean ScaffoldSubcomposeInMeasureFix;
   }
 
   @androidx.compose.runtime.Stable public final class ScaffoldState {
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index d319950..989002c 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -660,7 +660,10 @@
   public final class ScaffoldKt {
     method @androidx.compose.runtime.Composable public static void Scaffold(androidx.compose.foundation.layout.WindowInsets contentWindowInsets, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional boolean isFloatingActionButtonDocked, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional boolean isFloatingActionButtonDocked, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static boolean getScaffoldSubcomposeInMeasureFix();
     method @androidx.compose.runtime.Composable public static androidx.compose.material.ScaffoldState rememberScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
+    method @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static void setScaffoldSubcomposeInMeasureFix(boolean);
+    property @SuppressCompatibility @androidx.compose.material.ExperimentalMaterialApi public static final boolean ScaffoldSubcomposeInMeasureFix;
   }
 
   @androidx.compose.runtime.Stable public final class ScaffoldState {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
index c77c4e8..85e9194 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
@@ -39,6 +39,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.SubcomposeLayout
@@ -67,8 +68,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import kotlin.math.roundToInt
 import kotlinx.coroutines.runBlocking
 import org.junit.Ignore
@@ -356,7 +357,8 @@
         }
         with(rule.density) {
             assertThat(fabPosition.x).isWithin(1f).of(
-                (rule.rootWidth().toPx() - fabSize.width) / 2f)
+                (rule.rootWidth().toPx() - fabSize.width) / 2f
+            )
         }
         val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
         assertThat(fabPosition.y).isEqualTo(expectedFabY)
@@ -396,7 +398,8 @@
         }
         with(rule.density) {
             assertThat(fabPosition.x).isWithin(1f).of(
-                rule.rootWidth().toPx() - fabSize.width - fabSpacing.toPx())
+                rule.rootWidth().toPx() - fabSize.width - fabSpacing.toPx()
+            )
         }
         val expectedFabY = bottomBarPosition.y - (fabSize.height / 2)
         assertThat(fabPosition.y).isEqualTo(expectedFabY)
@@ -415,7 +418,8 @@
                 Scaffold(
                     topBar = {
                         Box(
-                            Modifier.requiredSize(10.dp)
+                            Modifier
+                                .requiredSize(10.dp)
                                 .shadow(4.dp)
                                 .zIndex(4f)
                                 .background(color = Color.White)
@@ -423,7 +427,8 @@
                     }
                 ) {
                     Box(
-                        Modifier.requiredSize(10.dp)
+                        Modifier
+                            .requiredSize(10.dp)
                             .background(color = Color.White)
                     )
                 }
@@ -491,9 +496,11 @@
             val animatedFab = @Composable {
                 AnimatedVisibility(visible = showFab.value) {
                     FloatingActionButton(
-                        modifier = Modifier.onGloballyPositioned { positioned ->
-                            actualFabSize = positioned.size
-                        }.testTag(fabTestTag),
+                        modifier = Modifier
+                            .onGloballyPositioned { positioned ->
+                                actualFabSize = positioned.size
+                            }
+                            .testTag(fabTestTag),
                         onClick = {}
                     ) {
                         Icon(Icons.Filled.Favorite, null)
@@ -768,7 +775,7 @@
 
                         layout(constraints.maxWidth, constraints.maxHeight) {
                             onPlaceCount++
-                            Truth.assertWithMessage("Expected onSizeChangedCount to be >= 1")
+                            assertWithMessage("Expected onSizeChangedCount to be >= 1")
                                 .that(onSizeChangedCount).isAtLeast(1)
                             assertThat(size).isNotNull()
                             placeables.forEach { it.place(0, 0) }
@@ -778,7 +785,73 @@
             }
         }
 
-        Truth.assertWithMessage("Expected placeCount to be >= 1").that(onPlaceCount).isAtLeast(1)
+        assertWithMessage("Expected placeCount to be >= 1").that(onPlaceCount).isAtLeast(1)
+    }
+
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun scaffold_subcomposeInMeasureFix_enabled_measuresChildrenInMeasurement() {
+        ScaffoldSubcomposeInMeasureFix = true
+        var size: IntSize? = null
+        var measured = false
+        rule.setContent {
+            Layout(
+                content = {
+                    Scaffold(
+                        content = {
+                            Box(Modifier.onSizeChanged { size = it })
+                        }
+                    )
+                }
+            ) { measurables, constraints ->
+                measurables.map { it.measure(constraints) }
+                measured = true
+                layout(0, 0) {
+                    // Empty measurement since we only care about placement
+                }
+            }
+        }
+
+        assertWithMessage("Measure should have been executed")
+            .that(measured).isTrue()
+        assertWithMessage("Expected size to be initialized")
+            .that(size).isNotNull()
+    }
+
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun scaffold_subcomposeInMeasureFix_disabled_measuresChildrenInPlacement() {
+        ScaffoldSubcomposeInMeasureFix = false
+        var size: IntSize? = null
+        var measured = false
+        var placed = false
+        rule.setContent {
+            Layout(
+                content = {
+                    Scaffold(
+                        content = {
+                            Box(Modifier.onSizeChanged { size = it })
+                        }
+                    )
+                }
+            ) { measurables, constraints ->
+                val placeables = measurables.map { it.measure(constraints) }
+                measured = true
+                assertWithMessage("Expected size to not be initialized in placement")
+                    .that(size).isNull()
+                layout(constraints.maxWidth, constraints.maxHeight) {
+                    placeables.forEach { it.place(0, 0) }
+                    placed = true
+                }
+            }
+        }
+
+        assertWithMessage("Measure should have been executed")
+            .that(measured).isTrue()
+        assertWithMessage("Placement should have been executed")
+            .that(placed).isTrue()
+        assertWithMessage("Expected size to be initialized")
+            .that(size).isNotNull()
     }
 
     private fun assertDpIsWithinThreshold(actual: Dp, expected: Dp, threshold: Dp) {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 106f8ad..41eaced 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -29,7 +29,10 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.UiComposable
@@ -78,7 +81,7 @@
 /**
  * The possible positions for a [FloatingActionButton] attached to a [Scaffold].
  */
[email protected]
+@JvmInline
 value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
     companion object {
         /**
@@ -364,6 +367,22 @@
 }
 
 /**
+ * Flag indicating if [Scaffold] should subcompose and measure its children during measurement or
+ * during placement.
+ * Set this flag to false to keep Scaffold's old measurement behavior (measuring in placement).
+ *
+ * <b>This flag will be removed in Compose 1.6.0-beta01.</b> If you encounter any issues with the
+ * new behavior, please file an issue at: issuetracker.google.com/issues/new?component=742043
+ */
+// TODO(b/299621062): Remove flag before beta
+@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:Suppress("GetterSetterNames")
+@get:ExperimentalMaterialApi
+@set:ExperimentalMaterialApi
+@ExperimentalMaterialApi
+var ScaffoldSubcomposeInMeasureFix by mutableStateOf(true)
+
+/**
  * Layout for a [Scaffold]'s content.
  *
  * @param isFabDocked whether the FAB (if present) is docked to the bottom bar or not
@@ -376,6 +395,7 @@
  * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
  * [content], typically a [BottomAppBar].
  */
+@OptIn(ExperimentalMaterialApi::class)
 @Composable
 @UiComposable
 private fun ScaffoldLayout(
@@ -388,6 +408,46 @@
     contentWindowInsets: WindowInsets,
     bottomBar: @Composable @UiComposable () -> Unit
 ) {
+    if (ScaffoldSubcomposeInMeasureFix) {
+        ScaffoldLayoutWithMeasureFix(
+            isFabDocked = isFabDocked,
+            fabPosition = fabPosition,
+            topBar = topBar,
+            content = content,
+            snackbar = snackbar,
+            fab = fab,
+            contentWindowInsets = contentWindowInsets,
+            bottomBar = bottomBar
+        )
+    } else {
+        LegacyScaffoldLayout(
+            isFabDocked = isFabDocked,
+            fabPosition = fabPosition,
+            topBar = topBar,
+            content = content,
+            snackbar = snackbar,
+            fab = fab,
+            contentWindowInsets = contentWindowInsets,
+            bottomBar = bottomBar
+        )
+    }
+}
+
+/**
+ * Layout for a [Scaffold]'s content, subcomposing and measuring during measurement.
+ */
+@Composable
+@UiComposable
+private fun ScaffoldLayoutWithMeasureFix(
+    isFabDocked: Boolean,
+    fabPosition: FabPosition,
+    topBar: @Composable @UiComposable () -> Unit,
+    content: @Composable @UiComposable (PaddingValues) -> Unit,
+    snackbar: @Composable @UiComposable () -> Unit,
+    fab: @Composable @UiComposable () -> Unit,
+    contentWindowInsets: WindowInsets,
+    bottomBar: @Composable @UiComposable () -> Unit
+) {
     SubcomposeLayout { constraints ->
         val layoutWidth = constraints.maxWidth
         val layoutHeight = constraints.maxHeight
@@ -447,6 +507,7 @@
                             layoutWidth - FabSpacing.roundToPx() - fabWidth
                         }
                     }
+
                     FabPosition.End -> {
                         if (layoutDirection == LayoutDirection.Ltr) {
                             layoutWidth - FabSpacing.roundToPx() - fabWidth
@@ -454,6 +515,7 @@
                             FabSpacing.roundToPx()
                         }
                     }
+
                     else -> (layoutWidth - fabWidth) / 2
                 }
 
@@ -550,6 +612,183 @@
 }
 
 /**
+ * Legacy layout for a [Scaffold]'s content, subcomposing and measuring during placement.
+ */
+@Composable
+@UiComposable
+private fun LegacyScaffoldLayout(
+    isFabDocked: Boolean,
+    fabPosition: FabPosition,
+    topBar: @Composable @UiComposable () -> Unit,
+    content: @Composable @UiComposable (PaddingValues) -> Unit,
+    snackbar: @Composable @UiComposable () -> Unit,
+    fab: @Composable @UiComposable () -> Unit,
+    contentWindowInsets: WindowInsets,
+    bottomBar: @Composable @UiComposable () -> Unit
+) {
+    SubcomposeLayout { constraints ->
+        val layoutWidth = constraints.maxWidth
+        val layoutHeight = constraints.maxHeight
+
+        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+
+        layout(layoutWidth, layoutHeight) {
+            val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
+                it.measure(looseConstraints)
+            }
+
+            val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
+                // respect only bottom and horizontal for snackbar and fab
+                val leftInset = contentWindowInsets
+                    .getLeft(this@SubcomposeLayout, layoutDirection)
+                val rightInset = contentWindowInsets
+                    .getRight(this@SubcomposeLayout, layoutDirection)
+                val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
+                // offset the snackbar constraints by the insets values
+                it.measure(
+                    looseConstraints.offset(
+                        -leftInset - rightInset,
+                        -bottomInset
+                    )
+                )
+            }
+
+            val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            val fabPlaceables =
+                subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
+                    // respect only bottom and horizontal for snackbar and fab
+                    val leftInset =
+                        contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
+                    val rightInset =
+                        contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
+                    val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
+                    measurable.measure(
+                        looseConstraints.offset(
+                            -leftInset - rightInset,
+                            -bottomInset
+                        )
+                    )
+                }
+
+            val fabPlacement = if (fabPlaceables.isNotEmpty()) {
+                val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
+                val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
+                // FAB distance from the left of the layout, taking into account LTR / RTL
+                if (fabWidth != 0 && fabHeight != 0) {
+                    val fabLeftOffset = when (fabPosition) {
+                        FabPosition.Start -> {
+                            if (layoutDirection == LayoutDirection.Ltr) {
+                                FabSpacing.roundToPx()
+                            } else {
+                                layoutWidth - FabSpacing.roundToPx() - fabWidth
+                            }
+                        }
+
+                        FabPosition.End -> {
+                            if (layoutDirection == LayoutDirection.Ltr) {
+                                layoutWidth - FabSpacing.roundToPx() - fabWidth
+                            } else {
+                                FabSpacing.roundToPx()
+                            }
+                        }
+
+                        else -> (layoutWidth - fabWidth) / 2
+                    }
+
+                    FabPlacement(
+                        isDocked = isFabDocked,
+                        left = fabLeftOffset,
+                        width = fabWidth,
+                        height = fabHeight
+                    )
+                } else {
+                    null
+                }
+            } else {
+                null
+            }
+
+            val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
+                CompositionLocalProvider(
+                    LocalFabPlacement provides fabPlacement,
+                    content = bottomBar
+                )
+            }.fastMap { it.measure(looseConstraints) }
+
+            val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
+            val fabOffsetFromBottom = fabPlacement?.let {
+                if (bottomBarHeight == null) {
+                    it.height + FabSpacing.roundToPx() +
+                        contentWindowInsets.getBottom(this@SubcomposeLayout)
+                } else {
+                    if (isFabDocked) {
+                        // Total height is the bottom bar height + half the FAB height
+                        bottomBarHeight + (it.height / 2)
+                    } else {
+                        // Total height is the bottom bar height + the FAB height + the padding
+                        // between the FAB and bottom bar
+                        bottomBarHeight + it.height + FabSpacing.roundToPx()
+                    }
+                }
+            }
+
+            val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
+                snackbarHeight +
+                    (fabOffsetFromBottom ?: bottomBarHeight
+                    ?: contentWindowInsets.getBottom(this@SubcomposeLayout))
+            } else {
+                0
+            }
+
+            val bodyContentHeight = layoutHeight - topBarHeight
+
+            val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
+                val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
+                val innerPadding = PaddingValues(
+                    top =
+                    if (topBarPlaceables.isEmpty()) {
+                        insets.calculateTopPadding()
+                    } else {
+                        0.dp
+                    },
+                    bottom =
+                    if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
+                        insets.calculateBottomPadding()
+                    } else {
+                        bottomBarHeight.toDp()
+                    },
+                    start = insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
+                    end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection)
+                )
+                content(innerPadding)
+            }.fastMap { it.measure(looseConstraints.copy(maxHeight = bodyContentHeight)) }
+
+            // Placing to control drawing order to match default elevation of each placeable
+            bodyContentPlaceables.fastForEach {
+                it.place(0, topBarHeight)
+            }
+            topBarPlaceables.fastForEach {
+                it.place(0, 0)
+            }
+            snackbarPlaceables.fastForEach {
+                it.place(0, layoutHeight - snackbarOffsetFromBottom)
+            }
+            // The bottom bar is always at the bottom of the layout
+            bottomBarPlaceables.fastForEach {
+                it.place(0, layoutHeight - (bottomBarHeight ?: 0))
+            }
+            // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
+            fabPlaceables.fastForEach {
+                it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
+            }
+        }
+    }
+}
+
+/**
  * Placement information for a [FloatingActionButton] inside a [Scaffold].
  *
  * @property isDocked whether the FAB should be docked with the bottom bar
diff --git a/compose/material3/material3-adaptive/api/current.txt b/compose/material3/material3-adaptive/api/current.txt
index 955380f..c21a89f 100644
--- a/compose/material3/material3-adaptive/api/current.txt
+++ b/compose/material3/material3-adaptive/api/current.txt
@@ -96,15 +96,6 @@
     property public abstract androidx.compose.material3.adaptive.ThreePaneScaffoldValue layoutValue;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum NavigationSuiteAlignment {
-    method public static androidx.compose.material3.adaptive.NavigationSuiteAlignment valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
-    method public static androidx.compose.material3.adaptive.NavigationSuiteAlignment[] values();
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment BottomHorizontal;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment EndVertical;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment StartVertical;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment TopHorizontal;
-  }
-
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteColors {
     method public long getNavigationBarContainerColor();
     method public long getNavigationBarContentColor();
@@ -121,14 +112,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteDefaults {
-    method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationBarAlignment();
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationDrawerAlignment();
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationRailAlignment();
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationBarAlignment;
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationDrawerAlignment;
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationRailAlignment;
     field public static final androidx.compose.material3.adaptive.NavigationSuiteDefaults INSTANCE;
   }
 
@@ -141,13 +125,15 @@
     property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
   }
 
-  public final class NavigationSuiteScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuite(androidx.compose.material3.adaptive.NavigationSuiteScaffoldScope, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScaffoldScope,kotlin.Unit> navigationSuite, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteScaffoldDefaults {
+    method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
+    field public static final androidx.compose.material3.adaptive.NavigationSuiteScaffoldDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface NavigationSuiteScaffoldScope {
-    method public androidx.compose.ui.Modifier alignment(androidx.compose.ui.Modifier, androidx.compose.material3.adaptive.NavigationSuiteAlignment alignment);
+  public final class NavigationSuiteScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface NavigationSuiteScope {
diff --git a/compose/material3/material3-adaptive/api/restricted_current.txt b/compose/material3/material3-adaptive/api/restricted_current.txt
index 955380f..c21a89f 100644
--- a/compose/material3/material3-adaptive/api/restricted_current.txt
+++ b/compose/material3/material3-adaptive/api/restricted_current.txt
@@ -96,15 +96,6 @@
     property public abstract androidx.compose.material3.adaptive.ThreePaneScaffoldValue layoutValue;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public enum NavigationSuiteAlignment {
-    method public static androidx.compose.material3.adaptive.NavigationSuiteAlignment valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
-    method public static androidx.compose.material3.adaptive.NavigationSuiteAlignment[] values();
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment BottomHorizontal;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment EndVertical;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment StartVertical;
-    enum_constant public static final androidx.compose.material3.adaptive.NavigationSuiteAlignment TopHorizontal;
-  }
-
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteColors {
     method public long getNavigationBarContainerColor();
     method public long getNavigationBarContentColor();
@@ -121,14 +112,7 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteDefaults {
-    method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.adaptive.NavigationSuiteColors colors(optional long navigationBarContainerColor, optional long navigationBarContentColor, optional long navigationRailContainerColor, optional long navigationRailContentColor, optional long navigationDrawerContainerColor, optional long navigationDrawerContentColor);
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationBarAlignment();
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationDrawerAlignment();
-    method public androidx.compose.material3.adaptive.NavigationSuiteAlignment getNavigationRailAlignment();
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationBarAlignment;
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationDrawerAlignment;
-    property public final androidx.compose.material3.adaptive.NavigationSuiteAlignment NavigationRailAlignment;
     field public static final androidx.compose.material3.adaptive.NavigationSuiteDefaults INSTANCE;
   }
 
@@ -141,13 +125,15 @@
     property public final androidx.compose.material3.NavigationRailItemColors navigationRailItemColors;
   }
 
-  public final class NavigationSuiteScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuite(androidx.compose.material3.adaptive.NavigationSuiteScaffoldScope, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScaffoldScope,kotlin.Unit> navigationSuite, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class NavigationSuiteScaffoldDefaults {
+    method public String calculateFromAdaptiveInfo(androidx.compose.material3.adaptive.WindowAdaptiveInfo adaptiveInfo);
+    field public static final androidx.compose.material3.adaptive.NavigationSuiteScaffoldDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface NavigationSuiteScaffoldScope {
-    method public androidx.compose.ui.Modifier alignment(androidx.compose.ui.Modifier, androidx.compose.material3.adaptive.NavigationSuiteAlignment alignment);
+  public final class NavigationSuiteScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuite(optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional androidx.compose.material3.adaptive.NavigationSuiteColors colors, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.NavigationSuiteScope,kotlin.Unit> navigationSuiteItems, optional androidx.compose.ui.Modifier modifier, optional String layoutType, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void NavigationSuiteScaffoldLayout(kotlin.jvm.functions.Function0<kotlin.Unit> navigationSuite, optional String layoutType, optional kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public interface NavigationSuiteScope {
diff --git a/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt b/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt
index 51ee44b..01ccec2 100644
--- a/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt
+++ b/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/NavigationSuiteScaffoldSamples.kt
@@ -23,10 +23,8 @@
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Text
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
-import androidx.compose.material3.adaptive.NavigationSuite
-import androidx.compose.material3.adaptive.NavigationSuiteAlignment
-import androidx.compose.material3.adaptive.NavigationSuiteDefaults
 import androidx.compose.material3.adaptive.NavigationSuiteScaffold
+import androidx.compose.material3.adaptive.NavigationSuiteScaffoldDefaults
 import androidx.compose.material3.adaptive.NavigationSuiteType
 import androidx.compose.material3.adaptive.calculateWindowAdaptiveInfo
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@@ -47,19 +45,17 @@
     var selectedItem by remember { mutableIntStateOf(0) }
     val navItems = listOf("Songs", "Artists", "Playlists")
     val navSuiteType =
-        NavigationSuiteDefaults.calculateFromAdaptiveInfo(calculateWindowAdaptiveInfo())
+        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(calculateWindowAdaptiveInfo())
 
     NavigationSuiteScaffold(
-        navigationSuite = {
-            NavigationSuite {
-                navItems.forEachIndexed { index, navItem ->
-                    item(
-                        icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
-                        label = { Text(navItem) },
-                        selected = selectedItem == index,
-                        onClick = { selectedItem = index }
-                    )
-                }
+        navigationSuiteItems = {
+            navItems.forEachIndexed { index, navItem ->
+                item(
+                    icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
+                    label = { Text(navItem) },
+                    selected = selectedItem == index,
+                    onClick = { selectedItem = index }
+                )
             }
         }
     ) {
@@ -79,36 +75,25 @@
     var selectedItem by remember { mutableIntStateOf(0) }
     val navItems = listOf("Songs", "Artists", "Playlists")
     val adaptiveInfo = calculateWindowAdaptiveInfo()
+    // Custom configuration that shows a navigation drawer in large screens.
     val customNavSuiteType = with(adaptiveInfo) {
         if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {
             NavigationSuiteType.NavigationDrawer
-        } else if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
-            NavigationSuiteType.NavigationRail
         } else {
-            NavigationSuiteDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
+            NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
         }
     }
 
-    // Custom configuration that shows nav rail on end of screen in small screens, and navigation
-    // drawer in large screens.
     NavigationSuiteScaffold(
-        navigationSuite = {
-            NavigationSuite(
-                layoutType = customNavSuiteType,
-                modifier = if (customNavSuiteType == NavigationSuiteType.NavigationRail) {
-                    Modifier.alignment(NavigationSuiteAlignment.EndVertical)
-                } else {
-                    Modifier
-                }
-            ) {
-                navItems.forEachIndexed { index, navItem ->
-                    item(
-                        icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
-                        label = { Text(navItem) },
-                        selected = selectedItem == index,
-                        onClick = { selectedItem = index }
-                    )
-                }
+        layoutType = customNavSuiteType,
+        navigationSuiteItems = {
+            navItems.forEachIndexed { index, navItem ->
+                item(
+                    icon = { Icon(Icons.Filled.Favorite, contentDescription = navItem) },
+                    label = { Text(navItem) },
+                    selected = selectedItem == index,
+                    onClick = { selectedItem = index }
+                )
             }
         }
     ) {
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
index 0629248..0d60c95 100644
--- a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffold.kt
@@ -53,14 +53,7 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.ParentDataModifierNode
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastFilterNotNull
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.util.fastMaxOfOrNull
@@ -74,8 +67,10 @@
  * Example custom configuration usage:
  * @sample androidx.compose.material3.adaptive.samples.NavigationSuiteScaffoldCustomConfigSample
  *
- * @param navigationSuite the navigation component to be displayed, typically [NavigationSuite]
+ * @param navigationSuiteItems the navigation items to be displayed
  * @param modifier the [Modifier] to be applied to the navigation suite scaffold
+ * @param layoutType the current [NavigationSuiteType]. Defaults to
+ * [NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo]
  * @param containerColor the color used for the background of the navigation suite scaffold. Use
  * [Color.Transparent] to have no color
  * @param contentColor the preferred color for content inside the navigation suite scaffold.
@@ -86,112 +81,87 @@
 @ExperimentalMaterial3AdaptiveApi
 @Composable
 fun NavigationSuiteScaffold(
-    navigationSuite: @Composable NavigationSuiteScaffoldScope.() -> Unit,
+    navigationSuiteItems: NavigationSuiteScope.() -> Unit,
     modifier: Modifier = Modifier,
+    layoutType: NavigationSuiteType =
+        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
     containerColor: Color = MaterialTheme.colorScheme.background,
     contentColor: Color = contentColorFor(containerColor),
     content: @Composable () -> Unit = {},
 ) {
     Surface(modifier = modifier, color = containerColor, contentColor = contentColor) {
         NavigationSuiteScaffoldLayout(
-            navigationSuite = navigationSuite,
+            navigationSuite = {
+                NavigationSuite(layoutType = layoutType, content = navigationSuiteItems)
+            },
+            layoutType = layoutType,
             content = content
         )
     }
 }
 
 /**
- * Layout for a [NavigationSuiteScaffold]'s content.
+ * Layout for a [NavigationSuiteScaffold]'s content. This function wraps the [content] and places
+ * the [navigationSuite] component according to the given [layoutType].
  *
- * @param navigationSuite the navigation suite of the [NavigationSuiteScaffold]
- * @param content the main body of the [NavigationSuiteScaffold]
- * @throws [IllegalArgumentException] if there is more than one [NavigationSuiteAlignment] for the
- * given navigation component
+ * @param navigationSuite the navigation component to be displayed, typically [NavigationSuite]
+ * @param layoutType the current [NavigationSuiteType]. Defaults to
+ * [NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo]
+ * @param content the content of your screen
  */
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@ExperimentalMaterial3AdaptiveApi
 @Composable
-private fun NavigationSuiteScaffoldLayout(
-    navigationSuite: @Composable NavigationSuiteScaffoldScope.() -> Unit,
+fun NavigationSuiteScaffoldLayout(
+    navigationSuite: @Composable () -> Unit,
+    layoutType: NavigationSuiteType =
+        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
     content: @Composable () -> Unit = {}
 ) {
     Layout(
-        contents = listOf({ NavigationSuiteScaffoldScopeImpl.navigationSuite() }, content)
+        contents = listOf(navigationSuite, content)
     ) { (navigationMeasurables, contentMeasurables), constraints ->
         val navigationPlaceables = navigationMeasurables.fastMap { it.measure(constraints) }
-        val alignments = navigationPlaceables.fastMap {
-            (it.parentData as NavigationSuiteParentData).alignment
-        }.fastFilterNotNull()
-        if (alignments.fastAll { alignments[0] != it }) {
-            throw IllegalArgumentException("There should be only one NavigationSuiteAlignment.")
-        }
-        val alignment = alignments.firstOrNull() ?: NavigationSuiteAlignment.StartVertical
+        val isNavigationBar = layoutType == NavigationSuiteType.NavigationBar
         val layoutHeight = constraints.maxHeight
         val layoutWidth = constraints.maxWidth
         val contentPlaceables = contentMeasurables.fastMap { it.measure(
-            if (alignment == NavigationSuiteAlignment.TopHorizontal ||
-                alignment == NavigationSuiteAlignment.BottomHorizontal
-            ) {
+            if (isNavigationBar) {
                 constraints.copy(
-                    minHeight = layoutHeight - navigationPlaceables.fastMaxOfOrNull { it.height }!!,
-                    maxHeight = layoutHeight - navigationPlaceables.fastMaxOfOrNull { it.height }!!
+                    minHeight = layoutHeight - (navigationPlaceables.fastMaxOfOrNull { it.height }
+                        ?: 0),
+                    maxHeight = layoutHeight - (navigationPlaceables.fastMaxOfOrNull { it.height }
+                        ?: 0)
                 )
             } else {
                 constraints.copy(
-                    minWidth = layoutWidth - navigationPlaceables.fastMaxOfOrNull { it.width }!!,
-                    maxWidth = layoutWidth - navigationPlaceables.fastMaxOfOrNull { it.width }!!
+                    minWidth = layoutWidth - (navigationPlaceables.fastMaxOfOrNull { it.width }
+                        ?: 0),
+                    maxWidth = layoutWidth - (navigationPlaceables.fastMaxOfOrNull { it.width }
+                        ?: 0)
                 )
             }
         ) }
 
         layout(layoutWidth, layoutHeight) {
-            when (alignment) {
-                NavigationSuiteAlignment.StartVertical -> {
-                    // Place the navigation component at the start of the screen.
-                    navigationPlaceables.fastForEach {
-                        it.placeRelative(0, 0)
-                    }
-                    // Place content to the side of the navigation component.
-                    contentPlaceables.fastForEach {
-                        it.placeRelative(navigationPlaceables.fastMaxOfOrNull { it.width }!!, 0)
-                    }
+            if (isNavigationBar) {
+                // Place content above the navigation component.
+                contentPlaceables.fastForEach {
+                    it.placeRelative(0, 0)
                 }
-
-                NavigationSuiteAlignment.EndVertical -> {
-                    // Place the navigation component at the end of the screen.
-                    navigationPlaceables.fastForEach {
-                        it.placeRelative(
-                            layoutWidth - navigationPlaceables.fastMaxOfOrNull { it.width }!!,
-                            0
-                        )
-                    }
-                    // Place content at the start of the screen.
-                    contentPlaceables.fastForEach {
-                        it.placeRelative(0, 0)
-                    }
+                // Place the navigation component at the bottom of the screen.
+                navigationPlaceables.fastForEach {
+                    it.placeRelative(
+                        0,
+                        layoutHeight - (navigationPlaceables.fastMaxOfOrNull { it.height } ?: 0))
                 }
-
-                NavigationSuiteAlignment.TopHorizontal -> {
-                    // Place the navigation component at the start of the screen.
-                    navigationPlaceables.fastForEach {
-                        it.placeRelative(0, 0)
-                    }
-                    // Place content below the navigation component.
-                    contentPlaceables.fastForEach {
-                        it.placeRelative(0, navigationPlaceables.fastMaxOfOrNull { it.height }!!)
-                    }
+            } else {
+                // Place the navigation component at the start of the screen.
+                navigationPlaceables.fastForEach {
+                    it.placeRelative(0, 0)
                 }
-
-                NavigationSuiteAlignment.BottomHorizontal -> {
-                    // Place content above the navigation component.
-                    contentPlaceables.fastForEach {
-                        it.placeRelative(0, 0)
-                    }
-                    // Place the navigation component at the bottom of the screen.
-                    navigationPlaceables.fastForEach {
-                        it.placeRelative(
-                            0,
-                            layoutHeight - navigationPlaceables.fastMaxOfOrNull { it.height }!!)
-                    }
+                // Place content to the side of the navigation component.
+                contentPlaceables.fastForEach {
+                    it.placeRelative((navigationPlaceables.fastMaxOfOrNull { it.width } ?: 0), 0)
                 }
             }
         }
@@ -207,7 +177,7 @@
  *
  * @param modifier the [Modifier] to be applied to the navigation component
  * @param layoutType the current [NavigationSuiteType] of the [NavigationSuiteScaffold]. Defaults to
- * [NavigationSuiteDefaults.calculateFromAdaptiveInfo]
+ * [NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo]
  * @param colors [NavigationSuiteColors] that will be used to determine the container (background)
  * color of the navigation component and the preferred color for content inside the navigation
  * component
@@ -216,10 +186,10 @@
  */
 @ExperimentalMaterial3AdaptiveApi
 @Composable
-fun NavigationSuiteScaffoldScope.NavigationSuite(
+fun NavigationSuite(
     modifier: Modifier = Modifier,
     layoutType: NavigationSuiteType =
-        NavigationSuiteDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
+        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
     colors: NavigationSuiteColors = NavigationSuiteDefaults.colors(),
     content: NavigationSuiteScope.() -> Unit
 ) {
@@ -228,7 +198,7 @@
     when (layoutType) {
         NavigationSuiteType.NavigationBar -> {
             NavigationBar(
-                modifier = modifier.alignment(NavigationSuiteDefaults.NavigationBarAlignment),
+                modifier = modifier,
                 containerColor = colors.navigationBarContainerColor,
                 contentColor = colors.navigationBarContentColor
             ) {
@@ -251,7 +221,7 @@
 
         NavigationSuiteType.NavigationRail -> {
             NavigationRail(
-                modifier = modifier.alignment(NavigationSuiteDefaults.NavigationRailAlignment),
+                modifier = modifier,
                 containerColor = colors.navigationRailContainerColor,
                 contentColor = colors.navigationRailContentColor
             ) {
@@ -274,7 +244,7 @@
 
         NavigationSuiteType.NavigationDrawer -> {
             PermanentDrawerSheet(
-                modifier = modifier.alignment(NavigationSuiteDefaults.NavigationDrawerAlignment),
+                modifier = modifier,
                 drawerContainerColor = colors.navigationDrawerContainerColor,
                 drawerContentColor = colors.navigationDrawerContentColor
             ) {
@@ -296,37 +266,7 @@
     }
 }
 
-/** The scope associated with the [NavigationSuiteScaffold]. */
-@ExperimentalMaterial3AdaptiveApi
-interface NavigationSuiteScaffoldScope {
-    /**
-     * [Modifier] that should be applied to the [NavigationSuite] of the [NavigationSuiteScaffold]
-     * in order to determine its alignment on the screen.
-     *
-     * @param alignment the desired [NavigationSuiteAlignment]
-     */
-    fun Modifier.alignment(alignment: NavigationSuiteAlignment): Modifier
-}
-
-/**
- * Represents the alignment of the navigation component of the [NavigationSuiteScaffold].
- *
- * The alignment informs the Navigation Suite Scaffold how to properly place the expected navigation
- * component on the screen in relation to the Navigation Suite Scaffold's content.
- */
-@ExperimentalMaterial3AdaptiveApi
-enum class NavigationSuiteAlignment {
-    /** The navigation component is vertical and positioned at the start of the screen. */
-    StartVertical,
-    /** The navigation component is vertical and positioned at the end of the screen. */
-    EndVertical,
-    /** The navigation component is horizontal and positioned at the top of the screen. */
-    TopHorizontal,
-    /** The navigation component is horizontal and positioned at the bottom of the screen. */
-    BottomHorizontal
-}
-
-/** The scope associated with the [NavigationSuite]. */
+/** The scope associated with the [NavigationSuiteScope]. */
 @ExperimentalMaterial3AdaptiveApi
 interface NavigationSuiteScope {
 
@@ -383,15 +323,16 @@
 
     companion object {
         /**
-         * A navigation suite type that instructs the [NavigationSuite] to expect a [NavigationBar].
+         * A navigation suite type that instructs the [NavigationSuite] to expect a [NavigationBar]
+         * that will be displayed at the bottom of the screen.
          *
          * @see NavigationBar
          */
         val NavigationBar = NavigationSuiteType(description = "NavigationBar")
 
         /**
-         * A navigation suite type that instructs the [NavigationSuite] to expect a
-         * [NavigationRail].
+         * A navigation suite type that instructs the [NavigationSuite] to expect a [NavigationRail]
+         * that will be displayed at the start of the screen.
          *
          * @see NavigationRail
          */
@@ -399,7 +340,7 @@
 
         /**
          * A navigation suite type that instructs the [NavigationSuite] to expect a
-         * [PermanentDrawerSheet].
+         * [PermanentDrawerSheet] that will be displayed at the start of the screen.
          *
          * @see PermanentDrawerSheet
          */
@@ -407,15 +348,15 @@
     }
 }
 
-/** Contains the default values used by the [NavigationSuite]. */
+/** Contains the default values used by the [NavigationSuiteScaffold]. */
 @ExperimentalMaterial3AdaptiveApi
-object NavigationSuiteDefaults {
+object NavigationSuiteScaffoldDefaults {
     /**
      * Returns the expected [NavigationSuiteType] according to the provided [WindowAdaptiveInfo].
-     * Usually used with the [NavigationSuite].
+     * Usually used with the [NavigationSuiteScaffold] and related APIs.
      *
      * @param adaptiveInfo the provided [WindowAdaptiveInfo]
-     * @see NavigationSuite
+     * @see NavigationSuiteScaffold
      */
     fun calculateFromAdaptiveInfo(adaptiveInfo: WindowAdaptiveInfo): NavigationSuiteType {
         return with(adaptiveInfo) {
@@ -428,16 +369,11 @@
             }
         }
     }
+}
 
-    /** Default alignment for the [NavigationSuiteType.NavigationBar]. */
-    val NavigationBarAlignment = NavigationSuiteAlignment.BottomHorizontal
-
-    /** Default alignment for the [NavigationSuiteType.NavigationRail]. */
-    val NavigationRailAlignment = NavigationSuiteAlignment.StartVertical
-
-    /** Default alignment for the [NavigationSuiteType.NavigationDrawer]. */
-    val NavigationDrawerAlignment = NavigationSuiteAlignment.StartVertical
-
+/** Contains the default values used by the [NavigationSuite]. */
+@ExperimentalMaterial3AdaptiveApi
+object NavigationSuiteDefaults {
     /**
      * Creates a [NavigationSuiteColors] with the provided colors for the container color, according
      * to the Material specification.
@@ -526,64 +462,6 @@
 )
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal object NavigationSuiteScaffoldScopeImpl : NavigationSuiteScaffoldScope {
-    override fun Modifier.alignment(alignment: NavigationSuiteAlignment): Modifier {
-        return this.then(
-            AlignmentElement(alignment = alignment)
-        )
-    }
-}
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal class AlignmentElement(
-    val alignment: NavigationSuiteAlignment
-) : ModifierNodeElement<AlignmentNode>() {
-    override fun create(): AlignmentNode {
-        return AlignmentNode(alignment)
-    }
-
-    override fun update(node: AlignmentNode) {
-        node.alignment = alignment
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "alignment"
-        value = alignment
-    }
-
-    override fun hashCode(): Int = alignment.hashCode()
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        val otherModifier = other as? AlignmentElement ?: return false
-        return alignment == otherModifier.alignment
-    }
-}
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal class AlignmentNode(
-    var alignment: NavigationSuiteAlignment
-) : ParentDataModifierNode, Modifier.Node() {
-    override fun Density.modifyParentData(parentData: Any?) =
-        ((parentData as? NavigationSuiteParentData) ?: NavigationSuiteParentData()).also {
-            it.alignment = alignment
-        }
-}
-
-/** Parent data associated with children. */
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal data class NavigationSuiteParentData(
-    var alignment: NavigationSuiteAlignment? = null
-)
-
-internal val IntrinsicMeasurable.navigationSuiteParentData: NavigationSuiteParentData?
-    get() = parentData as? NavigationSuiteParentData
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-internal val NavigationSuiteParentData?.alignment: NavigationSuiteAlignment
-    get() = this?.alignment ?: NavigationSuiteAlignment.StartVertical
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
 internal expect val WindowAdaptiveInfoDefault: WindowAdaptiveInfo
     @Composable
     get
@@ -594,7 +472,7 @@
 }
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
-private class NavigationSuiteItem constructor(
+private class NavigationSuiteItem(
     val selected: Boolean,
     val onClick: () -> Unit,
     val icon: @Composable () -> Unit,
diff --git a/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt b/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt
index 3ab8ee3..359f940 100644
--- a/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt
+++ b/compose/material3/material3-adaptive/src/test/kotlin/androidx/compose/material3/adaptive/NavigationSuiteScaffoldTest.kt
@@ -36,7 +36,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 400.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
@@ -47,7 +47,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 800.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
@@ -58,7 +58,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 1000.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
@@ -69,7 +69,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 400.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
@@ -80,7 +80,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 800.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
@@ -91,7 +91,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(800.dp, 1000.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
@@ -102,7 +102,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 400.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
@@ -113,7 +113,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 800.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationRail)
     }
 
@@ -124,7 +124,7 @@
                 windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(1000.dp, 1000.dp))
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationRail)
     }
 
@@ -136,7 +136,7 @@
                 isTableTop = true
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
@@ -148,7 +148,7 @@
                 isTableTop = true
             )
 
-        assertThat(NavigationSuiteDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
+        assertThat(NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(mockAdaptiveInfo))
             .isEqualTo(NavigationSuiteType.NavigationBar)
     }
 
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index d665b1b..82b469d 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -29,13 +29,17 @@
   }
 
   public final class AppBarKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SmallTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
   }
 
@@ -66,6 +70,7 @@
   }
 
   public final class BottomAppBarDefaults {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.BottomAppBarScrollBehavior exitAlwaysScrollBehavior(optional androidx.compose.material3.BottomAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
     method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
@@ -79,6 +84,39 @@
     field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
   }
 
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface BottomAppBarScrollBehavior {
+    method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? getFlingAnimationSpec();
+    method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection getNestedScrollConnection();
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float>? getSnapAnimationSpec();
+    method public androidx.compose.material3.BottomAppBarState getState();
+    method public boolean isPinned();
+    property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec;
+    property public abstract boolean isPinned;
+    property public abstract androidx.compose.ui.input.nestedscroll.NestedScrollConnection nestedScrollConnection;
+    property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec;
+    property public abstract androidx.compose.material3.BottomAppBarState state;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public interface BottomAppBarState {
+    method public float getCollapsedFraction();
+    method public float getContentOffset();
+    method public float getHeightOffset();
+    method public float getHeightOffsetLimit();
+    method public void setContentOffset(float);
+    method public void setHeightOffset(float);
+    method public void setHeightOffsetLimit(float);
+    property public abstract float collapsedFraction;
+    property public abstract float contentOffset;
+    property public abstract float heightOffset;
+    property public abstract float heightOffsetLimit;
+    field public static final androidx.compose.material3.BottomAppBarState.Companion Companion;
+  }
+
+  public static final class BottomAppBarState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> Saver;
+  }
+
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
     method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
     method @androidx.compose.runtime.Composable public long getContainerColor();
@@ -677,9 +715,11 @@
   public static final class FabPosition.Companion {
     method public int getCenter();
     method public int getEnd();
+    method public int getEndOverlay();
     method public int getStart();
     property public final int Center;
     property public final int End;
+    property public final int EndOverlay;
     property public final int Start;
   }
 
@@ -829,6 +869,10 @@
     property @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
   }
 
+  public final class LabelKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Label(kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean isPersistent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
   @androidx.compose.runtime.Immutable public final class ListItemColors {
     ctor public ListItemColors(long containerColor, long headlineColor, long leadingIconColor, long overlineColor, long supportingTextColor, long trailingIconColor, long disabledHeadlineColor, long disabledLeadingIconColor, long disabledTrailingIconColor);
     method public long getContainerColor();
@@ -1130,6 +1174,9 @@
 
   public final class ScaffoldKt {
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static boolean getScaffoldSubcomposeInMeasureFix();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static void setScaffoldSubcomposeInMeasureFix(boolean);
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final boolean ScaffoldSubcomposeInMeasureFix;
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index d665b1b..82b469d 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -29,13 +29,17 @@
   }
 
   public final class AppBarKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SmallTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
   }
 
@@ -66,6 +70,7 @@
   }
 
   public final class BottomAppBarDefaults {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.BottomAppBarScrollBehavior exitAlwaysScrollBehavior(optional androidx.compose.material3.BottomAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
     method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getContainerElevation();
@@ -79,6 +84,39 @@
     field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
   }
 
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface BottomAppBarScrollBehavior {
+    method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? getFlingAnimationSpec();
+    method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection getNestedScrollConnection();
+    method public androidx.compose.animation.core.AnimationSpec<java.lang.Float>? getSnapAnimationSpec();
+    method public androidx.compose.material3.BottomAppBarState getState();
+    method public boolean isPinned();
+    property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec;
+    property public abstract boolean isPinned;
+    property public abstract androidx.compose.ui.input.nestedscroll.NestedScrollConnection nestedScrollConnection;
+    property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec;
+    property public abstract androidx.compose.material3.BottomAppBarState state;
+  }
+
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public interface BottomAppBarState {
+    method public float getCollapsedFraction();
+    method public float getContentOffset();
+    method public float getHeightOffset();
+    method public float getHeightOffsetLimit();
+    method public void setContentOffset(float);
+    method public void setHeightOffset(float);
+    method public void setHeightOffsetLimit(float);
+    property public abstract float collapsedFraction;
+    property public abstract float contentOffset;
+    property public abstract float heightOffset;
+    property public abstract float heightOffsetLimit;
+    field public static final androidx.compose.material3.BottomAppBarState.Companion Companion;
+  }
+
+  public static final class BottomAppBarState.Companion {
+    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> getSaver();
+    property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> Saver;
+  }
+
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
     method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
     method @androidx.compose.runtime.Composable public long getContainerColor();
@@ -677,9 +715,11 @@
   public static final class FabPosition.Companion {
     method public int getCenter();
     method public int getEnd();
+    method public int getEndOverlay();
     method public int getStart();
     property public final int Center;
     property public final int End;
+    property public final int EndOverlay;
     property public final int Start;
   }
 
@@ -829,6 +869,10 @@
     property @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
   }
 
+  public final class LabelKt {
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void Label(kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional boolean isPersistent, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
   @androidx.compose.runtime.Immutable public final class ListItemColors {
     ctor public ListItemColors(long containerColor, long headlineColor, long leadingIconColor, long overlineColor, long supportingTextColor, long trailingIconColor, long disabledHeadlineColor, long disabledLeadingIconColor, long disabledTrailingIconColor);
     method public long getContainerColor();
@@ -1130,6 +1174,9 @@
 
   public final class ScaffoldKt {
     method @androidx.compose.runtime.Composable public static void Scaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> topBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar, optional kotlin.jvm.functions.Function0<kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional int floatingActionButtonPosition, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets contentWindowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static boolean getScaffoldSubcomposeInMeasureFix();
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static void setScaffoldSubcomposeInMeasureFix(boolean);
+    property @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static final boolean ScaffoldSubcomposeInMeasureFix;
   }
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Immutable public final class SearchBarColors {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index c6d720e..2e8c6a2 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -57,6 +57,7 @@
 import androidx.compose.material3.samples.ElevatedFilterChipSample
 import androidx.compose.material3.samples.ElevatedSuggestionChipSample
 import androidx.compose.material3.samples.EnterAlwaysTopAppBar
+import androidx.compose.material3.samples.ExitAlwaysBottomAppBar
 import androidx.compose.material3.samples.ExitUntilCollapsedLargeTopAppBar
 import androidx.compose.material3.samples.ExitUntilCollapsedMediumTopAppBar
 import androidx.compose.material3.samples.ExposedDropdownMenuSample
@@ -465,7 +466,12 @@
         name = ::BottomAppBarWithFAB.name,
         description = BottomAppBarsExampleDescription,
         sourceUrl = BottomAppBarsExampleSourceUrl,
-    ) { BottomAppBarWithFAB() }
+    ) { BottomAppBarWithFAB() },
+    Example(
+        name = ::ExitAlwaysBottomAppBar.name,
+        description = BottomAppBarsExampleDescription,
+        sourceUrl = BottomAppBarsExampleSourceUrl,
+    ) { ExitAlwaysBottomAppBar() }
 )
 
 private const val TopAppBarExampleDescription = "Top app bar examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
index 6cbc0aa..a6538f0 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.icons.Icons
@@ -31,6 +32,7 @@
 import androidx.compose.material3.BottomAppBarDefaults
 import androidx.compose.material3.CenterAlignedTopAppBar
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FabPosition
 import androidx.compose.material3.FloatingActionButton
 import androidx.compose.material3.FloatingActionButtonDefaults
 import androidx.compose.material3.Icon
@@ -418,11 +420,13 @@
 @Sampled
 @Composable
 fun SimpleBottomAppBar() {
-    BottomAppBar {
-        IconButton(onClick = { /* doSomething() */ }) {
-            Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+    BottomAppBar(
+        actions = {
+            IconButton(onClick = { /* doSomething() */ }) {
+                Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+            }
         }
-    }
+    )
 }
 
 @Preview
@@ -452,3 +456,59 @@
         }
     )
 }
+
+/**
+ * A sample for a [BottomAppBar] that collapses when the content is scrolled up, and
+ * appears when the content scrolled down.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun ExitAlwaysBottomAppBar() {
+    val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+    Scaffold(
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        bottomBar = {
+            BottomAppBar(
+                actions = {
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Check, contentDescription = "Localized description")
+                    }
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+                    }
+                },
+                scrollBehavior = scrollBehavior
+            )
+        },
+        floatingActionButton = {
+            FloatingActionButton(
+                modifier = Modifier.offset(y = 4.dp),
+                onClick = { /* do something */ },
+                containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+            ) {
+                Icon(Icons.Filled.Add, "Localized description")
+            }
+        },
+        floatingActionButtonPosition = FabPosition.EndOverlay,
+        content = { innerPadding ->
+            LazyColumn(
+                contentPadding = innerPadding,
+                verticalArrangement = Arrangement.spacedBy(8.dp)
+            ) {
+                val list = (0..75).map { it.toString() }
+                items(count = list.size) {
+                    Text(
+                        text = list[it],
+                        style = MaterialTheme.typography.bodyLarge,
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(horizontal = 16.dp)
+                    )
+                }
+            }
+        }
+    )
+}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
index 77929ef..2528da4 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
@@ -19,12 +19,16 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
+import androidx.compose.material3.Label
+import androidx.compose.material3.PlainTooltip
 import androidx.compose.material3.RangeSlider
 import androidx.compose.material3.RangeSliderState
 import androidx.compose.material3.Slider
@@ -41,6 +45,8 @@
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
 
 @Preview
 @Sampled
@@ -56,7 +62,6 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Preview
 @Sampled
 @Composable
@@ -84,25 +89,40 @@
 @Composable
 fun SliderWithCustomThumbSample() {
     var sliderPosition by remember { mutableStateOf(0f) }
+    val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
     Column {
-        Text(text = sliderPosition.toString())
         Slider(
             modifier = Modifier.semantics { contentDescription = "Localized Description" },
             value = sliderPosition,
             onValueChange = { sliderPosition = it },
-            valueRange = 0f..5f,
-            steps = 10,
+            valueRange = 0f..100f,
+            interactionSource = interactionSource,
             onValueChangeFinished = {
                 // launch some business logic update with the state you hold
                 // viewModel.updateSelectedSliderValue(sliderPosition)
             },
             thumb = {
-                Icon(
-                    imageVector = Icons.Filled.Favorite,
-                    contentDescription = null,
-                    modifier = Modifier.size(ButtonDefaults.IconSize),
-                    tint = Color.Red
-                )
+                Label(
+                    label = {
+                        PlainTooltip(
+                            modifier = Modifier
+                                .requiredSize(45.dp, 25.dp)
+                                .wrapContentWidth()
+                        ) {
+                            val roundedEnd =
+                                (sliderPosition * 100.0).roundToInt() / 100.0
+                            Text(roundedEnd.toString())
+                        }
+                    },
+                    interactionSource = interactionSource
+                ) {
+                    Icon(
+                        imageVector = Icons.Filled.Favorite,
+                        contentDescription = null,
+                        modifier = Modifier.size(ButtonDefaults.IconSize),
+                        tint = Color.Red
+                    )
+                }
             }
         )
     }
@@ -221,23 +241,52 @@
     )
     val endThumbColors = SliderDefaults.colors(thumbColor = Color.Green)
     Column {
-        Text(text = (rangeSliderState.activeRangeStart..rangeSliderState.activeRangeEnd).toString())
         RangeSlider(
             state = rangeSliderState,
             modifier = Modifier.semantics { contentDescription = "Localized Description" },
             startInteractionSource = startInteractionSource,
             endInteractionSource = endInteractionSource,
             startThumb = {
-                SliderDefaults.Thumb(
-                    interactionSource = startInteractionSource,
-                    colors = startThumbAndTrackColors
-                )
+                Label(
+                    label = {
+                        PlainTooltip(
+                            modifier = Modifier
+                                .requiredSize(45.dp, 25.dp)
+                                .wrapContentWidth()
+                        ) {
+                            val roundedStart =
+                                (rangeSliderState.activeRangeStart * 100.0).roundToInt() / 100.0
+                            Text(roundedStart.toString())
+                        }
+                    },
+                    interactionSource = startInteractionSource
+                ) {
+                    SliderDefaults.Thumb(
+                        interactionSource = startInteractionSource,
+                        colors = startThumbAndTrackColors
+                    )
+                }
             },
             endThumb = {
-                SliderDefaults.Thumb(
-                    interactionSource = endInteractionSource,
-                    colors = endThumbColors
-                )
+                Label(
+                    label = {
+                        PlainTooltip(
+                            modifier = Modifier
+                                .requiredSize(45.dp, 25.dp)
+                                .wrapContentWidth()
+                        ) {
+                            val roundedEnd =
+                                (rangeSliderState.activeRangeEnd * 100.0).roundToInt() / 100.0
+                            Text(roundedEnd.toString())
+                        }
+                    },
+                    interactionSource = endInteractionSource
+                ) {
+                    SliderDefaults.Thumb(
+                        interactionSource = endInteractionSource,
+                        colors = endThumbColors
+                    )
+                }
             },
             track = { rangeSliderState ->
                 SliderDefaults.Track(
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index ddbb438..7b8d1a5 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -24,6 +24,7 @@
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.BottomAppBarDefaults.bottomAppBarFabColor
 import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior
 import androidx.compose.material3.tokens.TopAppBarSmallTokens
 import androidx.compose.testutils.assertAgainstGolden
@@ -361,7 +362,7 @@
                     floatingActionButton = {
                         FloatingActionButton(
                             onClick = { /* do something */ },
-                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                            containerColor = bottomAppBarFabColor,
                             elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
                         ) {
                             Icon(Icons.Filled.Add, "Localized description")
@@ -393,7 +394,7 @@
                     floatingActionButton = {
                         FloatingActionButton(
                             onClick = { /* do something */ },
-                            containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+                            containerColor = bottomAppBarFabColor,
                             elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
                         ) {
                             Icon(Icons.Filled.Add, "Localized description")
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
index ef6ffe4..a913ceb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyListState
@@ -1183,6 +1184,105 @@
             .assertTopPositionInRootIsEqualTo(12.dp)
     }
 
+    @Test
+    fun bottomAppBar_exitAlways_scaffoldWithFAB_default_positioning() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+            Scaffold(
+                modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+                bottomBar = {
+                    BottomAppBar(
+                        modifier = Modifier.testTag(BottomAppBarTestTag),
+                        scrollBehavior = scrollBehavior
+                    ) {}
+                },
+                floatingActionButton = {
+                    FloatingActionButton(
+                        modifier = Modifier
+                            .testTag("FAB")
+                            .offset(y = 4.dp),
+                        onClick = { /* do something */ },
+                    ) {}
+                },
+                floatingActionButtonPosition = FabPosition.EndOverlay
+            ) {}
+        }
+
+        val appBarBounds = rule.onNodeWithTag(BottomAppBarTestTag).getUnclippedBoundsInRoot()
+        val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+        rule.onNodeWithTag("FAB")
+            // FAB should be 16.dp from the end
+            .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+            // FAB should be 12.dp from the bottom
+            .assertTopPositionInRootIsEqualTo(rule.rootHeight() - 12.dp - fabBounds.height)
+    }
+
+    @Test
+    fun bottomAppBar_exitAlways_scaffoldWithFAB_scrolled_positioning() {
+        lateinit var scrollBehavior: BottomAppBarScrollBehavior
+        val scrollHeightOffsetDp = 20.dp
+        var scrollHeightOffsetPx = 0f
+
+        rule.setMaterialContent(lightColorScheme()) {
+            scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+            scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
+            Scaffold(
+                modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+                bottomBar = {
+                    BottomAppBar(
+                        modifier = Modifier.testTag(BottomAppBarTestTag),
+                        scrollBehavior = scrollBehavior
+                    ) {}
+                },
+                floatingActionButton = {
+                    FloatingActionButton(
+                        modifier = Modifier
+                            .testTag("FAB")
+                            .offset(y = 4.dp),
+                        onClick = { /* do something */ },
+                    ) {}
+                },
+                floatingActionButtonPosition = FabPosition.EndOverlay
+            ) {}
+        }
+
+        // Simulate scrolled content.
+        rule.runOnIdle {
+            scrollBehavior.state.heightOffset = -scrollHeightOffsetPx
+            scrollBehavior.state.contentOffset = -scrollHeightOffsetPx
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(BottomAppBarTestTag)
+            .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight - scrollHeightOffsetDp)
+
+        val appBarBounds = rule.onNodeWithTag(BottomAppBarTestTag).getUnclippedBoundsInRoot()
+        val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+        rule.onNodeWithTag("FAB")
+            // FAB should be 16.dp from the end
+            .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+            // FAB should be 12.dp from the bottom
+            .assertTopPositionInRootIsEqualTo(rule.rootHeight() - 12.dp - fabBounds.height)
+    }
+
+    @Test
+    fun bottomAppBar_exitAlways_allowHorizontalScroll() {
+        lateinit var state: LazyListState
+        rule.setMaterialContent(lightColorScheme()) {
+            state = rememberLazyListState()
+            MultiPageContent(BottomAppBarDefaults.exitAlwaysScrollBehavior(), state)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+        rule.runOnIdle {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        }
+    }
+
     @OptIn(ExperimentalMaterial3Api::class)
     @Composable
     private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
@@ -1218,6 +1318,39 @@
         }
     }
 
+    @Composable
+    private fun MultiPageContent(scrollBehavior: BottomAppBarScrollBehavior, state: LazyListState) {
+        Scaffold(
+            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+            bottomBar = {
+                BottomAppBar(
+                    modifier = Modifier.testTag(BottomAppBarTestTag),
+                    scrollBehavior = scrollBehavior
+                ) {}
+            }
+        ) { contentPadding ->
+            LazyRow(
+                Modifier
+                    .fillMaxSize()
+                    .testTag(LazyListTag), state
+            ) {
+                items(2) { page ->
+                    LazyColumn(
+                        modifier = Modifier.fillParentMaxSize(),
+                        contentPadding = contentPadding
+                    ) {
+                        items(50) {
+                            Text(
+                                modifier = Modifier.fillParentMaxWidth(),
+                                text = "Item #$page x $it"
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Checks the app bar's components positioning when it's a [TopAppBar], a
      * [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
@@ -1583,7 +1716,8 @@
         (TopAppBarSmallTokens.ContainerHeight - FakeIconSize) / 2
 
     private val LazyListTag = "lazyList"
-    private val TopAppBarTestTag = "bar"
+    private val TopAppBarTestTag = "topAppBar"
+    private val BottomAppBarTestTag = "bottomAppBar"
     private val NavigationIconTestTag = "navigationIcon"
     private val TitleTestTag = "title"
     private val ActionsTestTag = "actions"
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
index 9dfb619..925e620 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/FloatingActionButtonTest.kt
@@ -552,7 +552,7 @@
         val pressedElevation = 2.dp
         val hoveredElevation = 3.dp
         val focusedElevation = 4.dp
-        lateinit var tonalElevation: State<Dp>
+        var tonalElevation: Dp = Dp.Unspecified
         lateinit var shadowElevation: State<Dp>
 
         rule.setMaterialContent(lightColorScheme()) {
@@ -563,12 +563,12 @@
                 focusedElevation = focusedElevation
             )
 
-            tonalElevation = fabElevation.tonalElevation(interactionSource)
+            tonalElevation = fabElevation.tonalElevation()
             shadowElevation = fabElevation.shadowElevation(interactionSource)
         }
 
         rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(defaultElevation)
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
             assertThat(shadowElevation.value).isEqualTo(defaultElevation)
         }
 
@@ -577,7 +577,7 @@
         }
 
         rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(pressedElevation)
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
             assertThat(shadowElevation.value).isEqualTo(pressedElevation)
         }
     }
@@ -589,7 +589,7 @@
         val pressedElevation = 2.dp
         val hoveredElevation = 3.dp
         val focusedElevation = 4.dp
-        lateinit var tonalElevation: State<Dp>
+        var tonalElevation: Dp = Dp.Unspecified
         lateinit var shadowElevation: State<Dp>
 
         rule.setMaterialContent(lightColorScheme()) {
@@ -600,12 +600,12 @@
                 focusedElevation = focusedElevation
             )
 
-            tonalElevation = fabElevation.tonalElevation(interactionSource)
+            tonalElevation = fabElevation.tonalElevation()
             shadowElevation = fabElevation.shadowElevation(interactionSource)
         }
 
         rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(defaultElevation)
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
             assertThat(shadowElevation.value).isEqualTo(defaultElevation)
         }
 
@@ -614,7 +614,7 @@
         }
 
         rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(5.dp)
+            assertThat(tonalElevation).isEqualTo(5.dp)
             assertThat(shadowElevation.value).isEqualTo(5.dp)
         }
     }
@@ -626,7 +626,7 @@
         var pressedElevation by mutableStateOf(2.dp)
         val hoveredElevation = 3.dp
         val focusedElevation = 4.dp
-        lateinit var tonalElevation: State<Dp>
+        var tonalElevation: Dp = Dp.Unspecified
         lateinit var shadowElevation: State<Dp>
 
         rule.setMaterialContent(lightColorScheme()) {
@@ -637,12 +637,12 @@
                 focusedElevation = focusedElevation
             )
 
-            tonalElevation = fabElevation.tonalElevation(interactionSource)
+            tonalElevation = fabElevation.tonalElevation()
             shadowElevation = fabElevation.shadowElevation(interactionSource)
         }
 
         rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(defaultElevation)
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
             assertThat(shadowElevation.value).isEqualTo(defaultElevation)
         }
 
@@ -651,7 +651,7 @@
         }
 
         rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(pressedElevation)
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
             assertThat(shadowElevation.value).isEqualTo(pressedElevation)
         }
 
@@ -661,7 +661,7 @@
 
         // We are still pressed, so we should now show the updated value for the pressed state
         rule.runOnIdle {
-            assertThat(tonalElevation.value).isEqualTo(5.dp)
+            assertThat(tonalElevation).isEqualTo(defaultElevation)
             assertThat(shadowElevation.value).isEqualTo(5.dp)
         }
     }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index 85a2ddc..859ae81 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -48,7 +48,6 @@
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -144,15 +143,49 @@
     }
 
     @Test
+    fun modalBottomSheet_isDismissedOnSwipeDown() {
+        var showBottomSheet by mutableStateOf(true)
+        val sheetState = SheetState(skipPartiallyExpanded = false, density = rule.density)
+
+        rule.setContent {
+            val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
+                WindowInsets(0) else BottomSheetDefaults.windowInsets
+
+            if (showBottomSheet) {
+                ModalBottomSheet(
+                    sheetState = sheetState,
+                    onDismissRequest = { showBottomSheet = false },
+                    windowInsets = windowInsets
+                ) {
+                    Box(
+                        Modifier
+                            .size(sheetHeight)
+                            .testTag(sheetTag)
+                    )
+                }
+            }
+        }
+
+        assertThat(sheetState.isVisible).isTrue()
+
+        // Swipe Down
+        rule.onNodeWithTag(sheetTag).performTouchInput {
+            swipeDown()
+        }
+        rule.waitForIdle()
+
+        // Bottom sheet should not exist
+        rule.onNodeWithTag(sheetTag).assertDoesNotExist()
+    }
+
+    @Test
     fun modalBottomSheet_fillsScreenWidth() {
         var boxWidth = 0
         var screenWidth by mutableStateOf(0)
 
         rule.setContent {
             val context = LocalContext.current
-            val density = LocalDensity.current
-            val resScreenWidth = context.resources.configuration.screenWidthDp
-            with(density) { screenWidth = resScreenWidth.dp.roundToPx() }
+            screenWidth = context.resources.displayMetrics.widthPixels
             val windowInsets = if (edgeToEdgeWrapper.edgeToEdgeEnabled)
                 WindowInsets(0) else BottomSheetDefaults.windowInsets
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
index 5ab689e..ab6f9b9 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ScaffoldTest.kt
@@ -32,6 +32,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.SubcomposeLayout
@@ -657,6 +658,72 @@
         assertWithMessage("Expected placeCount to be >= 1").that(onPlaceCount).isAtLeast(1)
     }
 
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun scaffold_subcomposeInMeasureFix_enabled_measuresChildrenInMeasurement() {
+        ScaffoldSubcomposeInMeasureFix = true
+        var size: IntSize? = null
+        var measured = false
+        rule.setContent {
+            Layout(
+                content = {
+                    Scaffold(
+                        content = {
+                            Box(Modifier.onSizeChanged { size = it })
+                        }
+                    )
+                }
+            ) { measurables, constraints ->
+                measurables.map { it.measure(constraints) }
+                measured = true
+                layout(0, 0) {
+                    // Empty measurement since we only care about placement
+                }
+            }
+        }
+
+        assertWithMessage("Measure should have been executed")
+            .that(measured).isTrue()
+        assertWithMessage("Expected size to be initialized")
+            .that(size).isNotNull()
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun scaffold_subcomposeInMeasureFix_disabled_measuresChildrenInPlacement() {
+        ScaffoldSubcomposeInMeasureFix = false
+        var size: IntSize? = null
+        var measured = false
+        var placed = false
+        rule.setContent {
+            Layout(
+                content = {
+                    Scaffold(
+                        content = {
+                            Box(Modifier.onSizeChanged { size = it })
+                        }
+                    )
+                }
+            ) { measurables, constraints ->
+                val placeables = measurables.map { it.measure(constraints) }
+                measured = true
+                assertWithMessage("Expected size to not be initialized in placement")
+                    .that(size).isNull()
+                layout(constraints.maxWidth, constraints.maxHeight) {
+                    placeables.forEach { it.place(0, 0) }
+                    placed = true
+                }
+            }
+        }
+
+        assertWithMessage("Measure should have been executed")
+            .that(measured).isTrue()
+        assertWithMessage("Placement should have been executed")
+            .that(placed).isTrue()
+        assertWithMessage("Expected size to be initialized")
+            .that(size).isNotNull()
+    }
+
     private fun assertDpIsWithinThreshold(actual: Dp, expected: Dp, threshold: Dp) {
         assertThat(actual.value).isWithin(threshold.value).of(expected.value)
     }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
index 85e41b3..a053929a 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
@@ -28,6 +28,7 @@
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
@@ -87,7 +88,6 @@
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 import java.util.UUID
 import kotlin.math.max
-import kotlin.math.roundToInt
 import kotlinx.coroutines.launch
 
 /**
@@ -198,10 +198,12 @@
                             )
                         }
                     )
-                    .anchoredDraggable(
-                        state = sheetState.anchoredDraggableState,
+                    .draggable(
+                        state = sheetState.anchoredDraggableState.draggableState,
                         orientation = Orientation.Vertical,
-                        enabled = sheetState.isVisible
+                        enabled = sheetState.isVisible,
+                        startDragImmediately = sheetState.anchoredDraggableState.isAnimationRunning,
+                        onDragStopped = { settleToDismiss(it) }
                     )
                     .modalBottomSheetAnchors(
                         sheetState = sheetState,
@@ -414,10 +416,7 @@
         composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
 
     private val displayWidth: Int
-        get() {
-            val density = context.resources.displayMetrics.density
-            return (context.resources.configuration.screenWidthDp * density).roundToInt()
-        }
+        get() = context.resources.displayMetrics.widthPixels
 
     private val params: WindowManager.LayoutParams =
         WindowManager.LayoutParams().apply {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index cd79a42..7d8c9f6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -73,6 +73,7 @@
 import androidx.compose.ui.layout.AlignmentLine
 import androidx.compose.ui.layout.LastBaseline
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -391,6 +392,7 @@
  * @param contentPadding the padding applied to the content of this BottomAppBar
  * @param windowInsets a window insets that app bar will respect.
  */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun BottomAppBar(
     actions: @Composable RowScope.() -> Unit,
@@ -402,12 +404,76 @@
     contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
     windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
 ) = BottomAppBar(
+    actions = actions,
+    modifier = modifier,
+    floatingActionButton = floatingActionButton,
+    containerColor = containerColor,
+    contentColor = contentColor,
+    tonalElevation = tonalElevation,
+    contentPadding = contentPadding,
+    windowInsets = windowInsets,
+    scrollBehavior = null
+)
+
+/**
+ * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>.
+ *
+ * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ *
+ * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ *
+ * @sample androidx.compose.material3.samples.SimpleBottomAppBar
+ *
+ * It can optionally display a [FloatingActionButton] embedded at the end of the BottomAppBar.
+ *
+ * @sample androidx.compose.material3.samples.BottomAppBarWithFAB
+ *
+ * A bottom app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when
+ * working in conjunction with a scrolling content looks like:
+ *
+ * @sample androidx.compose.material3.samples.ExitAlwaysBottomAppBar
+ *
+ * Also see [NavigationBar].
+ *
+ * @param actions the icon content of this BottomAppBar. The default layout here is a [Row],
+ * so content inside will be placed horizontally.
+ * @param modifier the [Modifier] to be applied to this BottomAppBar
+ * @param floatingActionButton optional floating action button at the end of this BottomAppBar
+ * @param containerColor the color used for the background of this BottomAppBar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param windowInsets a window insets that app bar will respect.
+ * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
+ * be applied by this bottom app bar to set up its height. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the bottom app bar appearance as the
+ * content scrolls. See [BottomAppBarScrollBehavior.nestedScrollConnection].
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BottomAppBar(
+    actions: @Composable RowScope.() -> Unit,
+    modifier: Modifier = Modifier,
+    floatingActionButton: @Composable (() -> Unit)? = null,
+    containerColor: Color = BottomAppBarDefaults.containerColor,
+    contentColor: Color = contentColorFor(containerColor),
+    tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
+    contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
+    windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
+    scrollBehavior: BottomAppBarScrollBehavior? = null,
+) = BottomAppBar(
     modifier = modifier,
     containerColor = containerColor,
     contentColor = contentColor,
     tonalElevation = tonalElevation,
     windowInsets = windowInsets,
-    contentPadding = contentPadding
+    contentPadding = contentPadding,
+    scrollBehavior = scrollBehavior
 ) {
     Row(
         modifier = Modifier.weight(1f),
@@ -455,6 +521,7 @@
  * @param content the content of this BottomAppBar. The default layout here is a [Row],
  * so content inside will be placed horizontally.
  */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun BottomAppBar(
     modifier: Modifier = Modifier,
@@ -464,7 +531,81 @@
     contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
     windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
     content: @Composable RowScope.() -> Unit
+) = BottomAppBar(
+    modifier = modifier,
+    containerColor = containerColor,
+    contentColor = contentColor,
+    tonalElevation = tonalElevation,
+    contentPadding = contentPadding,
+    windowInsets = windowInsets,
+    scrollBehavior = null,
+    content = content
+)
+
+/**
+ * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>.
+ *
+ * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ *
+ * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ *
+ * If you are interested in displaying a [FloatingActionButton], consider using another overload.
+ *
+ * Also see [NavigationBar].
+ *
+ * @param modifier the [Modifier] to be applied to this BottomAppBar
+ * @param containerColor the color used for the background of this BottomAppBar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param windowInsets a window insets that app bar will respect.
+ * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
+ * be applied by this bottom app bar to set up its height. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the bottom app bar appearance as the
+ * content scrolls. See [BottomAppBarScrollBehavior.nestedScrollConnection].
+ * @param content the content of this BottomAppBar. The default layout here is a [Row],
+ * so content inside will be placed horizontally.
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BottomAppBar(
+    modifier: Modifier = Modifier,
+    containerColor: Color = BottomAppBarDefaults.containerColor,
+    contentColor: Color = contentColorFor(containerColor),
+    tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
+    contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
+    windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
+    scrollBehavior: BottomAppBarScrollBehavior? = null,
+    content: @Composable RowScope.() -> Unit
 ) {
+    // Set up support for resizing the bottom app bar when vertically dragging the bar itself.
+    val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) {
+        Modifier.draggable(
+            orientation = Orientation.Vertical,
+            state = rememberDraggableState { delta ->
+                scrollBehavior.state.heightOffset -= delta
+            },
+            onDragStopped = { velocity ->
+                settleAppBarBottom(
+                    scrollBehavior.state,
+                    velocity,
+                    scrollBehavior.flingAnimationSpec,
+                    scrollBehavior.snapAnimationSpec
+                )
+            }
+        )
+    } else {
+        Modifier
+    }
+
+    // Compose a Surface with a Row content.
+    // The height of the app bar is determined by subtracting the bar's height offset from the
+    // app bar's defined constant height value (i.e. the ContainerHeight token).
     Surface(
         color = containerColor,
         contentColor = contentColor,
@@ -472,6 +613,19 @@
         // TODO(b/209583788): Consider adding a shape parameter if updated design guidance allows
         shape = BottomAppBarTokens.ContainerShape.value,
         modifier = modifier
+            .layout { measurable, constraints ->
+                // Sets the app bar's height offset to collapse the entire bar's height when content
+                // is scrolled.
+                scrollBehavior?.state?.heightOffsetLimit =
+                    -BottomAppBarTokens.ContainerHeight.toPx()
+
+                val placeable = measurable.measure(constraints)
+                val height = placeable.height + (scrollBehavior?.state?.heightOffset ?: 0f)
+                layout(placeable.width, height.roundToInt()) {
+                    placeable.place(0, 0)
+                }
+            }
+            .then(appBarDragModifier)
     ) {
         Row(
             Modifier
@@ -979,6 +1133,50 @@
     }
 }
 
+/**
+ * A BottomAppBarScrollBehavior defines how a bottom app bar should behave when the content under
+ * it is scrolled.
+ *
+ * @see [BottomAppBarDefaults.exitAlwaysScrollBehavior]
+ */
+@ExperimentalMaterial3Api
+@Stable
+interface BottomAppBarScrollBehavior {
+
+    /**
+     * A [BottomAppBarState] that is attached to this behavior and is read and updated when
+     * scrolling happens.
+     */
+    val state: BottomAppBarState
+
+    /**
+     * Indicates whether the bottom app bar is pinned.
+     *
+     * A pinned app bar will stay fixed in place when content is scrolled and will not react to any
+     * drag gestures.
+     */
+    val isPinned: Boolean
+
+    /**
+     * An optional [AnimationSpec] that defines how the bottom app bar snaps to either fully
+     * collapsed or fully extended state when a fling or a drag scrolled it into an intermediate
+     * position.
+     */
+    val snapAnimationSpec: AnimationSpec<Float>?
+
+    /**
+     * An optional [DecayAnimationSpec] that defined how to fling the bottom app bar when the user
+     * flings the app bar itself, or the content below it.
+     */
+    val flingAnimationSpec: DecayAnimationSpec<Float>?
+
+    /**
+     * A [NestedScrollConnection] that should be attached to a [Modifier.nestedScroll] in order to
+     * keep track of the scroll events.
+     */
+    val nestedScrollConnection: NestedScrollConnection
+}
+
 /** Contains default values used for the bottom app bar implementations. */
 object BottomAppBarDefaults {
 
@@ -1012,6 +1210,287 @@
     val bottomAppBarFabColor: Color
         @Composable get() =
             FabSecondaryTokens.ContainerColor.value
+
+    /**
+     * Returns a [BottomAppBarScrollBehavior]. A bottom app bar that is set up with this
+     * [BottomAppBarScrollBehavior] will immediately collapse when the content is pulled up, and
+     * will immediately appear when the content is pulled down.
+     *
+     * @param state the state object to be used to control or observe the bottom app bar's scroll
+     * state. See [rememberBottomAppBarState] for a state that is remembered across compositions.
+     * @param canScroll a callback used to determine whether scroll events are to be
+     * handled by this [ExitAlwaysScrollBehavior]
+     * @param snapAnimationSpec an optional [AnimationSpec] that defines how the bottom app bar
+     * snaps to either fully collapsed or fully extended state when a fling or a drag scrolled it
+     * into an intermediate position
+     * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the
+     * bottom app bar when the user flings the app bar itself, or the content below it
+     */
+    @ExperimentalMaterial3Api
+    @Composable
+    fun exitAlwaysScrollBehavior(
+        state: BottomAppBarState = rememberBottomAppBarState(),
+        canScroll: () -> Boolean = { true },
+        snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
+        flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
+    ): BottomAppBarScrollBehavior =
+        ExitAlwaysScrollBehavior(
+            state = state,
+            snapAnimationSpec = snapAnimationSpec,
+            flingAnimationSpec = flingAnimationSpec,
+            canScroll = canScroll
+        )
+}
+
+/**
+ * Creates a [BottomAppBarState] that is remembered across compositions.
+ *
+ * @param initialHeightOffsetLimit the initial value for [BottomAppBarState.heightOffsetLimit],
+ * which represents the pixel limit that a bottom app bar is allowed to collapse when the scrollable
+ * content is scrolled
+ * @param initialHeightOffset the initial value for [BottomAppBarState.heightOffset]. The initial
+ * offset height offset should be between zero and [initialHeightOffsetLimit].
+ * @param initialContentOffset the initial value for [BottomAppBarState.contentOffset]
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun rememberBottomAppBarState(
+    initialHeightOffsetLimit: Float = -Float.MAX_VALUE,
+    initialHeightOffset: Float = 0f,
+    initialContentOffset: Float = 0f
+): BottomAppBarState {
+    return rememberSaveable(saver = BottomAppBarState.Saver) {
+        BottomAppBarState(
+            initialHeightOffsetLimit,
+            initialHeightOffset,
+            initialContentOffset
+        )
+    }
+}
+
+/**
+ * A state object that can be hoisted to control and observe the bottom app bar state. The state is
+ * read and updated by a [BottomAppBarScrollBehavior] implementation.
+ *
+ * In most cases, this state will be created via [rememberBottomAppBarState].
+ */
+@ExperimentalMaterial3Api
+interface BottomAppBarState {
+
+    /**
+     * The bottom app bar's height offset limit in pixels, which represents the limit that a bottom
+     * app bar is allowed to collapse to.
+     *
+     * Use this limit to coerce the [heightOffset] value when it's updated.
+     */
+    var heightOffsetLimit: Float
+
+    /**
+     * The bottom app bar's current height offset in pixels. This height offset is applied to the
+     * fixed height of the app bar to control the displayed height when content is being scrolled.
+     *
+     * Updates to the [heightOffset] value are coerced between zero and [heightOffsetLimit].
+     */
+    var heightOffset: Float
+
+    /**
+     * The total offset of the content scrolled under the bottom app bar.
+     *
+     * This value is updated by a [BottomAppBarScrollBehavior] whenever a nested scroll connection
+     * consumes scroll events. A common implementation would update the value to be the sum of all
+     * [NestedScrollConnection.onPostScroll] `consumed.y` values.
+     */
+    var contentOffset: Float
+
+    /**
+     * A value that represents the collapsed height percentage of the app bar.
+     *
+     * A `0.0` represents a fully expanded bar, and `1.0` represents a fully collapsed bar (computed
+     * as [heightOffset] / [heightOffsetLimit]).
+     */
+    val collapsedFraction: Float
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [BottomAppBarState].
+         */
+        val Saver: Saver<BottomAppBarState, *> = listSaver(
+            save = { listOf(it.heightOffsetLimit, it.heightOffset, it.contentOffset) },
+            restore = {
+                BottomAppBarState(
+                    initialHeightOffsetLimit = it[0],
+                    initialHeightOffset = it[1],
+                    initialContentOffset = it[2]
+                )
+            }
+        )
+    }
+}
+
+/**
+ * Creates a [BottomAppBarState].
+ *
+ * @param initialHeightOffsetLimit the initial value for [BottomAppBarState.heightOffsetLimit],
+ * which represents the pixel limit that a bottom app bar is allowed to collapse when the scrollable
+ * content is scrolled
+ * @param initialHeightOffset the initial value for [BottomAppBarState.heightOffset]. The initial
+ * offset height offset should be between zero and [initialHeightOffsetLimit].
+ * @param initialContentOffset the initial value for [BottomAppBarState.contentOffset]
+ */
+@ExperimentalMaterial3Api
+fun BottomAppBarState(
+    initialHeightOffsetLimit: Float,
+    initialHeightOffset: Float,
+    initialContentOffset: Float
+): BottomAppBarState = BottomAppBarStateImpl(
+    initialHeightOffsetLimit,
+    initialHeightOffset,
+    initialContentOffset
+)
+
+@ExperimentalMaterial3Api
+@Stable
+private class BottomAppBarStateImpl(
+    initialHeightOffsetLimit: Float,
+    initialHeightOffset: Float,
+    initialContentOffset: Float
+) : BottomAppBarState {
+
+    override var heightOffsetLimit by mutableFloatStateOf(initialHeightOffsetLimit)
+
+    override var heightOffset: Float
+        get() = _heightOffset.floatValue
+        set(newOffset) {
+            _heightOffset.floatValue = newOffset.coerceIn(
+                minimumValue = heightOffsetLimit,
+                maximumValue = 0f
+            )
+        }
+
+    override var contentOffset by mutableFloatStateOf(initialContentOffset)
+
+    override val collapsedFraction: Float
+        get() = if (heightOffsetLimit != 0f) {
+            heightOffset / heightOffsetLimit
+        } else {
+            0f
+        }
+
+    private var _heightOffset = mutableFloatStateOf(initialHeightOffset)
+}
+
+/**
+ * A [BottomAppBarScrollBehavior] that adjusts its properties to affect the colors and height of a
+ * bottom app bar.
+ *
+ * A bottom app bar that is set up with this [BottomAppBarScrollBehavior] will immediately collapse
+ * when the nested content is pulled up, and will immediately appear when the content is pulled
+ * down.
+ *
+ * @param state a [BottomAppBarState]
+ * @param snapAnimationSpec an optional [AnimationSpec] that defines how the bottom app bar snaps to
+ * either fully collapsed or fully extended state when a fling or a drag scrolled it into an
+ * intermediate position
+ * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the bottom
+ * app bar when the user flings the app bar itself, or the content below it
+ * @param canScroll a callback used to determine whether scroll events are to be
+ * handled by this [ExitAlwaysScrollBehavior]
+ */
+@ExperimentalMaterial3Api
+private class ExitAlwaysScrollBehavior(
+    override val state: BottomAppBarState,
+    override val snapAnimationSpec: AnimationSpec<Float>?,
+    override val flingAnimationSpec: DecayAnimationSpec<Float>?,
+    val canScroll: () -> Boolean = { true }
+) : BottomAppBarScrollBehavior {
+    override val isPinned: Boolean = false
+    override var nestedScrollConnection =
+        object : NestedScrollConnection {
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                if (!canScroll()) return Offset.Zero
+                state.contentOffset += consumed.y
+                if (state.heightOffset == 0f || state.heightOffset == state.heightOffsetLimit) {
+                    if (consumed.y == 0f && available.y > 0f) {
+                        // Reset the total content offset to zero when scrolling all the way down.
+                        // This will eliminate some float precision inaccuracies.
+                        state.contentOffset = 0f
+                    }
+                }
+                state.heightOffset = state.heightOffset + consumed.y
+                return Offset.Zero
+            }
+
+            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+                val superConsumed = super.onPostFling(consumed, available)
+                return superConsumed + settleAppBarBottom(
+                    state,
+                    available.y,
+                    flingAnimationSpec,
+                    snapAnimationSpec
+                )
+            }
+        }
+}
+
+/**
+ * Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
+ * after the fling settles.
+ */
+@ExperimentalMaterial3Api
+private suspend fun settleAppBarBottom(
+    state: BottomAppBarState,
+    velocity: Float,
+    flingAnimationSpec: DecayAnimationSpec<Float>?,
+    snapAnimationSpec: AnimationSpec<Float>?
+): Velocity {
+    // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
+    // and just return Zero Velocity.
+    // Note that we don't check for 0f due to float precision with the collapsedFraction
+    // calculation.
+    if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
+        return Velocity.Zero
+    }
+    var remainingVelocity = velocity
+    // In case there is an initial velocity that was left after a previous user fling, animate to
+    // continue the motion to expand or collapse the app bar.
+    if (flingAnimationSpec != null && abs(velocity) > 1f) {
+        var lastValue = 0f
+        AnimationState(
+            initialValue = 0f,
+            initialVelocity = velocity,
+        )
+            .animateDecay(flingAnimationSpec) {
+                val delta = value - lastValue
+                val initialHeightOffset = state.heightOffset
+                state.heightOffset = initialHeightOffset + delta
+                val consumed = abs(initialHeightOffset - state.heightOffset)
+                lastValue = value
+                remainingVelocity = this.velocity
+                // avoid rounding errors and stop if anything is unconsumed
+                if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+            }
+    }
+    // Snap if animation specs were provided.
+    if (snapAnimationSpec != null) {
+        if (state.heightOffset < 0 &&
+            state.heightOffset > state.heightOffsetLimit
+        ) {
+            AnimationState(initialValue = state.heightOffset).animateTo(
+                if (state.collapsedFraction < 0.5f) {
+                    0f
+                } else {
+                    state.heightOffsetLimit
+                },
+                animationSpec = snapAnimationSpec
+            ) { state.heightOffset = value }
+        }
+    }
+
+    return Velocity(0f, remainingVelocity)
 }
 
 // Padding minus IconButton's min touch target expansion
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index 88b46ce..259114e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -117,7 +117,7 @@
     val containerColor = colors.containerColor(enabled).value
     val contentColor = colors.contentColor(enabled).value
     val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
-    val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp
+    val tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp
     Surface(
         onClick = onClick,
         modifier = modifier.semantics { role = Role.Button },
@@ -769,8 +769,7 @@
     private val disabledElevation: Dp,
 ) {
     /**
-     * Represents the tonal elevation used in a button, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [shadowElevation].
+     * Represents the tonal elevation used in a button, depending on its [enabled] state.
      *
      * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
      * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -779,16 +778,14 @@
      * See [shadowElevation] which controls the elevation of the shadow drawn around the button.
      *
      * @param enabled whether the button is enabled
-     * @param interactionSource the [InteractionSource] for this button
      */
-    @Composable
-    internal fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
-        return animateElevation(enabled = enabled, interactionSource = interactionSource)
+    internal fun tonalElevation(enabled: Boolean): Dp {
+        return if (enabled) defaultElevation else disabledElevation
     }
 
     /**
      * Represents the shadow elevation used in a button, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [tonalElevation].
+     * [interactionSource].
      *
      * Shadow elevation is used to apply a shadow around the button to give it higher emphasis.
      *
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
index 8a55fb0..bbec175 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt
@@ -86,7 +86,7 @@
         shape = shape,
         color = colors.containerColor(enabled = true).value,
         contentColor = colors.contentColor(enabled = true).value,
-        tonalElevation = elevation.tonalElevation(enabled = true, interactionSource = null).value,
+        tonalElevation = elevation.tonalElevation(enabled = true),
         shadowElevation = elevation.shadowElevation(enabled = true, interactionSource = null).value,
         border = border,
     ) {
@@ -147,7 +147,7 @@
         shape = shape,
         color = colors.containerColor(enabled).value,
         contentColor = colors.contentColor(enabled).value,
-        tonalElevation = elevation.tonalElevation(enabled, interactionSource).value,
+        tonalElevation = elevation.tonalElevation(enabled),
         shadowElevation = elevation.shadowElevation(enabled, interactionSource).value,
         border = border,
         interactionSource = interactionSource,
@@ -564,8 +564,7 @@
     private val disabledElevation: Dp
 ) {
     /**
-     * Represents the tonal elevation used in a card, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [shadowElevation].
+     * Represents the tonal elevation used in a card, depending on its [enabled].
      *
      * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
      * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -574,22 +573,13 @@
      * See [shadowElevation] which controls the elevation of the shadow drawn around the card.
      *
      * @param enabled whether the card is enabled
-     * @param interactionSource the [InteractionSource] for this card
      */
-    @Composable
-    internal fun tonalElevation(
-        enabled: Boolean,
-        interactionSource: InteractionSource?
-    ): State<Dp> {
-        if (interactionSource == null) {
-            return remember { mutableStateOf(defaultElevation) }
-        }
-        return animateElevation(enabled = enabled, interactionSource = interactionSource)
-    }
+    internal fun tonalElevation(enabled: Boolean): Dp =
+        if (enabled) defaultElevation else disabledElevation
 
     /**
      * Represents the shadow elevation used in a card, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [tonalElevation].
+     * [interactionSource].
      *
      * Shadow elevation is used to apply a shadow around the card to give it higher emphasis.
      *
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index 4f00d2c..09096d6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -1325,7 +1325,7 @@
         enabled = enabled,
         shape = shape,
         color = colors.containerColor(enabled).value,
-        tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp,
+        tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp,
         shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
         border = border,
         interactionSource = interactionSource,
@@ -1373,7 +1373,7 @@
         enabled = enabled,
         shape = shape,
         color = colors.containerColor(enabled, selected).value,
-        tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp,
+        tonalElevation = elevation?.tonalElevation(enabled) ?: 0.dp,
         shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp,
         border = border,
         interactionSource = interactionSource,
@@ -1454,8 +1454,7 @@
     private val disabledElevation: Dp
 ) {
     /**
-     * Represents the tonal elevation used in a chip, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [shadowElevation].
+     * Represents the tonal elevation used in a chip, depending on its [enabled] state.
      *
      * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
      * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -1464,19 +1463,14 @@
      * See [shadowElevation] which controls the elevation of the shadow drawn around the chip.
      *
      * @param enabled whether the chip is enabled
-     * @param interactionSource the [InteractionSource] for this chip
      */
-    @Composable
-    internal fun tonalElevation(
-        enabled: Boolean,
-        interactionSource: InteractionSource
-    ): State<Dp> {
-        return animateElevation(enabled = enabled, interactionSource = interactionSource)
+    internal fun tonalElevation(enabled: Boolean): Dp {
+        return if (enabled) elevation else disabledElevation
     }
 
     /**
      * Represents the shadow elevation used in a chip, depending on its [enabled] state and
-     * [interactionSource]. This should typically be the same value as the [tonalElevation].
+     * [interactionSource].
      *
      * Shadow elevation is used to apply a shadow around the chip to give it higher emphasis.
      *
@@ -1617,8 +1611,7 @@
     val disabledElevation: Dp
 ) {
     /**
-     * Represents the tonal elevation used in a chip, depending on [enabled] and
-     * [interactionSource]. This should typically be the same value as the [shadowElevation].
+     * Represents the tonal elevation used in a chip, depending on [enabled].
      *
      * Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
      * When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
@@ -1627,19 +1620,14 @@
      * See [shadowElevation] which controls the elevation of the shadow drawn around the Chip.
      *
      * @param enabled whether the chip is enabled
-     * @param interactionSource the [InteractionSource] for this chip
      */
-    @Composable
-    internal fun tonalElevation(
-        enabled: Boolean,
-        interactionSource: InteractionSource
-    ): State<Dp> {
-        return animateElevation(enabled = enabled, interactionSource = interactionSource)
+    internal fun tonalElevation(enabled: Boolean): Dp {
+        return if (enabled) elevation else disabledElevation
     }
 
     /**
      * Represents the shadow elevation used in a chip, depending on [enabled] and
-     * [interactionSource]. This should typically be the same value as the [tonalElevation].
+     * [interactionSource].
      *
      * Shadow elevation is used to apply a shadow around the surface to give it higher emphasis.
      *
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
index ae586c8..dd8ce60 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt
@@ -108,7 +108,7 @@
         shape = shape,
         color = containerColor,
         contentColor = contentColor,
-        tonalElevation = elevation.tonalElevation(interactionSource = interactionSource).value,
+        tonalElevation = elevation.tonalElevation(),
         shadowElevation = elevation.shadowElevation(interactionSource = interactionSource).value,
         interactionSource = interactionSource,
     ) {
@@ -496,9 +496,8 @@
         return animateElevation(interactionSource = interactionSource)
     }
 
-    @Composable
-    internal fun tonalElevation(interactionSource: InteractionSource): State<Dp> {
-        return animateElevation(interactionSource = interactionSource)
+    internal fun tonalElevation(): Dp {
+        return defaultElevation
     }
 
     @Composable
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt
new file mode 100644
index 0000000..1aedef2
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Label.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.BasicTooltipBox
+import androidx.compose.foundation.BasicTooltipState
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.HoverInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.rememberBasicTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.flow.collectLatest
+
+/**
+ * Label component that will append a [label] to [content].
+ * The positioning logic uses [TooltipDefaults.rememberPlainTooltipPositionProvider].
+ *
+ * Label appended to thumbs of Slider:
+ *
+ * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
+ *
+ * Label appended to thumbs of RangeSlider:
+ *
+ * @sample androidx.compose.material3.samples.RangeSliderWithCustomComponents
+ *
+ * @param label composable that will be appended to [content]
+ * @param modifier [Modifier] that will be applied to [content]
+ * @param interactionSource the [MutableInteractionSource] representing the
+ * stream of [Interaction]s for the [content].
+ * @param isPersistent boolean to determine if the label should be persistent.
+ * If true, then the label will always show and be anchored to [content].
+ * if false, then the label will only show when pressing down or hovering over the [content].
+ * @param content the composable that [label] will anchor to.
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun Label(
+    label: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    isPersistent: Boolean = false,
+    content: @Composable () -> Unit
+) {
+    // Has the same positioning logic as PlainTooltips
+    val positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider()
+    val state = if (isPersistent)
+        remember { LabelStateImpl() }
+    else
+        rememberBasicTooltipState(mutatorMutex = MutatorMutex())
+    BasicTooltipBox(
+        positionProvider = positionProvider,
+        tooltip = label,
+        state = state,
+        modifier = modifier,
+        focusable = false,
+        enableUserInput = false,
+        content = content
+    )
+    HandleInteractions(
+        enabled = !isPersistent,
+        state = state,
+        interactionSource = interactionSource
+    )
+}
+
+@Composable
+private fun HandleInteractions(
+    enabled: Boolean,
+    state: BasicTooltipState,
+    interactionSource: MutableInteractionSource
+) {
+    if (enabled) {
+        LaunchedEffect(interactionSource) {
+            interactionSource.interactions.collectLatest { interaction ->
+                when (interaction) {
+                    is PressInteraction.Press,
+                    is DragInteraction.Start,
+                    is HoverInteraction.Enter -> { state.show(MutatePriority.UserInput) }
+                    is PressInteraction.Release,
+                    is DragInteraction.Stop,
+                    is HoverInteraction.Exit -> { state.dismiss() }
+                }
+            }
+        }
+    }
+}
+
+private class LabelStateImpl(
+    override val isVisible: Boolean = true,
+    override val isPersistent: Boolean = true
+) : BasicTooltipState {
+    override suspend fun show(mutatePriority: MutatePriority) {}
+
+    override fun dismiss() {}
+
+    override fun onDispose() {}
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index b346d44..4e2d001 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -27,7 +27,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -126,6 +129,7 @@
  * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
  * [content], typically a [NavigationBar].
  */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun ScaffoldLayout(
     fabPosition: FabPosition,
@@ -135,7 +139,42 @@
     fab: @Composable () -> Unit,
     contentWindowInsets: WindowInsets,
     bottomBar: @Composable () -> Unit
+) {
+    if (ScaffoldSubcomposeInMeasureFix) {
+        ScaffoldLayoutWithMeasureFix(
+            fabPosition = fabPosition,
+            topBar = topBar,
+            content = content,
+            snackbar = snackbar,
+            fab = fab,
+            contentWindowInsets = contentWindowInsets,
+            bottomBar = bottomBar
+        )
+    } else {
+        LegacyScaffoldLayout(
+            fabPosition = fabPosition,
+            topBar = topBar,
+            content = content,
+            snackbar = snackbar,
+            fab = fab,
+            contentWindowInsets = contentWindowInsets,
+            bottomBar = bottomBar
+        )
+    }
+}
 
+/**
+ * Layout for a [Scaffold]'s content, subcomposing and measuring during measurement.
+ */
+@Composable
+private fun ScaffoldLayoutWithMeasureFix(
+    fabPosition: FabPosition,
+    topBar: @Composable () -> Unit,
+    content: @Composable (PaddingValues) -> Unit,
+    snackbar: @Composable () -> Unit,
+    fab: @Composable () -> Unit,
+    contentWindowInsets: WindowInsets,
+    bottomBar: @Composable () -> Unit
 ) {
     SubcomposeLayout { constraints ->
         val layoutWidth = constraints.maxWidth
@@ -197,7 +236,7 @@
                         layoutWidth - FabSpacing.roundToPx() - fabWidth
                     }
                 }
-                FabPosition.End -> {
+                FabPosition.End, FabPosition.EndOverlay -> {
                     if (layoutDirection == LayoutDirection.Ltr) {
                         layoutWidth - FabSpacing.roundToPx() - fabWidth
                     } else {
@@ -225,7 +264,7 @@
 
         val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
         val fabOffsetFromBottom = fabPlacement?.let {
-            if (bottomBarHeight == null) {
+            if (bottomBarHeight == null || fabPosition == FabPosition.EndOverlay) {
                 it.height + FabSpacing.roundToPx() +
                     contentWindowInsets.getBottom(this@SubcomposeLayout)
             } else {
@@ -295,6 +334,175 @@
 }
 
 /**
+ * Layout for a [Scaffold]'s content, subcomposing and measuring during measurement.
+ */
+@Composable
+private fun LegacyScaffoldLayout(
+    fabPosition: FabPosition,
+    topBar: @Composable () -> Unit,
+    content: @Composable (PaddingValues) -> Unit,
+    snackbar: @Composable () -> Unit,
+    fab: @Composable () -> Unit,
+    contentWindowInsets: WindowInsets,
+    bottomBar: @Composable () -> Unit
+) {
+    SubcomposeLayout { constraints ->
+        val layoutWidth = constraints.maxWidth
+        val layoutHeight = constraints.maxHeight
+
+        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+
+        layout(layoutWidth, layoutHeight) {
+            val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
+                it.measure(looseConstraints)
+            }
+
+            val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
+                // respect only bottom and horizontal for snackbar and fab
+                val leftInset = contentWindowInsets
+                    .getLeft(this@SubcomposeLayout, layoutDirection)
+                val rightInset = contentWindowInsets
+                    .getRight(this@SubcomposeLayout, layoutDirection)
+                val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
+                // offset the snackbar constraints by the insets values
+                it.measure(
+                    looseConstraints.offset(
+                        -leftInset - rightInset,
+                        -bottomInset
+                    )
+                )
+            }
+
+            val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
+            val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
+
+            val fabPlaceables =
+                subcompose(ScaffoldLayoutContent.Fab, fab).fastMapNotNull { measurable ->
+                    // respect only bottom and horizontal for snackbar and fab
+                    val leftInset =
+                        contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
+                    val rightInset =
+                        contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
+                    val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
+                    measurable.measure(
+                        looseConstraints.offset(
+                            -leftInset - rightInset,
+                            -bottomInset
+                        )
+                    )
+                        .takeIf { it.height != 0 && it.width != 0 }
+                }
+
+            val fabPlacement = if (fabPlaceables.isNotEmpty()) {
+                val fabWidth = fabPlaceables.fastMaxBy { it.width }!!.width
+                val fabHeight = fabPlaceables.fastMaxBy { it.height }!!.height
+                // FAB distance from the left of the layout, taking into account LTR / RTL
+                val fabLeftOffset = when (fabPosition) {
+                    FabPosition.Start -> {
+                        if (layoutDirection == LayoutDirection.Ltr) {
+                            FabSpacing.roundToPx()
+                        } else {
+                            layoutWidth - FabSpacing.roundToPx() - fabWidth
+                        }
+                    }
+                    FabPosition.End -> {
+                        if (layoutDirection == LayoutDirection.Ltr) {
+                            layoutWidth - FabSpacing.roundToPx() - fabWidth
+                        } else {
+                            FabSpacing.roundToPx()
+                        }
+                    }
+                    else -> (layoutWidth - fabWidth) / 2
+                }
+
+                FabPlacement(
+                    left = fabLeftOffset,
+                    width = fabWidth,
+                    height = fabHeight
+                )
+            } else {
+                null
+            }
+
+            val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
+                CompositionLocalProvider(
+                    LocalFabPlacement provides fabPlacement,
+                    content = bottomBar
+                )
+            }.fastMap { it.measure(looseConstraints) }
+
+            val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
+            val fabOffsetFromBottom = fabPlacement?.let {
+                if (bottomBarHeight == null) {
+                    it.height + FabSpacing.roundToPx() +
+                        contentWindowInsets.getBottom(this@SubcomposeLayout)
+                } else {
+                    // Total height is the bottom bar height + the FAB height + the padding
+                    // between the FAB and bottom bar
+                    bottomBarHeight + it.height + FabSpacing.roundToPx()
+                }
+            }
+
+            val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
+                snackbarHeight +
+                    (fabOffsetFromBottom ?: bottomBarHeight
+                    ?: contentWindowInsets.getBottom(this@SubcomposeLayout))
+            } else {
+                0
+            }
+
+            val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
+                val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
+                val innerPadding = PaddingValues(
+                    top =
+                    if (topBarPlaceables.isEmpty()) {
+                        insets.calculateTopPadding()
+                    } else {
+                        topBarHeight.toDp()
+                    },
+                    bottom =
+                    if (bottomBarPlaceables.isEmpty() || bottomBarHeight == null) {
+                        insets.calculateBottomPadding()
+                    } else {
+                        bottomBarHeight.toDp()
+                    },
+                    start = insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
+                    end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection)
+                )
+                content(innerPadding)
+            }.fastMap { it.measure(looseConstraints) }
+
+            // Placing to control drawing order to match default elevation of each placeable
+            bodyContentPlaceables.fastForEach {
+                it.place(0, 0)
+            }
+            topBarPlaceables.fastForEach {
+                it.place(0, 0)
+            }
+            snackbarPlaceables.fastForEach {
+                it.place(
+                    (layoutWidth - snackbarWidth) / 2 +
+                        contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection),
+                    layoutHeight - snackbarOffsetFromBottom
+                )
+            }
+            // The bottom bar is always at the bottom of the layout
+            bottomBarPlaceables.fastForEach {
+                it.place(0, layoutHeight - (bottomBarHeight ?: 0))
+            }
+            // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
+            fabPlacement?.let { placement ->
+                fabPlaceables.fastForEach {
+                    it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
+                }
+            }
+        }
+    }
+}
+
+/**
  * Object containing various default values for [Scaffold] component.
  */
 object ScaffoldDefaults {
@@ -329,18 +537,41 @@
          * exists)
          */
         val End = FabPosition(2)
+
+        /**
+         * Position FAB at the bottom of the screen at the end, overlaying the [NavigationBar] (if
+         * it exists)
+         */
+        val EndOverlay = FabPosition(3)
     }
 
     override fun toString(): String {
         return when (this) {
             Start -> "FabPosition.Start"
             Center -> "FabPosition.Center"
-            else -> "FabPosition.End"
+            End -> "FabPosition.End"
+            else -> "FabPosition.EndOverlay"
         }
     }
 }
 
 /**
+ * Flag indicating if [Scaffold] should subcompose and measure its children during measurement or
+ * during placement.
+ * Set this flag to false to keep Scaffold's old measurement behavior (measuring in placement).
+ *
+ * <b>This flag will be removed in Compose 1.6.0-beta01.</b> If you encounter any issues with the
+ * new behavior, please file an issue at: issuetracker.google.com/issues/new?component=742043
+ */
+// TODO(b/299621062): Remove flag before beta
+@Suppress("GetterSetterNames", "OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:Suppress("GetterSetterNames")
+@get:ExperimentalMaterial3Api
+@set:ExperimentalMaterial3Api
+@ExperimentalMaterial3Api
+var ScaffoldSubcomposeInMeasureFix by mutableStateOf(true)
+
+/**
  * Placement information for a [FloatingActionButton] inside a [Scaffold].
  *
  * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
index 67bdfb5..21e04e7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt
@@ -89,7 +89,7 @@
  * relative to the anchor content.
  * @param tooltip the composable that will be used to populate the tooltip's content.
  * @param state handles the state of the tooltip's visibility.
- * @param modifier the [Modifier] to be applied to the tooltip.
+ * @param modifier the [Modifier] to be applied to the TooltipBox.
  * @param focusable [Boolean] that determines if the tooltip is focusable. When true,
  * the tooltip will consume touch events while it's shown and will have accessibility
  * focus move to the first element of the component. When false, the tooltip
@@ -142,16 +142,16 @@
     content: @Composable () -> Unit
 ) {
     Surface(
-        modifier = modifier
+        shape = shape,
+        color = containerColor
+    ) {
+        Box(modifier = modifier
             .sizeIn(
                 minWidth = TooltipMinWidth,
                 maxWidth = PlainTooltipMaxWidth,
                 minHeight = TooltipMinHeight
-            ),
-        shape = shape,
-        color = containerColor
-    ) {
-        Box(modifier = Modifier.padding(PlainTooltipContentPadding)) {
+            ).padding(PlainTooltipContentPadding)
+        ) {
             val textStyle =
                 MaterialTheme.typography.fromToken(PlainTooltipTokens.SupportingTextFont)
             CompositionLocalProvider(
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index a1f94db..4e0c3fe 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -1278,7 +1278,7 @@
     private var reusingGroup = -1
     private var childrenComposing: Int = 0
     private var compositionToken: Int = 0
-    private var sourceInformationEnabled = true
+    private var sourceInformationEnabled = false
     private val derivedStateObserver = object : DerivedStateObserver {
         override fun start(derivedState: DerivedState<*>) {
             childrenComposing++
@@ -1458,6 +1458,12 @@
         if (!forceRecomposeScopes) {
             forceRecomposeScopes = parentContext.collectingParameterInformation
         }
+
+        // Propagate collecting source information
+        if (!sourceInformationEnabled) {
+            sourceInformationEnabled = parentContext.collectingSourceInformation
+        }
+
         parentProvider.read(LocalInspectionTables)?.let {
             it.add(slotTable)
             parentContext.recordInspectionTable(it)
@@ -1544,11 +1550,13 @@
         private set
 
     /**
-     * Start collecting parameter information. This enables the tools API to always be able to
-     * determine the parameter values of composable calls.
+     * Start collecting parameter information and line number information. This enables the tools
+     * API to always be able to determine the parameter values of composable calls as well as the
+     * source location of calls.
      */
     override fun collectParameterInformation() {
         forceRecomposeScopes = true
+        sourceInformationEnabled = true
     }
 
     @OptIn(InternalComposeApi::class)
@@ -2115,6 +2123,7 @@
                 CompositionContextImpl(
                     compoundKeyHash,
                     forceRecomposeScopes,
+                    sourceInformationEnabled,
                     (composition as? CompositionImpl)?.observerHolder
                 )
             )
@@ -3158,20 +3167,22 @@
     @ComposeCompilerApi
     override fun sourceInformation(sourceInformation: String) {
         if (inserting && sourceInformationEnabled) {
-            writer.insertAux(sourceInformation)
+            writer.recordGroupSourceInformation(sourceInformation)
         }
     }
 
     @ComposeCompilerApi
     override fun sourceInformationMarkerStart(key: Int, sourceInformation: String) {
-        if (sourceInformationEnabled)
-            start(key, objectKey = null, kind = GroupKind.Group, data = sourceInformation)
+        if (inserting && sourceInformationEnabled) {
+            writer.recordGrouplessCallSourceInformationStart(key, sourceInformation)
+        }
     }
 
     @ComposeCompilerApi
     override fun sourceInformationMarkerEnd() {
-        if (sourceInformationEnabled)
-            end(isNode = false)
+        if (inserting && sourceInformationEnabled) {
+            writer.recordGrouplessCallSourceInformationEnd()
+        }
     }
 
     override fun disableSourceInformation() {
@@ -3503,6 +3514,7 @@
     private inner class CompositionContextImpl(
         override val compoundHashKey: Int,
         override val collectingParameterInformation: Boolean,
+        override val collectingSourceInformation: Boolean,
         override val observerHolder: CompositionObserverHolder?
     ) : CompositionContext() {
         var inspectionTables: MutableSet<MutableSet<CompositionData>>? = null
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
index 4b64873..ae6583c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
@@ -38,6 +38,7 @@
 abstract class CompositionContext internal constructor() {
     internal abstract val compoundHashKey: Int
     internal abstract val collectingParameterInformation: Boolean
+    internal abstract val collectingSourceInformation: Boolean
     internal open val observerHolder: CompositionObserverHolder? get() = null
 
     /**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index d75d4bf..6b38d81 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -1303,6 +1303,9 @@
     internal override val collectingParameterInformation: Boolean
         get() = false
 
+    internal override val collectingSourceInformation: Boolean
+        get() = false
+
     internal override fun recordInspectionTable(table: MutableSet<CompositionData>) {
         // TODO: The root recomposer might be a better place to set up inspection
         // than the current configuration with an CompositionLocal
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index 32a9f5c..decf811 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-@file:OptIn(InternalComposeApi::class)
-
 package androidx.compose.runtime
 
+import androidx.compose.runtime.snapshots.fastAny
 import androidx.compose.runtime.snapshots.fastFilterIndexed
 import androidx.compose.runtime.snapshots.fastForEach
 import androidx.compose.runtime.snapshots.fastMap
@@ -133,6 +132,11 @@
     internal var anchors: ArrayList<Anchor> = arrayListOf()
 
     /**
+     * A map of source information to anchor.
+     */
+    internal var sourceInformationMap: HashMap<Anchor, GroupSourceInformation>? = null
+
+    /**
      * Returns true if the slot table is empty
      */
     override val isEmpty get() = groupsSize == 0
@@ -191,7 +195,7 @@
         runtimeCheck(readers <= 0) { "Cannot start a writer when a reader is pending" }
         writer = true
         version++
-        return SlotWriter(this)
+        return SlotWriter(table = this)
     }
 
     /**
@@ -204,7 +208,7 @@
      * at [index] is still in this table.
      */
     fun anchor(index: Int): Anchor {
-        runtimeCheck(!writer) { "use active SlotWriter to create an anchor location instead " }
+        runtimeCheck(!writer) { "use active SlotWriter to create an anchor location instead" }
         require(index in 0 until groupsSize) { "Parameter index is out of range" }
         return anchors.getOrAdd(index, groupsSize) {
             Anchor(index)
@@ -212,6 +216,14 @@
     }
 
     /**
+     * Return an anchor to the given index if there is one already, null otherwise.
+     */
+    fun tryAnchor(index: Int): Anchor? {
+        runtimeCheck(!writer) { "use active SlotWriter to crate an anchor for location instead" }
+        return if (index in 0 until groupsSize) anchors.find(index, groupsSize) else null
+    }
+
+    /**
      * Return the group index for [anchor]. This [SlotTable] is assumed to own [anchor] but that
      * is not validated. If [anchor] is not owned by this [SlotTable] the result is undefined.
      * If a [SlotWriter] is open the [SlotWriter.anchorIndex] must be called instead as [anchor]
@@ -247,9 +259,22 @@
     /**
      * Close [reader].
      */
-    internal fun close(reader: SlotReader) {
+    internal fun close(
+        reader: SlotReader,
+        sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?
+    ) {
         runtimeCheck(reader.table === this && readers > 0) { "Unexpected reader close()" }
         readers--
+        if (sourceInformationMap != null) {
+            synchronized(this) {
+                val thisMap = this.sourceInformationMap
+                if (thisMap != null) {
+                    thisMap.putAll(sourceInformationMap)
+                } else {
+                    this.sourceInformationMap = sourceInformationMap
+                }
+            }
+        }
     }
 
     /**
@@ -263,11 +288,12 @@
         groupsSize: Int,
         slots: Array<Any?>,
         slotsSize: Int,
-        anchors: ArrayList<Anchor>
+        anchors: ArrayList<Anchor>,
+        sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?,
     ) {
         require(writer.table === this && this.writer) { "Unexpected writer close()" }
         this.writer = false
-        setTo(groups, groupsSize, slots, slotsSize, anchors)
+        setTo(groups, groupsSize, slots, slotsSize, anchors, sourceInformationMap)
     }
 
     /**
@@ -279,7 +305,8 @@
         groupsSize: Int,
         slots: Array<Any?>,
         slotsSize: Int,
-        anchors: ArrayList<Anchor>
+        anchors: ArrayList<Anchor>,
+        sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?,
     ) {
         // Adopt the slots from the writer
         this.groups = groups
@@ -287,6 +314,7 @@
         this.slots = slots
         this.slotsSize = slotsSize
         this.anchors = anchors
+        this.sourceInformationMap = sourceInformationMap
     }
 
     /**
@@ -357,6 +385,10 @@
         return groupsSize > 0 && groups.containsMark(0)
     }
 
+    fun sourceInformationOf(group: Int) = sourceInformationMap?.let { map ->
+        tryAnchor(group)?.let { anchor -> map[anchor] }
+    }
+
     /**
      * Find the nearest recompose scope for [group] that, when invalidated, will cause [group]
      * group to be recomposed.
@@ -376,7 +408,7 @@
 
     /**
      * Finds the nearest recompose scope to the provided group and invalidates it. Return
-     * true if the invalidation will cause the scope to reccompose, otherwise false which will
+     * true if the invalidation will cause the scope to recompose, otherwise false which will
      * require forcing recomposition some other way.
      */
     private fun invalidateGroup(group: Int): Boolean {
@@ -487,6 +519,35 @@
             require(lastLocation < location) { "Anchor is out of order" }
             lastLocation = location
         }
+
+        // Verify source information is well-formed
+        fun verifySourceGroup(group: GroupSourceInformation) {
+            group.groups?.fastForEach { item ->
+                when (item) {
+                    is Anchor -> {
+                        require(item.valid) {
+                            "Source map contains invalid anchor"
+                        }
+                        require(ownsAnchor(item)) {
+                            "Source map anchor is not owned by the slot table"
+                        }
+                    }
+                    is GroupSourceInformation -> verifySourceGroup(item)
+                }
+            }
+        }
+
+        sourceInformationMap?.let { sourceInformationMap ->
+            for ((anchor, sourceGroup) in sourceInformationMap) {
+                require(anchor.valid) {
+                    "Source map contains invalid anchor"
+                }
+                require(ownsAnchor(anchor)) {
+                    "Source map anchor is not owned by the slot table"
+                }
+                verifySourceGroup(sourceGroup)
+            }
+        }
     }
 
     /**
@@ -518,7 +579,21 @@
         repeat(level) { append(' ') }
         append("Group(")
         append(index)
-        append(") key=")
+        append(")")
+        tryAnchor(index)?.let { anchor ->
+            sourceInformationMap?.get(anchor)?.let { groupInformation ->
+                groupInformation.sourceInformation?.let {
+                    if (it.startsWith("C(") || it.startsWith("CC(")) {
+                        val start = it.indexOf("(") + 1
+                        val endParen = it.indexOf(')')
+                        append(" ")
+                        append(it.substring(start, endParen))
+                        append("()")
+                    }
+                }
+            }
+        }
+        append(" key=")
         append(groups.key(index))
         fun dataIndex(index: Int) =
             if (index >= groupsSize) slotsSize else groups.dataAnchor(index)
@@ -635,6 +710,105 @@
     val valid get() = location != Int.MIN_VALUE
     fun toIndexFor(slots: SlotTable) = slots.anchorIndex(this)
     fun toIndexFor(writer: SlotWriter) = writer.anchorIndex(this)
+
+    override fun toString(): String {
+        return "${super.toString()}{ location = $location }"
+    }
+}
+
+internal class GroupSourceInformation(val key: Int, var sourceInformation: String?) {
+    var groups: ArrayList<Any /* Anchor | GroupSourceInformation */>? = null
+    var closed = false
+
+    fun startGrouplessCall(key: Int, sourceInformation: String) {
+        openInformation().add(GroupSourceInformation(key, sourceInformation))
+    }
+
+    fun endGrouplessCall() { openInformation().close() }
+
+    fun reportGroup(writer: SlotWriter, group: Int) {
+        openInformation().add(writer.anchor(group))
+    }
+
+    fun reportGroup(table: SlotTable, group: Int) {
+        openInformation().add(table.anchor(group))
+    }
+
+    fun addGroupAfter(writer: SlotWriter, predecessor: Int, group: Int) {
+        val groups = groups ?: ArrayList<Any>().also { groups = it }
+        val index = if (predecessor >= 0) {
+            val anchor = writer.tryAnchor(predecessor)
+            if (anchor != null) {
+                groups.fastIndexOf {
+                    it == anchor ||
+                        (it is GroupSourceInformation && it.hasAnchor(anchor))
+                }
+            } else 0
+        } else 0
+        groups.add(index, writer.anchor(group))
+    }
+
+    fun close() { closed = true }
+
+    // Return the current open nested source information or this.
+    private fun openInformation(): GroupSourceInformation =
+        (groups?.let {
+            groups -> groups.fastLastOrNull { it is GroupSourceInformation && !it.closed }
+        } as? GroupSourceInformation)?.openInformation() ?: this
+
+    private fun add(group: Any /* Anchor | GroupSourceInformation */) {
+        val groups = groups ?: ArrayList()
+        this.groups = groups
+        groups.add(group)
+    }
+
+    private fun hasAnchor(anchor: Anchor): Boolean =
+        groups?.fastAny {
+            it == anchor || (it is GroupSourceInformation && hasAnchor(anchor))
+        } == true
+
+    fun removeAnchor(anchor: Anchor): Boolean {
+        val groups = groups
+        if (groups != null) {
+            var index = groups.size - 1
+            while (index >= 0) {
+                when (val item = groups[index]) {
+                    is Anchor -> if (item == anchor) groups.removeAt(index)
+                    is GroupSourceInformation -> if (!item.removeAnchor(anchor)) {
+                        groups.removeAt(index)
+                    }
+                }
+                index--
+            }
+            if (groups.isEmpty()) {
+                this.groups = null
+                return false
+            }
+            return true
+        }
+        return true
+    }
+}
+
+private inline fun <T> ArrayList<T>.fastLastOrNull(predicate: (T) -> Boolean): T? {
+    var index = size - 1
+    while (index >= 0) {
+        val value = get(index)
+        if (predicate(value)) return value
+        index--
+    }
+    return null
+}
+
+private inline fun <T> ArrayList<T>.fastIndexOf(predicate: (T) -> Boolean): Int {
+    var index = 0
+    val size = size
+    while (index < size) {
+        val value = get(index)
+        if (predicate(value)) return index
+        index++
+    }
+    return -1
 }
 
 /**
@@ -668,6 +842,12 @@
     private val slotsSize: Int = table.slotsSize
 
     /**
+     * A local copy of the [sourceInformationMap] being created to be merged into [table]
+     * when the reader closes.
+     */
+    private var sourceInformationMap: HashMap<Anchor, GroupSourceInformation>? = null
+
+    /**
      * True if the reader has been closed
      */
     var closed: Boolean = false
@@ -927,7 +1107,7 @@
      */
     fun close() {
         closed = true
-        table.close(this)
+        table.close(this, sourceInformationMap)
     }
 
     /**
@@ -935,14 +1115,17 @@
      */
     fun startGroup() {
         if (emptyCount <= 0) {
+            val parent = parent
+            val currentGroup = currentGroup
             require(groups.parentAnchor(currentGroup) == parent) { "Invalid slot table detected" }
-            parent = currentGroup
+            sourceInformationMap?.get(anchor(parent))?.reportGroup(table, currentGroup)
+            this.parent = currentGroup
             currentEnd = currentGroup + groups.groupSize(currentGroup)
-            val current = currentGroup++
-            currentSlot = groups.slotAnchor(current)
-            currentSlotEnd = if (current >= groupsSize - 1)
+            this.currentGroup = currentGroup + 1
+            currentSlot = groups.slotAnchor(currentGroup)
+            currentSlotEnd = if (currentGroup >= groupsSize - 1)
                 slotsSize else
-                groups.dataAnchor(current + 1)
+                groups.dataAnchor(currentGroup + 1)
         }
     }
 
@@ -1023,6 +1206,20 @@
         }
     }
 
+    fun recordGroupSourceInformation(sourceInformation: String) {
+        val map = sourceInformationMap ?: HashMap()
+        this.sourceInformationMap = map
+        map[anchor(parent)] = GroupSourceInformation(0, sourceInformation)
+    }
+
+    fun recordGrouplessCallSourceInformationStart(key: Int, sourceInformation: String) {
+        sourceInformationMap?.get(tryAnchor(parent))?.startGrouplessCall(key, sourceInformation)
+    }
+
+    fun recordGrouplessCallSourceInformationEnd() {
+        sourceInformationMap?.get(tryAnchor(parent))?.endGrouplessCall()
+    }
+
     /**
      * Extract the keys from this point to the end of the group. The current is left unaffected.
      * Must be called inside a group.
@@ -1057,6 +1254,11 @@
         Anchor(index)
     }
 
+    /**
+     * Return an anchor if one has already been created, null otherwise.
+     */
+    private fun tryAnchor(index: Int) = table.anchors.find(index, groupsSize)
+
     private fun IntArray.node(index: Int) = if (isNode(index)) {
         slots[nodeIndex(index)]
     } else Composer.Empty
@@ -1126,11 +1328,16 @@
     private var slots: Array<Any?> = table.slots
 
     /**
-     * A copy of the [SlotWriter.anchors] to avoid having to index through [table].
+     * A copy of the [SlotTable.anchors] to avoid having to index through [table].
      */
     private var anchors: ArrayList<Anchor> = table.anchors
 
     /**
+     * A copy of [SlotTable.sourceInformationMap] to avoid having to index through [table]
+     */
+    private var sourceInformationMap = table.sourceInformationMap
+
+    /**
      * Group index of the start of the gap in the groups array.
      */
     private var groupGapStart: Int = table.groupsSize
@@ -1340,7 +1547,8 @@
             groupsSize = groupGapStart,
             slots = slots,
             slotsSize = slotsGapStart,
-            anchors = anchors
+            anchors = anchors,
+            sourceInformationMap = sourceInformationMap,
         )
     }
 
@@ -1411,6 +1619,49 @@
         currentSlot++
     }
 
+    fun recordGroupSourceInformation(sourceInformation: String) {
+        if (insertCount > 0) {
+            groupSourceInformationFor(parent, sourceInformation)
+        }
+    }
+
+    fun recordGrouplessCallSourceInformationStart(key: Int, value: String) {
+        if (insertCount > 0) {
+            groupSourceInformationFor(parent, null)?.startGrouplessCall(key, value)
+        }
+    }
+
+    fun recordGrouplessCallSourceInformationEnd() {
+        if (insertCount > 0) {
+            groupSourceInformationFor(parent, null)?.endGrouplessCall()
+        }
+    }
+
+    private fun groupSourceInformationFor(
+        parent: Int,
+        sourceInformation: String?
+    ): GroupSourceInformation? {
+        val map = sourceInformationMap ?: HashMap()
+        this.sourceInformationMap = map
+        return map.getOrPut(anchor(parent)) {
+            val result = GroupSourceInformation(0, sourceInformation)
+
+            // If we called from a groupless call then the groups added before this call
+            // are not reflected in this group information so they need to be added now
+            // if they exist.
+            if (sourceInformation == null) {
+                var child = parent + 1
+                val end = currentGroup
+                while (child < end) {
+                    result.reportGroup(this, child)
+                    child += groups.groupSize(child)
+                }
+            }
+
+            result
+        }
+    }
+
     /**
      * Updates the node for the current node group to [value].
      */
@@ -1601,6 +1852,7 @@
     fun startData(key: Int, aux: Any?) = startGroup(key, Composer.Empty, isNode = false, aux = aux)
 
     private fun startGroup(key: Int, objectKey: Any?, isNode: Boolean, aux: Any?) {
+        val previousParent = parent
         val inserting = insertCount > 0
         nodeCountStack.push(nodeCount)
 
@@ -1637,9 +1889,11 @@
             val newCurrent = current + 1
             this.parent = current
             this.currentGroup = newCurrent
+            if (previousParent >= 0) {
+                sourceInformationOf(previousParent)?.reportGroup(this, current)
+            }
             newCurrent
         } else {
-            val previousParent = parent
             startStack.push(previousParent)
             saveCurrentGroupEnd()
             val currentGroup = currentGroup
@@ -1740,7 +1994,7 @@
     internal fun bashGroup() {
         startGroup()
         while (!isGroupEnd) {
-            insertParentGroup(-3)
+            bashParentGroup()
             skipGroup()
         }
         endGroup()
@@ -1799,6 +2053,13 @@
         val oldSlot = currentSlot
         val count = skipGroup()
 
+        // Remove the group from its parent information
+        sourceInformationOf(parent)?.let { sourceInformation ->
+            tryAnchor(oldGroup)?.let { anchor ->
+                sourceInformation.removeAnchor(anchor)
+            }
+        }
+
         // Remove any recalculate markers ahead of this delete as they are in the group
         // that is being deleted.
         pendingRecalculateMarks?.let {
@@ -2080,6 +2341,42 @@
                 anchors
             } else emptyList()
 
+            // Move any source information from the source table to the destination table
+            if (anchors.isNotEmpty()) {
+                val sourceSourceInformationMap = fromWriter.sourceInformationMap
+                if (sourceSourceInformationMap != null) {
+                    var destinationSourceInformation = toWriter.sourceInformationMap
+                    anchors.fastForEach { anchor ->
+                        val information = sourceSourceInformationMap[anchor]
+                        if (information != null) {
+                            sourceSourceInformationMap.remove(anchor)
+                            val map = destinationSourceInformation ?: run {
+                                val map = HashMap<Anchor, GroupSourceInformation>()
+                                destinationSourceInformation = map
+                                toWriter.sourceInformationMap = destinationSourceInformation
+                                map
+                            }
+                            map[anchor] = information
+                        }
+                    }
+                    if (sourceSourceInformationMap.isEmpty()) {
+                        fromWriter.sourceInformationMap = null
+                    }
+                }
+            }
+
+            // Record the new group in the parent information
+            val toWriterParent = toWriter.parent
+            toWriter.sourceInformationOf(parent)?.let {
+                var predecessor = -1
+                var child = toWriterParent + 1
+                val endGroup = toWriter.currentGroup
+                while (child < endGroup) {
+                    predecessor = child
+                    child += toWriter.groups.groupSize(child)
+                }
+                it.addGroupAfter(toWriter, predecessor, endGroup)
+            }
             val parentGroup = fromWriter.parent(fromIndex)
             val anchorsRemoved = if (!removeSourceGroup) {
                 // e.g.: we can skip groups removal for insertTable of Composer because
@@ -2131,6 +2428,7 @@
             if (hasMarks) {
                 toWriter.updateContainsMark(parent)
             }
+
             return anchors
         }
     }
@@ -2205,10 +2503,12 @@
             val myGroups = groups
             val mySlots = slots
             val myAnchors = anchors
+            val mySourceInformation = sourceInformationMap
             val groups = table.groups
             val groupsSize = table.groupsSize
             val slots = table.slots
             val slotsSize = table.slotsSize
+            val sourceInformation = table.sourceInformationMap
             this.groups = groups
             this.slots = slots
             this.anchors = table.anchors
@@ -2217,8 +2517,9 @@
             this.slotsGapStart = slotsSize
             this.slotsGapLen = slots.size - slotsSize
             this.slotsGapOwner = groupsSize
+            this.sourceInformationMap = sourceInformation
 
-            table.setTo(myGroups, 0, mySlots, 0, myAnchors)
+            table.setTo(myGroups, 0, mySlots, 0, myAnchors, mySourceInformation)
             return this.anchors
         }
 
@@ -2239,7 +2540,8 @@
      * all remaining children of the current group will be parented by a new group and the
      * [currentSlot] will be moved to after the group inserted.
      */
-    fun insertParentGroup(key: Int) {
+    private fun bashParentGroup() {
+        val key = -3
         runtimeCheck(insertCount == 0) { "Writer cannot be inserting" }
         if (isGroupEnd) {
             beginInsert()
@@ -2282,6 +2584,11 @@
             addToGroupSizeAlongSpine(parentAddress, 1)
             fixParentAnchorsFor(parent, currentGroupEnd, currentGroup)
             this.currentGroup = currentGroupEnd
+
+            // Remove any source information for child groups in the bashed group as updating it
+            // will not work as the children are now separated by a group. Just clearing the list
+            // is sufficient as list will be rebuilt when the new content is generated.
+            sourceInformationOf(parent)?.let { group -> group.groups = null }
         }
     }
 
@@ -2693,7 +3000,9 @@
 
             // Move the gap to start of the removal and grow the gap
             moveGroupGapTo(start)
-            if (anchors.isNotEmpty()) anchorsRemoved = removeAnchors(start, len)
+            if (anchors.isNotEmpty()) {
+                anchorsRemoved = removeAnchors(start, len, sourceInformationMap)
+            }
             groupGapStart = start
             val previousGapLen = groupGapLen
             val newGapLen = previousGapLen + len
@@ -2707,14 +3016,25 @@
             }
             if (currentGroupEnd >= groupGapStart) currentGroupEnd -= len
 
+            val parent = parent
             // Update markers if necessary
             if (containsGroupMark(parent)) {
                 updateContainsMark(parent)
             }
+
+            // Remove the group from its parent source information
             anchorsRemoved
         } else false
     }
 
+    private fun sourceInformationOf(group: Int): GroupSourceInformation? =
+        sourceInformationMap?.let { map ->
+            tryAnchor(group)?.let { anchor -> map[anchor] }
+        }
+
+    internal fun tryAnchor(group: Int) =
+        if (group in 0 until size) anchors.find(group, size) else null
+
     /**
      * Remove [len] slots from [start].
      */
@@ -2782,7 +3102,11 @@
     /**
      * A helper function to remove the anchors for groups that are removed.
      */
-    private fun removeAnchors(gapStart: Int, size: Int): Boolean {
+    private fun removeAnchors(
+        gapStart: Int,
+        size: Int,
+        sourceInformationMap: HashMap<Anchor, GroupSourceInformation>?
+    ): Boolean {
         val gapLen = groupGapLen
         val removeEnd = gapStart + size
         val groupsSize = capacity - gapLen
@@ -2797,6 +3121,7 @@
             if (location >= gapStart) {
                 if (location < removeEnd) {
                     anchor.location = Int.MIN_VALUE
+                    sourceInformationMap?.remove(anchor)
                     removeAnchorStart = index
                     if (removeAnchorEnd == 0) removeAnchorEnd = index + 1
                 }
@@ -3037,7 +3362,9 @@
     override val sourceInfo: String?
         get() = if (table.groups.hasAux(group))
             table.slots[table.groups.auxIndex(group)] as? String
-        else null
+        else table.tryAnchor(group)?.let {
+            table.sourceInformationMap?.get(it)?.sourceInformation
+        }
 
     override val node: Any?
         get() = if (table.groups.isNode(group))
@@ -3056,11 +3383,12 @@
 
     override fun iterator(): Iterator<CompositionGroup> {
         validateRead()
-        return GroupIterator(
-            table,
-            group + 1,
-            group + table.groups.groupSize(group)
-        )
+        return table.sourceInformationOf(group)?.let { SourceInformationGroupIterator(table, it) }
+            ?: GroupIterator(
+                table,
+                group + 1,
+                group + table.groups.groupSize(group)
+            )
     }
 
     override val groupSize: Int get() = table.groups.groupSize(group)
@@ -3090,6 +3418,21 @@
         }
 }
 
+private class SourceInformationSlotTableGroup(
+    val table: SlotTable,
+    val sourceInformation: GroupSourceInformation
+) : CompositionGroup, Iterable<CompositionGroup> {
+    override val key: Any = sourceInformation.key
+    override val sourceInfo: String? get() = sourceInformation.sourceInformation
+    override val node: Any? get() = null
+    override val data: Iterable<Any?> = emptyList()
+    override val compositionGroups: Iterable<CompositionGroup> = this
+    override val isEmpty: Boolean
+        get() = sourceInformation.groups?.isEmpty() != false
+    override fun iterator(): Iterator<CompositionGroup> =
+        SourceInformationGroupIterator(table, sourceInformation)
+}
+
 private class GroupIterator(
     val table: SlotTable,
     start: Int,
@@ -3121,7 +3464,7 @@
 
 private class DataIterator(
     val table: SlotTable,
-    val group: Int,
+    group: Int,
 ) : Iterable<Any?>, Iterator<Any?> {
     val start = table.groups.dataAnchor(group)
     val end = if (group + 1 < table.groupsSize)
@@ -3136,6 +3479,22 @@
     ).also { index++ }
 }
 
+private class SourceInformationGroupIterator(
+    val table: SlotTable,
+    val group: GroupSourceInformation,
+) : Iterator<CompositionGroup> {
+    private val version = table.version
+    private var index = 0
+    override fun hasNext(): Boolean = group.groups?.let { index < it.size } ?: false
+    override fun next(): CompositionGroup {
+        return when (val group = group.groups?.get(index++)) {
+            is Anchor -> SlotTableGroup(table, group.location, version)
+            is GroupSourceInformation -> SourceInformationSlotTableGroup(table, group)
+            else -> composeRuntimeError("Unexpected group information structure")
+        }
+    }
+}
+
 // Parent -1 is reserved to be the root parent index so the anchor must pivot on -2.
 private const val parentAnchorPivot = -2
 
@@ -3364,6 +3723,11 @@
     } else get(location)
 }
 
+private fun ArrayList<Anchor>.find(index: Int, effectiveSize: Int): Anchor? {
+    val location = search(index, effectiveSize)
+    return if (location >= 0) get(location) else null
+}
+
 /**
  * This is inlined here instead to avoid allocating a lambda for the compare when this is used.
  */
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
index 0ed6a87..dd8461b 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionAndDerivedStateTests.kt
@@ -252,6 +252,8 @@
             }
         }
 
+        verifyConsistent()
+
         expect(useD)
 
         // Modify A
@@ -265,6 +267,7 @@
             use = newUse
             a++
             expectChanges()
+            verifyConsistent()
             revalidate()
             expect(newUse, previous)
         }
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
index 333289b..4fc09ce 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-@file:OptIn(InternalComposeApi::class)
 package androidx.compose.runtime
 
+import androidx.compose.runtime.snapshots.fastForEach
+import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
 import kotlin.random.Random
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -24,7 +25,6 @@
 import kotlin.test.assertSame
 import kotlin.test.assertTrue
 
-@OptIn(InternalComposeApi::class)
 class SlotTableTests {
     @Test
     fun testCanCreate() {
@@ -3910,6 +3910,445 @@
     }
 
     @Test
+    fun canReportNonGroupCallInformationDuringWrite() {
+        val slots = SlotTable()
+        slots.write { writer ->
+            writer.insert {
+                writer.group(100) {
+                    writer.group(200, "C(200)") {
+                        writer.grouplessCall(300, "C(300)") { }
+                        writer.grouplessCall(301, "C(301)") { }
+                        writer.group(302, "C(302)") { }
+                        writer.grouplessCall(303, "C(303)") { }
+                        writer.group(304, "C(304)") { }
+                        writer.grouplessCall(305, "C(305)") {
+                            writer.group(400, "C(400)") { }
+                            writer.group(401, "C(401)") { }
+                        }
+                        writer.grouplessCall(306, "C(306)") {
+                            writer.group(402, "C(402)") { }
+                            writer.grouplessCall(403, "C(403)") {
+                                writer.group(500, "C(500)") { }
+                                writer.group(501, "C(501)") { }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expectedRoot = SourceGroup.group(100) {
+            group(200, "C(200)") {
+                group(300, "C(300)") { }
+                group(301, "C(301)") { }
+                group(302, "C(302)") { }
+                group(303, "C(303)") { }
+                group(304, "C(304)") { }
+                group(305, "C(305)") {
+                    group(400, "C(400)") { }
+                    group(401, "C(401)") { }
+                }
+                group(306, "C(306)") {
+                    group(402, "C(402)") { }
+                    group(403, "C(403)") {
+                        group(500, "C(500)") { }
+                        group(501, "C(501)") { }
+                    }
+                }
+            }
+        }
+        val slotsRoot = SourceGroup.group(slots)
+        assertEquals(expectedRoot, slotsRoot)
+    }
+
+    @Test
+    fun canMoveSourceInformationFromAnotherTable() {
+        val sourceTable = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(200, "C(200)") {
+                            grouplessCall(300, "C(300)") {
+                                group(400, "C(400)") { }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        sourceTable.verifyWellFormed()
+
+        val mainTable = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(201, "C(201)") { }
+                        }
+                    }
+                }
+            }
+        }
+        mainTable.verifyWellFormed()
+
+        mainTable.write { writer ->
+            with(writer) {
+                group {
+                    insert {
+                        moveFrom(sourceTable, 0)
+                    }
+                    skipToGroupEnd()
+                }
+            }
+        }
+        mainTable.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(200, "C(200)") {
+                group(300, "C(300)") {
+                    group(400, "C(400)") { }
+                }
+            }
+            group(201, "C(201)") { }
+        }
+        val received = SourceGroup.group(mainTable)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canMoveSourceInformationIntoAGroupWithSourceInformation() {
+        val sourceTable = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(300, "C(300)") {
+                            grouplessCall(400, "C(400)") {
+                                group(500, "C(500)") { }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        sourceTable.verifyWellFormed()
+
+        val mainTable = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(201, "C(201)") { }
+                        }
+                    }
+                }
+            }
+        }
+        mainTable.verifyWellFormed()
+
+        mainTable.write { writer ->
+            with(writer) {
+                group {
+                    group(201) {
+                        insert {
+                            moveFrom(sourceTable, 0)
+                        }
+                        skipToGroupEnd()
+                    }
+                    skipToGroupEnd()
+                }
+            }
+        }
+        mainTable.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(201, "C(201)") {
+                group(300, "C(300)") {
+                    group(400, "C(400)") {
+                        group(500, "C(500)") { }
+                    }
+                }
+            }
+        }
+        val received = SourceGroup.group(mainTable)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canRemoveAGroupBeforeAnEmptyGrouplessCall() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200, "C(2001)") { }
+                            grouplessCall(201, "C(201)") { }
+                            group(202, "C(202)") { }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        slots.write { writer ->
+            with(writer) {
+                group {
+                    removeGroup()
+                    skipToGroupEnd()
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(201, "C(201)") { }
+            group(202, "C(202)") { }
+        }
+        val received = SourceGroup.group(slots)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canRemoveAGroupAfterAnEmptyGrouplessCall() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200, "C(200)") { }
+                            grouplessCall(201, "C(201)") { }
+                            group(202, "C(202)") { }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        slots.write { writer ->
+            with(writer) {
+                group {
+                    skipGroup()
+                    removeGroup()
+                    skipToGroupEnd()
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(200, "C(200)") { }
+            group(201, "C(201)") { }
+        }
+        val received = SourceGroup.group(slots)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canRemoveAGroupProducedInAGrouplessCall() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200, "C(200)") { }
+                            grouplessCall(201, "C(201)") {
+                                group(300, "C(300)") { }
+                            }
+                            group(202, "C(202)") { }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        slots.write { writer ->
+            with(writer) {
+                group {
+                    skipGroup()
+                    removeGroup()
+                    skipToGroupEnd()
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(200, "C(200)") { }
+            group(202, "C(202)") { }
+        }
+        val received = SourceGroup.group(slots)
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canRemoveAGroupWithSourceInformation() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200, "C(200)") { }
+                            group(201, "C(201)") { }
+                            group(202, "C(202)") {
+                                grouplessCall(300, "C(300)") {
+                                    group(400, "C(400)") {
+                                        group(500, "C(500)") { }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        slots.write { writer ->
+            with(writer) {
+                group(100) {
+                    skipGroup()
+                    skipGroup()
+                    group {
+                        group {
+                            removeGroup() // Remove group 500
+                            skipToGroupEnd()
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val expected = SourceGroup.group(100) {
+            group(200, "C(200)") { }
+            group(201, "C(201)") { }
+            group(202, "C(202)") {
+                group(300, "C(300)") {
+                    group(400, "C(400)") { }
+                }
+            }
+        }
+        val received = SourceGroup.group(slots)
+
+        assertEquals(expected, received)
+    }
+
+    @Test
+    fun canAddSourceInformationUsingAReader() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200) { }
+                            group(201) { }
+                            group(202) {
+                                group(400) {
+                                    group(500) { }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        slots.verifyWellFormed()
+
+        val initialRootActual = SourceGroup.group(slots)
+        val initialRootExpected = SourceGroup.group(100) {
+            group(200) { }
+            group(201) { }
+            group(202) {
+                group(400) {
+                    group(500) { }
+                }
+            }
+        }
+        assertEquals(initialRootExpected, initialRootActual)
+
+        slots.read { reader ->
+            with(reader) {
+                group(100) {
+                    group(200, "C(200)") { }
+                    group(201, "C(201)") { }
+                    group(202, "C(202)") {
+                        grouplessCall(300, "C(300)") {
+                            group(400, "C(400)") {
+                                group(500, "C(500)") { }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        val updatedRootActual = SourceGroup.group(slots)
+        val updatedRootExpected = SourceGroup.group(100) {
+            group(200, "C(200)") { }
+            group(201, "C(201)") { }
+            group(202, "C(202)") {
+                group(300, "C(300)") {
+                    group(400, "C(400)") {
+                        group(500, "C(500)") { }
+                    }
+                }
+            }
+        }
+        assertEquals(updatedRootExpected, updatedRootActual)
+    }
+
+    @Test
+    fun canAddAGrouplessCallToAGroupWithNoSourceInformation() {
+        val slots = SlotTable().apply {
+            write { writer ->
+                with(writer) {
+                    insert {
+                        group(100) {
+                            group(200) {
+                                group(300, "C(300)") { }
+                                group(301, "C(301)") { }
+                                grouplessCall(302, "C(302)") {
+                                    group(400, "C(400)") { }
+                                }
+                            }
+                            group(201, "C(201)") {
+                                group(303) {
+                                    group(401, "C(401)") { }
+                                    grouplessCall(402, "C(402)") { }
+                                    group(403, "C(403)") { }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        val expected = SourceGroup.group(100) {
+            group(200) {
+                group(300, "C(300)") { }
+                group(301, "C(301)") { }
+                group(302, "C(302)") {
+                    group(400, "C(400)") { }
+                }
+            }
+            group(201, "C(201)") {
+                group(303) {
+                    group(401, "C(401)") { }
+                    group(402, "C(402)") { }
+                    group(403, "C(403)") { }
+                }
+            }
+        }
+        val received = SourceGroup.group(slots)
+
+        assertEquals(expected, received)
+    }
+
+    @Test
     fun canMoveAGroupFromATableIntoAnotherGroupAndModifyThatGroup() {
         val slots = SlotTable()
         var insertAnchor = Anchor(-1)
@@ -4013,34 +4452,47 @@
 private fun SlotWriter.startNode(key: Any?, node: Any?) =
     startNode(NodeKey, key, node)
 
-@OptIn(InternalComposeApi::class)
 internal inline fun SlotWriter.group(block: () -> Unit) {
     startGroup()
     block()
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 internal inline fun SlotWriter.group(key: Int, block: () -> Unit) {
     startGroup(key)
     block()
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
+internal inline fun SlotWriter.group(key: Int, sourceInformation: String, block: () -> Unit) {
+    group(key) {
+        recordGroupSourceInformation(sourceInformation)
+        block()
+    }
+}
+
+internal inline fun SlotWriter.grouplessCall(
+    key: Int,
+    sourceInformation: String,
+    block: () -> Unit
+) {
+    recordGrouplessCallSourceInformationStart(key, sourceInformation)
+    block()
+    recordGrouplessCallSourceInformationEnd()
+}
+
 internal inline fun SlotWriter.nodeGroup(key: Int, node: Any, block: () -> Unit = { }) {
     startNode(NodeKey, key, node)
     block()
     endGroup()
 }
-@OptIn(InternalComposeApi::class)
+
 internal inline fun SlotWriter.insert(block: () -> Unit) {
     beginInsert()
     block()
     endInsert()
 }
 
-@OptIn(InternalComposeApi::class)
 internal inline fun SlotReader.group(key: Int, block: () -> Unit) {
     assertEquals(key, groupKey)
     startGroup()
@@ -4048,14 +4500,30 @@
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
+internal inline fun SlotReader.group(key: Int, sourceInformation: String, block: () -> Unit) {
+    assertEquals(key, groupKey)
+    startGroup()
+    recordGroupSourceInformation(sourceInformation)
+    block()
+    endGroup()
+}
+
+internal inline fun SlotReader.grouplessCall(
+    key: Int,
+    sourceInformation: String,
+    block: () -> Unit
+) {
+    recordGrouplessCallSourceInformationStart(key, sourceInformation)
+    block()
+    recordGrouplessCallSourceInformationEnd()
+}
+
 internal inline fun SlotReader.group(block: () -> Unit) {
     startGroup()
     block()
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 private inline fun SlotReader.expectNode(key: Int, node: Any, block: () -> Unit = { }) {
     assertEquals(key, groupObjectKey)
     assertEquals(node, groupNode)
@@ -4067,7 +4535,6 @@
 private const val treeRoot = -1
 private const val elementKey = 100
 
-@OptIn(InternalComposeApi::class)
 private fun testSlotsNumbered(): SlotTable {
     val slotTable = SlotTable()
     slotTable.write { writer ->
@@ -4084,7 +4551,6 @@
 }
 
 // Creates 0 until 10 items each with 10 elements numbered 0...n with 0..n slots
-@OptIn(InternalComposeApi::class)
 private fun testItems(): SlotTable {
     val slots = SlotTable()
     slots.write { writer ->
@@ -4121,7 +4587,6 @@
     return slots
 }
 
-@OptIn(InternalComposeApi::class)
 private fun validateItems(slots: SlotTable) {
     slots.read { reader ->
         check(reader.groupKey == treeRoot) { "Invalid root key" }
@@ -4172,7 +4637,6 @@
     }
 }
 
-@OptIn(InternalComposeApi::class)
 private fun narrowTrees(): Pair<SlotTable, List<Anchor>> {
     val slots = SlotTable()
     val anchors = mutableListOf<Anchor>()
@@ -4221,13 +4685,11 @@
     return slots to anchors
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.expectGroup(key: Int): Int {
     assertEquals(key, groupKey)
     return skipGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.expectGroup(
     key: Int,
     block: () -> Unit
@@ -4238,12 +4700,10 @@
     endGroup()
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.expectData(value: Any) {
     assertEquals(value, next())
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.expectGroup(
     key: Int,
     objectKey: Any?,
@@ -4280,3 +4740,43 @@
         "Expected test to throw an exception containing \"$message\""
     )
 }
+
+data class SourceGroup(val key: Any, val source: String?, val children: List<SourceGroup>) {
+
+    override fun toString(): String = buildString { toStringBuilder(this, 0) }
+
+    private fun toStringBuilder(builder: StringBuilder, indent: Int) {
+        repeat(indent) { builder.append(' ') }
+        builder.append("Group(")
+        builder.append(key)
+        builder.append(")")
+        if (source != null) {
+            builder.append(' ')
+            builder.append(source)
+        }
+        builder.appendLine()
+        children.fastForEach { it.toStringBuilder(builder, indent + 2) }
+    }
+
+    data class BuilderScope(private val children: ArrayList<SourceGroup> = ArrayList()) {
+        fun group(key: Int, source: String? = null, block: BuilderScope.() -> Unit) {
+            val scope = BuilderScope()
+            scope.block()
+            this.children.add(SourceGroup(key, source, scope.children))
+        }
+    }
+
+    companion object {
+        fun group(key: Int, block: BuilderScope.() -> Unit): SourceGroup {
+            val children = ArrayList<SourceGroup>()
+            val scope = BuilderScope(children)
+            scope.block()
+            return SourceGroup(key, null, children)
+        }
+
+        fun group(compositionData: CompositionData): SourceGroup =
+            groupOf(compositionData.compositionGroups.first())
+        private fun groupOf(group: CompositionGroup): SourceGroup =
+            SourceGroup(group.key, group.sourceInfo, group.compositionGroups.map(::groupOf))
+    }
+}
diff --git a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/LiveEditTests.kt b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/LiveEditTests.kt
index dcb954f..7fc505e 100644
--- a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/LiveEditTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/LiveEditTests.kt
@@ -102,7 +102,9 @@
     }
 
     @Test
-    fun testNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
+    fun testNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest(
+        collectSourceInformation = SourceInfo.None
+    ) {
         EnsureStatePreservedButRecomposed("a")
         RestartGroup {
             Text("Hello World")
@@ -112,7 +114,9 @@
     }
 
     @Test
-    fun testMultipleNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
+    fun testMultipleNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest(
+        collectSourceInformation = SourceInfo.None
+    ) {
         RestartGroup {
             EnsureStatePreservedButRecomposed("a")
             Target("b", restartable = false)
@@ -136,7 +140,9 @@
     }
 
     @Test
-    fun testInlineComposableLambda() = liveEditTest {
+    fun testInlineComposableLambda() = liveEditTest(
+        collectSourceInformation = SourceInfo.None
+    ) {
         RestartGroup {
             InlineTarget("a")
             EnsureStatePreservedButRecomposed("b")
@@ -165,7 +171,10 @@
     @Test
     fun testThrowing_recomposition() {
         var recomposeCount = 0
-        liveEditTest(reloadCount = 2) {
+        liveEditTest(
+            reloadCount = 2,
+            collectSourceInformation = SourceInfo.None,
+        ) {
             RestartGroup {
                 MarkAsTarget()
 
@@ -216,7 +225,9 @@
     @Test
     fun testThrowing_recomposition_sideEffect() {
         var recomposeCount = 0
-        liveEditTest {
+        liveEditTest(
+            collectSourceInformation = SourceInfo.None
+        ) {
             RestartGroup {
                 MarkAsTarget()
 
@@ -286,7 +297,9 @@
     @Test
     fun testThrowing_recomposition_remembered() {
         var recomposeCount = 0
-        liveEditTest {
+        liveEditTest(
+            collectSourceInformation = SourceInfo.None,
+        ) {
             RestartGroup {
                 MarkAsTarget()
 
@@ -333,7 +346,10 @@
     fun testThrowing_invalidationsCarriedAfterCrash() {
         var recomposeCount = 0
         val state = mutableStateOf(0)
-        liveEditTest(reloadCount = 2) {
+        liveEditTest(
+            reloadCount = 2,
+            collectSourceInformation = SourceInfo.None,
+        ) {
             RestartGroup {
                 RestartGroup {
                     MarkAsTarget()
@@ -391,7 +407,10 @@
     @Test
     fun testThrowing_movableContent_recomposition() {
         var recomposeCount = 0
-        liveEditTest(reloadCount = 2) {
+        liveEditTest(
+            reloadCount = 2,
+            collectSourceInformation = SourceInfo.None,
+        ) {
             RestartGroup {
                 MarkAsTarget()
 
@@ -423,7 +442,10 @@
     @Test
     fun testThrowing_movableContent_throwAfterMove() {
         var recomposeCount = 0
-        liveEditTest(reloadCount = 2) {
+        liveEditTest(
+            reloadCount = 2,
+            collectSourceInformation = SourceInfo.None,
+        ) {
             expectError("throwInMovableContent", 1)
 
             val content = remember {
@@ -567,28 +589,71 @@
     addTargetKey((currentComposer as ComposerImpl).parentKey())
 }
 
+enum class SourceInfo {
+    None,
+    Collect,
+    Both,
+}
+
 @OptIn(InternalComposeApi::class)
 fun liveEditTest(
     reloadCount: Int = 1,
+    collectSourceInformation: SourceInfo = SourceInfo.Both,
     fn: @Composable LiveEditTestScope.() -> Unit,
-) = compositionTest {
-    with(LiveEditTestScope()) {
-        addCheck {
-            (composition as? ControlledComposition)?.verifyConsistent()
-        }
+) {
+    if (
+        collectSourceInformation == SourceInfo.Both ||
+        collectSourceInformation == SourceInfo.Collect
+    ) {
+        compositionTest {
+            with(LiveEditTestScope()) {
+                addCheck {
+                    (composition as? ControlledComposition)?.verifyConsistent()
+                }
 
-        recordErrors {
-            compose { fn(this) }
-        }
+                recordErrors {
+                    compose {
+                        currentComposer.collectParameterInformation()
+                        fn(this)
+                    }
+                }
 
-        repeat(reloadCount) {
-            invalidateTargets()
-            recordErrors {
-                advance()
+                repeat(reloadCount) {
+                    invalidateTargets()
+                    recordErrors {
+                        advance()
+                    }
+                }
+
+                runChecks()
             }
         }
+    }
 
-        runChecks()
+    if (
+        collectSourceInformation == SourceInfo.Both ||
+        collectSourceInformation == SourceInfo.None
+    ) {
+        compositionTest {
+            with(LiveEditTestScope()) {
+                addCheck {
+                    (composition as? ControlledComposition)?.verifyConsistent()
+                }
+
+                recordErrors {
+                    compose { fn(this) }
+                }
+
+                repeat(reloadCount) {
+                    invalidateTargets()
+                    recordErrors {
+                        advance()
+                    }
+                }
+
+                runChecks()
+            }
+        }
     }
 }
 
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 8ba40fa..a36e3d6 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -502,23 +502,26 @@
         val box = context.bounds
         val size = box.size.toSize()
         val coordinates = layoutInfo.coordinates
-        val topLeft = toIntOffset(coordinates.localToWindow(Offset.Zero))
-        val topRight = toIntOffset(coordinates.localToWindow(Offset(size.width, 0f)))
-        val bottomRight = toIntOffset(coordinates.localToWindow(Offset(size.width, size.height)))
-        val bottomLeft = toIntOffset(coordinates.localToWindow(Offset(0f, size.height)))
         var bounds: QuadBounds? = null
+        if (layoutInfo.isAttached) {
+            val topLeft = toIntOffset(coordinates.localToWindow(Offset.Zero))
+            val topRight = toIntOffset(coordinates.localToWindow(Offset(size.width, 0f)))
+            val bottomRight =
+                toIntOffset(coordinates.localToWindow(Offset(size.width, size.height)))
+            val bottomLeft = toIntOffset(coordinates.localToWindow(Offset(0f, size.height)))
 
-        if (topLeft.x != box.left || topLeft.y != box.top ||
-            topRight.x != box.right || topRight.y != box.top ||
-            bottomRight.x != box.right || bottomRight.y != box.bottom ||
-            bottomLeft.x != box.left || bottomLeft.y != box.bottom
-        ) {
-            bounds = QuadBounds(
-                topLeft.x, topLeft.y,
-                topRight.x, topRight.y,
-                bottomRight.x, bottomRight.y,
-                bottomLeft.x, bottomLeft.y,
-            )
+            if (topLeft.x != box.left || topLeft.y != box.top ||
+                topRight.x != box.right || topRight.y != box.top ||
+                bottomRight.x != box.right || bottomRight.y != box.bottom ||
+                bottomLeft.x != box.left || bottomLeft.y != box.bottom
+            ) {
+                bounds = QuadBounds(
+                    topLeft.x, topLeft.y,
+                    topRight.x, topRight.y,
+                    bottomRight.x, bottomRight.y,
+                    bottomLeft.x, bottomLeft.y,
+                )
+            }
         }
         if (!includeNodesOutsizeOfWindow) {
             // Ignore this node if the bounds are completely outside the window
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
deleted file mode 100644
index 72f0dbc..0000000
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/HyphensLineBreakBenchmark.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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.ui.text.benchmark
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.text.LineBreakConfig
-import android.graphics.text.LineBreaker
-import android.os.Build
-import android.text.Layout
-import android.text.TextPaint
-import androidx.benchmark.junit4.BenchmarkRule
-import androidx.benchmark.junit4.measureRepeated
-import androidx.compose.ui.text.android.InternalPlatformTextApi
-import androidx.compose.ui.text.android.StaticLayoutFactory
-import androidx.compose.ui.text.style.Hyphens
-import androidx.compose.ui.text.style.LineBreak
-import androidx.test.filters.LargeTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@RunWith(Parameterized::class)
-@OptIn(InternalPlatformTextApi::class)
-class HyphensLineBreakBenchmark(
-    private val textLength: Int,
-    private val hyphensString: String,
-    private val lineBreakString: String
-) {
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "length={0} hyphens={1} lineBreak={2}")
-        fun initParameters(): List<Array<Any?>> {
-            return cartesian(
-                arrayOf(32, 128, 512),
-                arrayOf(
-                    Hyphens.None.toTestString(),
-                    Hyphens.Auto.toTestString()
-                ),
-                arrayOf(
-                    LineBreak.Paragraph.toTestString(),
-                    LineBreak.Simple.toTestString(),
-                    LineBreak.Heading.toTestString()
-                )
-            )
-        }
-    }
-
-    @get:Rule
-    val benchmarkRule = BenchmarkRule()
-
-    @get:Rule
-    val textBenchmarkRule = TextBenchmarkTestRule()
-
-    private val width = 100
-    private val textSize: Float = 10F
-    private val hyphenationFrequency = toLayoutHyphenationFrequency(hyphensString.toHyphens())
-    private val lineBreakStyle = toLayoutLineBreakStyle(lineBreakString.toLineBreak().strictness)
-    private val breakStrategy = toLayoutBreakStrategy(lineBreakString.toLineBreak().strategy)
-    private val lineBreakWordStyle =
-        toLayoutLineBreakWordStyle(lineBreakString.toLineBreak().wordBreak)
-
-    @Test
-    fun constructLayout() {
-        textBenchmarkRule.generator { textGenerator ->
-            val text = textGenerator.nextParagraph(textLength)
-            val textPaint = TextPaint()
-            textPaint.textSize = textSize
-            benchmarkRule.measureRepeated {
-                StaticLayoutFactory.create(
-                    text = text,
-                    width = width,
-                    paint = textPaint,
-                    hyphenationFrequency = hyphenationFrequency,
-                    lineBreakStyle = lineBreakStyle,
-                    breakStrategy = breakStrategy,
-                    lineBreakWordStyle = lineBreakWordStyle
-                )
-            }
-        }
-    }
-
-    @Test
-    fun constructLayoutDraw() {
-        textBenchmarkRule.generator { textGenerator ->
-            val text = textGenerator.nextParagraph(textLength)
-            val textPaint = TextPaint()
-            textPaint.textSize = textSize
-            val canvas = Canvas(Bitmap.createBitmap(width, 1000, Bitmap.Config.ARGB_8888))
-            benchmarkRule.measureRepeated {
-                val layout = StaticLayoutFactory.create(
-                    text = text,
-                    width = width,
-                    paint = textPaint,
-                    hyphenationFrequency = hyphenationFrequency,
-                    lineBreakStyle = lineBreakStyle,
-                    breakStrategy = breakStrategy,
-                    lineBreakWordStyle = lineBreakWordStyle
-                )
-                layout.draw(canvas)
-            }
-        }
-    }
-
-    private fun toLayoutHyphenationFrequency(hyphens: Hyphens?): Int = when (hyphens) {
-        Hyphens.Auto -> if (Build.VERSION.SDK_INT <= 32) {
-            Layout.HYPHENATION_FREQUENCY_NORMAL
-        } else {
-            Layout.HYPHENATION_FREQUENCY_NORMAL_FAST
-        }
-        Hyphens.None -> Layout.HYPHENATION_FREQUENCY_NONE
-        else -> Layout.HYPHENATION_FREQUENCY_NONE
-    }
-
-    private fun toLayoutBreakStrategy(breakStrategy: LineBreak.Strategy?): Int =
-        when (breakStrategy) {
-            LineBreak.Strategy.Simple -> LineBreaker.BREAK_STRATEGY_SIMPLE
-            LineBreak.Strategy.HighQuality -> LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
-            LineBreak.Strategy.Balanced -> LineBreaker.BREAK_STRATEGY_BALANCED
-            else -> LineBreaker.BREAK_STRATEGY_SIMPLE
-        }
-
-    private fun toLayoutLineBreakStyle(lineBreakStrictness: LineBreak.Strictness?): Int =
-        when (lineBreakStrictness) {
-            LineBreak.Strictness.Default -> LineBreakConfig.LINE_BREAK_STYLE_NONE
-            LineBreak.Strictness.Loose -> LineBreakConfig.LINE_BREAK_STYLE_LOOSE
-            LineBreak.Strictness.Normal -> LineBreakConfig.LINE_BREAK_STYLE_NORMAL
-            LineBreak.Strictness.Strict -> LineBreakConfig.LINE_BREAK_STYLE_STRICT
-            else -> LineBreakConfig.LINE_BREAK_STYLE_NONE
-        }
-
-    private fun toLayoutLineBreakWordStyle(lineBreakWordStyle: LineBreak.WordBreak?): Int =
-        when (lineBreakWordStyle) {
-            LineBreak.WordBreak.Default -> LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
-            LineBreak.WordBreak.Phrase -> LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE
-            else -> LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
-        }
-}
-
-/**
- * Required to make this test work due to a bug with value classes and Parameterized JUnit tests.
- * https://youtrack.jetbrains.com/issue/KT-35523
- *
- * However, it's not enough to use a wrapper because wrapper makes the test name unnecessarily
- * long which causes Perfetto to be unable to create output files with a very long name in some
- * file systems.
- *
- * Using a String instead of an Integer gives us a better test naming.
- */
-private fun String.toLineBreak(): LineBreak = when (this) {
-    "Simple" -> LineBreak.Simple
-    "Heading" -> LineBreak.Heading
-    "Paragraph" -> LineBreak.Paragraph
-    else -> throw IllegalArgumentException("Unrecognized LineBreak value for this test")
-}
-
-private fun LineBreak.toTestString(): String = when (this) {
-    LineBreak.Simple -> "Simple"
-    LineBreak.Heading -> "Heading"
-    LineBreak.Paragraph -> "Paragraph"
-    else -> throw IllegalArgumentException("Unrecognized LineBreak value for this test")
-}
-
-private fun String.toHyphens(): Hyphens = when (this) {
-    "None" -> Hyphens.None
-    "Auto" -> Hyphens.Auto
-    else -> throw IllegalArgumentException("Unrecognized Hyphens value for this test")
-}
-
-private fun Hyphens.toTestString(): String = when (this) {
-    Hyphens.None -> "None"
-    Hyphens.Auto -> "Auto"
-    else -> throw IllegalArgumentException("Unrecognized Hyphens value for this test")
-}
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
index a0802ab..196eb55 100644
--- a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
+++ b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
@@ -150,9 +150,6 @@
             assertFalse(receiver.parameterCursor.hasNext())
         }
 
-        // Skip Inspectable
-        callCursor.next()
-
         // OneParameter(1)
         validate {
             parameter(name = "a", value = 1, fromDefault = false, static = true, compared = false)
@@ -339,11 +336,11 @@
         val tree = slotTableRecord.store.first().asTree()
         val list = tree.asList()
         val parameters = list.filter { group ->
-            group.parameters.isNotEmpty() && group.location.let {
+            group.parameters.isNotEmpty() && group.name == "Text" && group.location.let {
                 it != null && it.sourceFile == "InspectableTests.kt"
             }
         }
-        val names = parameters.drop(1).first().parameters.map { it.name }
+        val names = parameters.first().parameters.map { it.name }
         assertEquals(
             "text, modifier, color, fontSize, fontStyle, fontWeight, fontFamily, " +
                 "letterSpacing, textDecoration, textAlign, lineHeight, overflow, softWrap, " +
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 59a93d9..e5c14c2 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -609,7 +609,7 @@
   }
 
   public final class FocusRestorerKt {
-    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier);
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function0<androidx.compose.ui.focus.FocusRequester>? onRestoreFailed);
   }
 
   public interface FocusState {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 7608ff3..71da5eb 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -609,7 +609,7 @@
   }
 
   public final class FocusRestorerKt {
-    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier);
+    method @SuppressCompatibility @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier focusRestorer(androidx.compose.ui.Modifier, optional kotlin.jvm.functions.Function0<androidx.compose.ui.focus.FocusRequester>? onRestoreFailed);
   }
 
   public interface FocusState {
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt
new file mode 100644
index 0000000..e96dac5
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.benchmark.graphics.vector
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.benchmark.R
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class CreateVectorPainterBenchmark {
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    @Test
+    fun recreateContent() {
+        benchmarkRule.toggleStateBenchmarkDraw({
+            RecreateVectorPainterTestCase()
+        }, assertOneRecomposition = false)
+    }
+}
+
+private class RecreateVectorPainterTestCase : ComposeTestCase, ToggleableTestCase {
+
+    private var alpha by mutableStateOf(1f)
+
+    @Composable
+    override fun Content() {
+        Column {
+            Box(modifier = Modifier.wrapContentSize()) {
+                Image(
+                    painter = painterResource(R.drawable.ic_hourglass),
+                    contentDescription = null,
+                    modifier = Modifier.size(200.dp),
+                    alpha = alpha
+                )
+            }
+        }
+    }
+
+    override fun toggleState() {
+        if (alpha == 1.0f) {
+            alpha = 0.5f
+        } else {
+            alpha = 1.0f
+        }
+    }
+}
diff --git a/compose/ui/ui/benchmark/src/main/res/drawable/ic_hourglass.xml b/compose/ui/ui/benchmark/src/main/res/drawable/ic_hourglass.xml
new file mode 100644
index 0000000..1666c76
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/main/res/drawable/ic_hourglass.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24" >
+    <group
+        android:name="hourglass_frame"
+        android:translateX="12"
+        android:translateY="12"
+        android:scaleX="0.75"
+        android:scaleY="0.75" >
+        <group
+            android:name="hourglass_frame_pivot"
+            android:translateX="-12"
+            android:translateY="-12" >
+            <group
+                android:name="group_2_2"
+                android:translateX="12"
+                android:translateY="6.5" >
+                <path
+                    android:name="path_2_2"
+                    android:pathData="M 6.52099609375 -3.89300537109 c 0.0 0.0 -6.52099609375 6.87901306152 -6.52099609375 6.87901306152 c 0 0.0 -6.52099609375 -6.87901306152 -6.52099609375 -6.87901306152 c 0.0 0.0 13.0419921875 0.0 13.0419921875 0.0 Z M 9.99800109863 -6.5 c 0.0 0.0 -19.9960021973 0.0 -19.9960021973 0.0 c -0.890991210938 0.0 -1.33700561523 1.07699584961 -0.707000732422 1.70700073242 c 0.0 0.0 10.7050018311 11.2929992676 10.7050018311 11.2929992676 c 0 0.0 10.7050018311 -11.2929992676 10.7050018311 -11.2929992676 c 0.630004882812 -0.630004882812 0.183990478516 -1.70700073242 -0.707000732422 -1.70700073242 Z"
+                    android:fillColor="#FF777777" />
+            </group>
+            <group
+                android:name="group_1_2"
+                android:translateX="12"
+                android:translateY="17.5" >
+                <path
+                    android:name="path_2_1"
+                    android:pathData="M 0 -2.98600769043 c 0 0.0 6.52099609375 6.87901306152 6.52099609375 6.87901306152 c 0.0 0.0 -13.0419921875 0.0 -13.0419921875 0.0 c 0.0 0.0 6.52099609375 -6.87901306152 6.52099609375 -6.87901306152 Z M 0 -6.5 c 0 0.0 -10.7050018311 11.2929992676 -10.7050018311 11.2929992676 c -0.630004882812 0.630004882812 -0.184005737305 1.70700073242 0.707000732422 1.70700073242 c 0.0 0.0 19.9960021973 0.0 19.9960021973 0.0 c 0.890991210938 0.0 1.33699035645 -1.07699584961 0.707000732422 -1.70700073242 c 0.0 0.0 -10.7050018311 -11.2929992676 -10.7050018311 -11.2929992676 Z"
+                    android:fillColor="#FF777777" />
+            </group>
+        </group>
+    </group>
+    <group
+        android:name="fill_outlines"
+        android:translateX="12"
+        android:translateY="12"
+        android:scaleX="0.75"
+        android:scaleY="0.75" >
+        <group
+            android:name="fill_outlines_pivot"
+            android:translateX="-12"
+            android:translateY="-12" >
+            <clip-path
+                android:name="mask_1"
+                android:pathData="M 24 13.3999938965 c 0 0.0 -24 0.0 -24 0.0 c 0 0.0 0 10.6000061035 0 10.6000061035 c 0 0 24 0 24 0 c 0 0 0 -10.6000061035 0 -10.6000061035 Z" />
+            <group
+                android:name="group_1_3"
+                android:translateX="12"
+                android:translateY="12" >
+                <path
+                    android:name="path_1_6"
+                    android:pathData="M 10.7100067139 10.2900085449 c 0.629989624023 0.629989624023 0.179992675781 1.70999145508 -0.710006713867 1.70999145508 c 0 0 -20 0 -20 0 c -0.889999389648 0 -1.33999633789 -1.08000183105 -0.710006713867 -1.70999145508 c 0.0 0.0 9.76000976562 -10.2900085449 9.76000976563 -10.2900085449 c 0.0 0 -9.76000976562 -10.2899932861 -9.76000976563 -10.2899932861 c -0.629989624023 -0.630004882812 -0.179992675781 -1.71000671387 0.710006713867 -1.71000671387 c 0 0 20 0 20 0 c 0.889999389648 0 1.33999633789 1.08000183105 0.710006713867 1.71000671387 c 0.0 0.0 -9.76000976562 10.2899932861 -9.76000976563 10.2899932861 c 0.0 0 9.76000976562 10.2900085449 9.76000976563 10.2900085449 Z"
+                    android:fillColor="#FF777777" />
+            </group>
+        </group>
+    </group>
+</vector>
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
index 69035e5..4041e63 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/FocusSamples.kt
@@ -134,6 +134,27 @@
     }
 }
 
+@OptIn(ExperimentalComposeUiApi::class)
+@Sampled
+@Composable
+fun FocusRestorerCustomFallbackSample() {
+    val focusRequester = remember { FocusRequester() }
+    LazyRow(
+        // If restoration fails, focus would fallback to the item associated with focusRequester.
+        Modifier.focusRestorer { focusRequester }
+    ) {
+        item {
+            Button(
+                modifier = Modifier.focusRequester(focusRequester),
+                onClick = {}
+            ) { Text("1") }
+        }
+        item { Button(onClick = {}) { Text("2") } }
+        item { Button(onClick = {}) { Text("3") } }
+        item { Button(onClick = {}) { Text("4") } }
+    }
+}
+
 @Sampled
 @Composable
 fun RequestFocusSample() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
index dbe8b98..469567e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRestorerTest.kt
@@ -177,4 +177,48 @@
             assertThat(grandChildState.isFocused).isFalse()
         }
     }
+
+    @Test
+    fun restorationFailed_fallbackToOnRestoreFailedDestination() {
+        // Arrange.
+        val (parent, child2) = FocusRequester.createRefs()
+        lateinit var child1State: FocusState
+        lateinit var child2State: FocusState
+        rule.setFocusableContent {
+            Box(
+                Modifier
+                    .size(10.dp)
+                    .focusRequester(parent)
+                    .focusRestorer { child2 }
+                    .focusGroup()
+            ) {
+                key(1) {
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .onFocusChanged { child1State = it }
+                            .focusTarget()
+                    )
+                }
+                key(2) {
+                    Box(
+                        Modifier
+                            .size(10.dp)
+                            .focusRequester(child2)
+                            .onFocusChanged { child2State = it }
+                            .focusTarget()
+                    )
+                }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { parent.requestFocus() }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(child1State.isFocused).isFalse()
+            assertThat(child2State.isFocused).isTrue()
+        }
+    }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt
index 68786aa..cacc19c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerIconTest.kt
@@ -531,7 +531,7 @@
      *  Parent Box (output icon = [PointerIcon.Crosshair])
      *    ⤷ Child Box (output icon = [PointerIcon.Crosshair])
      */
-    @Ignore("b/267170292 - not yet implemented")
+    @Ignore("b/299482894 - not yet implemented")
     @Test
     fun parentChildPartialOverlap_parentModifierDynamicallyAddedWithMoveEvents() {
         val isVisible = mutableStateOf(false)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
index c490f79..bfa2812 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRestorer.kt
@@ -19,7 +19,6 @@
 import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRestorerNode.Companion.FocusRestorerElement
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.currentValueOf
@@ -63,17 +62,25 @@
 
 // TODO: Move focusRestorer to foundation after saveFocusedChild and restoreFocusedChild are stable.
 /**
- * This modifier can be uses to save and restore focus to a focus group.
+ * This modifier can be used to save and restore focus to a focus group.
  * When focus leaves the focus group, it stores a reference to the item that was previously focused.
  * Then when focus re-enters this focus group, it restores focus to the previously focused item.
  *
+ * @param onRestoreFailed callback provides a lambda that is invoked if focus restoration fails.
+ * This lambda can be used to return a custom fallback item by providing a [FocusRequester]
+ * attached to that item. This can be used to customize the initially focused item.
+ *
  * @sample androidx.compose.ui.samples.FocusRestorerSample
+ * @sample androidx.compose.ui.samples.FocusRestorerCustomFallbackSample
  */
 @ExperimentalComposeUiApi
-fun Modifier.focusRestorer(): Modifier = this then FocusRestorerElement
+fun Modifier.focusRestorer(
+    onRestoreFailed: (() -> FocusRequester)? = null
+): Modifier = this then FocusRestorerElement(onRestoreFailed)
 
-internal class FocusRestorerNode :
-    FocusPropertiesModifierNode, FocusRequesterModifierNode, Modifier.Node() {
+internal class FocusRestorerNode(
+    var onRestoreFailed: (() -> FocusRequester)?
+) : FocusPropertiesModifierNode, FocusRequesterModifierNode, Modifier.Node() {
     private val onExit: (FocusDirection) -> FocusRequester = {
         @OptIn(ExperimentalComposeUiApi::class)
         saveFocusedChild()
@@ -83,22 +90,32 @@
     @OptIn(ExperimentalComposeUiApi::class)
     private val onEnter: (FocusDirection) -> FocusRequester = {
         @OptIn(ExperimentalComposeUiApi::class)
-        if (restoreFocusedChild()) FocusRequester.Cancel else FocusRequester.Default
+        if (restoreFocusedChild()) {
+            FocusRequester.Cancel
+        } else {
+            onRestoreFailed?.invoke() ?: FocusRequester.Default
+        }
     }
+
     override fun applyFocusProperties(focusProperties: FocusProperties) {
         @OptIn(ExperimentalComposeUiApi::class)
         focusProperties.enter = onEnter
         @OptIn(ExperimentalComposeUiApi::class)
         focusProperties.exit = onExit
     }
+}
 
-    companion object {
-        val FocusRestorerElement = object : ModifierNodeElement<FocusRestorerNode>() {
-            override fun create() = FocusRestorerNode()
-            override fun update(node: FocusRestorerNode) {}
-            override fun InspectorInfo.inspectableProperties() { name = "focusRestorer" }
-            override fun hashCode(): Int = "focusRestorer".hashCode()
-            override fun equals(other: Any?) = other === this
-        }
+private data class FocusRestorerElement(
+    val onRestoreFailed: (() -> FocusRequester)?
+) : ModifierNodeElement<FocusRestorerNode>() {
+    override fun create() = FocusRestorerNode(onRestoreFailed)
+
+    override fun update(node: FocusRestorerNode) {
+        node.onRestoreFailed = onRestoreFailed
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "focusRestorer"
+        properties["onRestoreFailed"] = onRestoreFailed
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
index 2ab3382..f55c989 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
@@ -17,12 +17,11 @@
 package androidx.compose.ui.input.pointer
 
 import androidx.compose.runtime.Stable
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.PointerEventPass.Main
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
-import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.PointerInputModifierNode
 import androidx.compose.ui.node.TraversableNode
 import androidx.compose.ui.node.TraversableNode.Companion.VisitSubtreeIfAction
 import androidx.compose.ui.node.currentValueOf
@@ -31,6 +30,7 @@
 import androidx.compose.ui.node.traverseSubtreeIf
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.LocalPointerIconService
+import androidx.compose.ui.unit.IntSize
 
 /**
  * Represents a pointer icon to use in [Modifier.pointerHoverIcon]
@@ -107,20 +107,20 @@
 
 /*
  * Changes the pointer hover icon if the node is in bounds and if the node is not overridden
- * by a parent pointer hover icon node. This node delegates to the pointer input node
- * (SuspendingPointerInputModifierNode) to determine if the pointer has entered or exited the
- * bounds of the modifier itself.
- *
- * Note: It will not delegate if a parent has overridden descendants.
+ * by a parent pointer hover icon node. This node implements [PointerInputModifierNode] so it can
+ * listen to pointer input events and determine if the pointer has entered or exited the bounds of
+ * the modifier itself.
  *
  * If the icon or overrideDescendants values are changed, this node will determine if it needs to
  * walk down and/or up the modifier chain to update those pointer hover icon modifier nodes as well.
  */
-@OptIn(ExperimentalComposeUiApi::class)
 internal class PointerHoverIconModifierNode(
     icon: PointerIcon,
     overrideDescendants: Boolean = false
-) : DelegatingNode(), TraversableNode, CompositionLocalConsumerModifierNode {
+) : Modifier.Node(),
+    TraversableNode,
+    PointerInputModifierNode,
+    CompositionLocalConsumerModifierNode {
     /* Traversal key used with the [TraversableNode] interface to enable all the traversing
      * functions (ancestor, child, subtree, and subtreeIf).
      */
@@ -158,27 +158,30 @@
     private val pointerIconService: PointerIconService?
         get() = currentValueOf(LocalPointerIconService)
 
-    // Pointer Input handler for determining if a Pointer has Entered or Exited this node.
-    private var delegatePointerInputModifierNode = delegate(
-        SuspendingPointerInputModifierNodeImpl {
-            awaitPointerEventScope {
-                while (true) {
-                    val event = awaitPointerEvent(Main)
+    private var cursorInBoundsOfNode = false
 
-                    // Cursor within the surface area of this node's bounds
-                    if (event.type == PointerEventType.Enter) {
-                        cursorInBoundsOfNode = true
-                        displayIconIfDescendantsDoNotHavePriority()
-                    } else if (event.type == PointerEventType.Exit) {
-                        cursorInBoundsOfNode = false
-                        displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
-                    }
-                }
+    // Pointer Input callback for determining if a Pointer has Entered or Exited this node.
+    override fun onPointerEvent(
+        pointerEvent: PointerEvent,
+        pass: PointerEventPass,
+        bounds: IntSize
+    ) {
+        if (pass == Main) {
+            // Cursor within the surface area of this node's bounds
+            if (pointerEvent.type == PointerEventType.Enter) {
+                cursorInBoundsOfNode = true
+                displayIconIfDescendantsDoNotHavePriority()
+            } else if (pointerEvent.type == PointerEventType.Exit) {
+                cursorInBoundsOfNode = false
+                displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
             }
         }
-    )
+    }
 
-    private var cursorInBoundsOfNode = false
+    override fun onCancelPointerInput() {
+        // We aren't processing the event (only listening for enter/exit), so we don't need to
+        // do anything.
+    }
 
     override fun onDetach() {
         cursorInBoundsOfNode = false
diff --git a/core/core-performance-play-services/build.gradle b/core/core-performance-play-services/build.gradle
index c7a44dd..5ae6276 100644
--- a/core/core-performance-play-services/build.gradle
+++ b/core/core-performance-play-services/build.gradle
@@ -29,23 +29,13 @@
     implementation(libs.kotlinCoroutinesCore)
     implementation(project(":core:core-performance"))
     implementation("androidx.datastore:datastore-preferences:1.0.0")
-    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
 
-    testImplementation(libs.robolectric)
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.truth)
-    androidTestImplementation(libs.espressoCore, excludes.espresso)
-    androidTestImplementation(libs.mockitoAndroid)
 
 
-    testImplementation(libs.testCore)
-    testImplementation(libs.kotlinStdlib)
-    testImplementation(libs.kotlinCoroutinesTest)
-    testImplementation(libs.junit)
-    testImplementation(libs.truth)
-
 }
 
 android {
diff --git a/credentials/credentials-fido/credentials-fido/build.gradle b/credentials/credentials-fido/credentials-fido/build.gradle
index 400e738..0fa93f9 100644
--- a/credentials/credentials-fido/credentials-fido/build.gradle
+++ b/credentials/credentials-fido/credentials-fido/build.gradle
@@ -15,6 +15,7 @@
  */
 
 import androidx.build.LibraryType
+import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
@@ -53,4 +54,5 @@
     type = LibraryType.PUBLISHED_LIBRARY
     inceptionYear = "2023"
     description = "Util library for apps using FIDO"
+    publish = Publish.SNAPSHOT_AND_RELEASE
 }
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 01c1c3e..e9ff045 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -681,6 +681,8 @@
 public abstract java\.util\.List<androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameEntity> jvmQuery\(\);
 public abstract androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameDao jvmDao\(\);
 \^
+\$SUPPORT/slice/slice\-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics\.java:[0-9]+: warning: \[deprecation\] SliceView in androidx\.slice\.widget has been deprecated
+import androidx\.slice\.widget\.SliceView;
 # b/296419682
 \$SUPPORT/concurrent/concurrent\-futures/src/test/java/androidx/concurrent/futures/AbstractResolvableFutureTest\.java:[0-9]+: warning: \[removal\] resume\(\) in Thread has been deprecated and marked for removal
 thread\.resume\(\);
@@ -729,4 +731,13 @@
 # b/271306193 remove after aosp/2589888 :emoji:emoji:spdxSbomForRelease
 spdx sboms require a version but project: noto\-emoji\-compat\-flatbuffers has no specified version
 # > Configure project :internal-testutils-appcompat
-WARNING: The option setting 'android\.experimental\.lint\.reservedMemoryPerTask=[0-9]+g' is experimental\.
\ No newline at end of file
+WARNING: The option setting 'android\.experimental\.lint\.reservedMemoryPerTask=[0-9]+g' is experimental\.
+# > Task :slice:slice-builders-ktx:compileDebugKotlin
+w: file://\$SUPPORT/slice/slice\-builders\-ktx/src/main/java/androidx/slice/builders/ListBuilder\.kt:[0-9]+:[0-9]+ 'ListBuilder' is deprecated\. Deprecated in Java
+# > Task :slice:slice-builders-ktx:compileDebugAndroidTestKotlin
+w: file://\$SUPPORT/slice/slice\-builders\-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest\.kt:[0-9]+:[0-9]+ 'SliceProvider' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/slice/slice\-builders\-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest\.kt:[0-9]+:[0-9]+ 'SliceSpecs' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/slice/slice\-builders\-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest\.kt:[0-9]+:[0-9]+ 'ListBuilder' is deprecated\. Deprecated in Java
+# > Task :slice:slice-benchmark:compileReleaseAndroidTestJavaWithJavac
+\$SUPPORT/slice/slice\-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics\.java:[0-9]+: warning: \[deprecation\] SliceHints in androidx\.slice\.core has been deprecated
+import androidx\.slice\.core\.SliceHints;
diff --git a/development/update-verification-metadata.sh b/development/update-verification-metadata.sh
index d0a6593e..a0487f3 100755
--- a/development/update-verification-metadata.sh
+++ b/development/update-verification-metadata.sh
@@ -57,7 +57,7 @@
   fi
 
   # next, remove 'version=' lines https://github.com/gradle/gradle/issues/20192
-  sed -i 's/ \(trusted-key.*\)version="[^"]*"/\1/' gradle/verification-metadata.xml
+  sed -i 's/\(trusted-key.*\)version="[^"]*"/\1/' gradle/verification-metadata.xml
 
   # rename keyring
   mv gradle/verification-keyring-dryrun.keys gradle/verification-keyring.keys 2>/dev/null || true
diff --git a/docs/api_guidelines/dependencies.md b/docs/api_guidelines/dependencies.md
index 011fab9..5a43db3 100644
--- a/docs/api_guidelines/dependencies.md
+++ b/docs/api_guidelines/dependencies.md
@@ -107,9 +107,16 @@
 image and are made available to developers through the `<uses-library>` manifest
 tag.
 
-Examples include Wear OS extensions (`com.google.android.wearable`), Camera OEM
-extensions (`androidx.camera.extensions.impl`), and Window OEM extensions
-(`androix.window.extensions`).
+Examples include Camera OEM extensions (`androidx.camera.extensions.impl`) and
+Window OEM extensions (`androidx.window.extensions`).
+
+Extension libraries may be defined in AndroidX library projects (see
+`androidx.window.extensions`) or externally, ex. in AOSP alongside the platform.
+In either case, we recommend that libraries use extensions as pinned, rather
+than project-type, dependencies to facilitate versioning across repositories.
+
+*Do not* ship extension interfaces to Google Maven. Teams may choose to ship
+stub JARs publicly, but that is not covered by AndroidX workflows.
 
 Project dependencies on extension libraries **must** use `compileOnly`:
 
diff --git a/docs/api_guidelines/deprecation.md b/docs/api_guidelines/deprecation.md
index f5dc6d8..2d7069e 100644
--- a/docs/api_guidelines/deprecation.md
+++ b/docs/api_guidelines/deprecation.md
@@ -91,8 +91,9 @@
     artifact as `@Deprecated` and update the API files
     ([example CL](https://android-review.googlesource.com/c/platform/frameworks/support/+/1938773))
 1.  Schedule a release of the artifact as a new minor version. When you populate
-    the release notes, explain that the entire artifact has been deprecated.
-    Include the reason for deprecation and the migration strategy.
+    the release notes, explain that the entire artifact has been deprecated and
+    will no longer receive new features or bug fixes. Include the reason for
+    deprecation and the migration strategy.
 1.  After the artifact has been released, remove the artifact from the source
     tree, versions file, and tip-of-tree docs configuration
     ([example CL](https://android-review.googlesource.com/c/platform/frameworks/support/+/2061731/))
@@ -107,3 +108,58 @@
 
 After an artifact has been released as fully-deprecated, it can be removed from
 the source tree.
+
+#### Long-term support
+
+Artifacts which have been fully deprecated and removed are not required to fix
+any bugs -- including security issues -- which are reported after the library
+has been removed from source control; however, library owners *may* utilize
+release branches to provide long-term support.
+
+When working on long-term support in a release branch, you may encounter the
+following issues:
+
+-   Release metadata produced by the build system is not compatible with the
+    release scheduling tool
+-   Build targets associated with the release branch do not match targets used
+    by the snapped build ID
+-   Delta between last snapped build ID and proposed snap build ID is too large
+    and cannot be processed by the release branch management tool
+
+### Discouraging usage in Play Store
+
+[Google Play SDK Console](https://play.google.com/sdk-console/) allows library
+owners to annotate specific library versions with notes, which are shown to app
+developers in the Play Store Console, or permanently mark them as outdated,
+which shows a warning in Play Store Console asking app developers to upgrade.
+
+In both cases, library owners have the option to prevent app developers from
+releasing apps to Play Store that have been built against specific library
+versions.
+
+Generally, Jetpack discourages the use of either notes or marking versions as
+outdated. There are few cases that warrant pushing notifications to app
+developers, and it is easy to abuse notes as advertising to drive adoption. As a
+rule, upgrades to Jetpack libraries should be driven by the needs of app
+developers.
+
+Cases where notes may be used include:
+
+1.  The library is used directly, rather than transitively, and contains `P0` or
+    `P1` (ship-blocking, from the app's perspective) issues
+    -   Transitively-included libraries should instead urge their dependent
+        libraries to bump their pinned dependency versions
+1.  The library contains ship-blocking security issues. In this case, we
+    recommend preventing app releases since developers may be less aware of
+    security issues.
+1.  The library was built against a pre-release SDK which has been superseded by
+    a finalized SDK. In this case, we recommend preventing app releases since
+    the library may crash or show unexpected behavior.
+
+Cases where marking a version as outdated maybe used:
+
+1.  The library has security implications and the version is no longer receiving
+    security updates, e.g. the release branch has moved to the next version.
+
+In all cases, there must be a newer stable or bugfix release of the library that
+app developers can use to replace the outdated version.
diff --git a/docs/onboarding_images/image10.png b/docs/onboarding_images/image10.png
new file mode 100644
index 0000000..ed1fd74
--- /dev/null
+++ b/docs/onboarding_images/image10.png
Binary files differ
diff --git a/docs/onboarding_images/image6.png b/docs/onboarding_images/image6.png
new file mode 100644
index 0000000..41795ce
--- /dev/null
+++ b/docs/onboarding_images/image6.png
Binary files differ
diff --git a/docs/onboarding_images/image7.png b/docs/onboarding_images/image7.png
new file mode 100644
index 0000000..27eef7d
--- /dev/null
+++ b/docs/onboarding_images/image7.png
Binary files differ
diff --git a/docs/onboarding_images/image8.png b/docs/onboarding_images/image8.png
new file mode 100644
index 0000000..da0dc66
--- /dev/null
+++ b/docs/onboarding_images/image8.png
Binary files differ
diff --git a/docs/onboarding_images/image9.png b/docs/onboarding_images/image9.png
new file mode 100644
index 0000000..d90dcc2
--- /dev/null
+++ b/docs/onboarding_images/image9.png
Binary files differ
diff --git a/docs/testing.md b/docs/testing.md
index 8050e0a..84e47d2 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -40,6 +40,164 @@
 ([example](https://r.android.com/2428721)). You can run these tests just like
 any other JVM test using `test` Gradle task.
 
+### Adding screenshots tests using scuba library
+
+#### Prerequisites
+
+Golden project: Make sure that you have the golden directory in your root
+checkout (sibling of frameworks directory). If not re-init your repo to fetch
+the latest manifest file:
+
+```
+$ repo init -u sso://android/platform/manifest \
+    -b androidx-main && repo sync -c -j8
+```
+
+Set up your module: If your module is not using screenshot tests yet, you need
+to do the initial setup.
+
+1.  Modify your gradle file: Add dependency on the diffing library into your
+    gradle file:
+
+    ```
+    androidTestImplementation project(“:test:screenshot:screenshot”)
+    ```
+
+    Important step: Add golden asset directory to be linked to your test apk:
+
+    ```
+    android {
+        sourceSets.androidTest.assets.srcDirs +=
+            // For androidx project (not in ui dir) use "/../../golden/project"
+            project.rootDir.absolutePath + "/../../golden/compose/material/material"
+    }
+    ```
+
+    This will bundle the goldens into your apk so they can be retrieved during
+    the test.
+
+2.  Create directory and variable: In the golden directory, create a new
+    directory for your module (the directory that you added to your gradle file,
+    which in case of material was “compose/material/material”).
+
+    In your test module, create a variable pointing at your new directory:
+
+    ```
+    const val GOLDEN_MATERIAL = "compose/material/material"
+    ```
+
+#### Adding a screenshot test
+
+Here is an example of a minimal screenshot test for compose material.
+
+```
+@LargeTest
+@RunWith(JUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class CheckboxScreenshotTest {
+    @get:Rule val composeTestRule = createComposeRule()
+    @get:Rule val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL)
+
+    @Test
+    fun checkBoxTest_checked() {
+        composeTestRule.setMaterialContent {
+            Checkbox(Modifier.wrapContentSize(Alignment.TopStart),
+                checked = true,
+                onCheckedChange = {}
+            )
+        }
+        find(isToggleable())
+            .captureToBitmap()
+            .assertAgainstGolden(screenshotRule, "checkbox_checked")
+    }
+}
+```
+
+NOTE: The string “checkbox_checked” is the unique identifier of your golden in
+your module. We use that string to name the golden file so avoid special
+characters. Please avoid any substrings like: golden, image etc. as there is no
+need - instead just describe what the image contains.
+
+#### Guidance around diffing
+
+Try to take the smallest screenshot possible. This will reduce interference from
+other elements.
+
+By default we use a MSSIM comparer. This one is based on similarity. However we
+have quite a high bar currently which is 0.98 (1 is an exact match). You can
+provide your own threshold or even opt into a pixel perfect comparer for some
+reason.
+
+Note: The bigger screenshots you take the more you sacrifice in the precision as
+you can aggregate larger diffing errors, see the examples below.
+
+![alt_text](onboarding_images/image6.png "screenshot diff at different MSSIM")
+
+#### Generating your goldens in CI (Gerrit)
+
+Upload your CL to gerrit and run presubmit. You should see your test fail.
+
+Step 1: Click on the “Test” button below:
+
+![alt_text](onboarding_images/image7.png "Presubmit link to failed test")
+
+Step 2: Click on the “Update scuba goldens” below:
+![alt_text](onboarding_images/image8.png "Update scuba button")
+
+Step 3: You should see a dashboard similar to the example below. Check-out if
+the new screenshots look as expected and if yes click approve. This will create
+a new CL.
+![alt_text](onboarding_images/image9.png "Button to approve scuba changes")
+
+Step 4: Link your original CL with the new goldens CL by setting the same Topic
+field in both CLs (any arbitrary string will do). This tells Gerrit to submit
+the CLs together, effectively providing a reference from the original CL to the
+new goldens. And re-run presubmit. Your tests should now pass!
+![alt_text](onboarding_images/image10.png "Topic for connecting cls")
+
+#### Running manually / debugging
+
+Screenshot tests can be run locally using pixel 2 api33 emulator. Start the
+emulator using [these](#emulator) steps.
+
+Wait until the emulator is running and run the tests as you would on a regular
+device.
+
+```
+$ ./gradlew <module>:cAT -Pandroid.testInstrumentationRunnerArguments.class=<class>
+```
+
+If the test passes, the results are limited to a .textproto file for each
+screenshot test. If the test fails, the results will also contain the actual
+screenshot and, if available, the golden reference image and the diff between
+the two. Note that this means that if you want to regenerate the golden image,
+you have to remove the golden image before running the test.
+
+To get the screenshot related results from the device onto your workstation, you
+can run
+
+```
+$ adb pull /sdcard/Android/data/<test-package>/cache/androidx_screenshots
+```
+
+where test-package is the identifier of you test apk, e.g.
+androidx.compose.material.test
+
+#### Locally updating the golden images
+
+After you run a screenshot test and pull the results to a desired location,
+verify that the actual images are the correct ones and copy them to the golden
+screenshots directory (the one you use to create the AndroidXScreenshotTestRule
+with) using this script.
+
+```
+androidx-main/frameworks/support/development/copy_screenshots_to_golden_repo.py \
+--input-dir=/tmp/androidx_screenshots/ --output-dir=androidx-main/golden/<test>/
+```
+
+Repeat for all screenshots, then create and upload a CL in the golden
+repository.
+
 ### What gets tested, and when {#affected-module-detector}
 
 With over 45000 tests executed on every CI run, it is necessary for us to run
@@ -81,12 +239,6 @@
 
 #### Disabling tests {#disabling-tests}
 
-To disable a device-side test in presubmit testing only -- but still have it run
-in postsubmit -- use the
-[`@FlakyTest`](https://developer.android.com/reference/androidx/test/filters/FlakyTest)
-annotation. There is currently no support for presubmit-only disabling of
-host-side tests.
-
 If you need to stop a host- or device-side test from running entirely, use
 JUnit's [`@Ignore`](http://junit.sourceforge.net/javadoc/org/junit/Ignore.html)
 annotation. Do *not* use Android's `@Suppress` annotation, which only works with
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 2f50bc8..d1e02606 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -451,21 +451,28 @@
     ctor public FragmentTransitionImpl();
     method public abstract void addTarget(Object, android.view.View);
     method public abstract void addTargets(Object, java.util.ArrayList<android.view.View!>);
+    method public void animateToEnd(Object);
+    method public void animateToStart(Object);
     method public abstract void beginDelayedTransition(android.view.ViewGroup, Object?);
     method protected static void bfsAddViewChildren(java.util.List<android.view.View!>!, android.view.View!);
     method public abstract boolean canHandle(Object);
     method public abstract Object! cloneTransition(Object?);
+    method public Object? controlDelayedTransition(android.view.ViewGroup, Object);
     method protected void getBoundsOnScreen(android.view.View!, android.graphics.Rect!);
     method protected static boolean isNullOrEmpty(java.util.List!);
+    method public boolean isSeekingSupported();
+    method public boolean isSeekingSupported(Object);
     method public abstract Object! mergeTransitionsInSequence(Object?, Object?, Object?);
     method public abstract Object! mergeTransitionsTogether(Object?, Object?, Object?);
     method public abstract void removeTarget(Object, android.view.View);
     method public abstract void replaceTargets(Object, java.util.ArrayList<android.view.View!>!, java.util.ArrayList<android.view.View!>!);
     method public abstract void scheduleHideFragmentView(Object, android.view.View, java.util.ArrayList<android.view.View!>);
     method public abstract void scheduleRemoveTargets(Object, Object?, java.util.ArrayList<android.view.View!>?, Object?, java.util.ArrayList<android.view.View!>?, Object?, java.util.ArrayList<android.view.View!>?);
+    method public void setCurrentPlayTime(Object, float);
     method public abstract void setEpicenter(Object, android.graphics.Rect);
     method public abstract void setEpicenter(Object, android.view.View?);
     method public void setListenerForTransitionEnd(androidx.fragment.app.Fragment, Object, androidx.core.os.CancellationSignal, Runnable);
+    method public void setListenerForTransitionEnd(androidx.fragment.app.Fragment, Object, androidx.core.os.CancellationSignal, Runnable?, Runnable);
     method public abstract void setSharedElementTargets(Object, android.view.View, java.util.ArrayList<android.view.View!>);
     method public abstract void swapSharedElementTargets(Object?, java.util.ArrayList<android.view.View!>?, java.util.ArrayList<android.view.View!>?);
     method public abstract Object! wrapTransitionInSet(Object?);
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
index c0ed273..a5f9e91 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.kt
@@ -188,13 +188,16 @@
         firstOut: Operation?,
         lastIn: Operation?
     ) {
-        // First verify that we can run all transitions together
-        val transitionImpl = transitionInfos.filterNot { transitionInfo ->
+        val filteredInfos = transitionInfos.filterNot { transitionInfo ->
             // If there is no change in visibility, we can skip the TransitionInfo
             transitionInfo.isVisibilityUnchanged
         }.filter { transitionInfo ->
             transitionInfo.handlingImpl != null
-        }.fold(null as FragmentTransitionImpl?) { chosenImpl, transitionInfo ->
+        }
+        // First verify that we can run all transitions together
+        val transitionImpl = filteredInfos.fold(
+            null as FragmentTransitionImpl?
+        ) { chosenImpl, transitionInfo ->
             val handlingImpl = transitionInfo.handlingImpl
             require(chosenImpl == null || handlingImpl === chosenImpl) {
                 "Mixing framework transitions and AndroidX transitions is not allowed. Fragment " +
@@ -215,7 +218,7 @@
         var exitingNames = ArrayList<String>()
         val firstOutViews = ArrayMap<String, View>()
         val lastInViews = ArrayMap<String, View>()
-        for (transitionInfo: TransitionInfo in transitionInfos) {
+        for (transitionInfo: TransitionInfo in filteredInfos) {
             val hasSharedElementTransition = transitionInfo.hasSharedElementTransition()
             // Compute the shared element transition between the firstOut and lastIn Fragments
             if (hasSharedElementTransition && (firstOut != null) && (lastIn != null)) {
@@ -344,12 +347,12 @@
         }
 
         val transitionEffect = TransitionEffect(
-            transitionInfos, firstOut, lastIn, transitionImpl, sharedElementTransition,
+            filteredInfos, firstOut, lastIn, transitionImpl, sharedElementTransition,
             sharedElementFirstOutViews, sharedElementLastInViews, sharedElementNameMapping,
             enteringNames, exitingNames, firstOutViews, lastInViews, isPop
         )
 
-        transitionInfos.forEach { transitionInfo ->
+        filteredInfos.forEach { transitionInfo ->
             transitionInfo.operation.addEffect(transitionEffect)
         }
     }
@@ -700,7 +703,134 @@
     ) : Effect() {
         val transitionSignal = CancellationSignal()
 
+        var controller: Any? = null
+
+        override val isSeekingSupported: Boolean
+            get() = transitionImpl.isSeekingSupported &&
+                transitionInfos.all {
+                    Build.VERSION.SDK_INT >= 34 &&
+                        it.transition != null &&
+                        transitionImpl.isSeekingSupported(it.transition)
+                }
+
+        val transitioning: Boolean
+            get() = transitionInfos.all {
+                it.operation.fragment.mTransitioning
+            }
+
+        override fun onStart(container: ViewGroup) {
+            // If the container has never been laid out, transitions will not start so
+            // so lets instantly complete them.
+            if (!ViewCompat.isLaidOut(container)) {
+                transitionInfos.forEach { transitionInfo: TransitionInfo ->
+                    val operation: Operation = transitionInfo.operation
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(FragmentManager.TAG,
+                            "SpecialEffectsController: Container $container has not been " +
+                                "laid out. Skipping onStart for operation $operation")
+                    }
+                }
+                return
+            }
+            if (isSeekingSupported && transitioning) {
+                // We need to set the listener before we create the controller, but we need the
+                // controller to do the desired cancel behavior (animateToStart). So we use this
+                // lambda to set the proper cancel behavior to pass into the listener before the
+                // function is created.
+                var seekCancelLambda: (() -> Unit)? = null
+                // Now set up our completion signal on the completely merged transition set
+                val (enteringViews, mergedTransition) =
+                    createMergedTransition(container, lastIn, firstOut)
+                transitionInfos.map { it.operation }.forEach { operation ->
+                    val cancelRunnable = Runnable { seekCancelLambda?.invoke() }
+                    transitionImpl.setListenerForTransitionEnd(
+                        operation.fragment,
+                        mergedTransition,
+                        transitionSignal,
+                        cancelRunnable,
+                        Runnable {
+                            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                                Log.v(FragmentManager.TAG,
+                                    "Transition for operation $operation has completed")
+                            }
+                            operation.completeEffect(this)
+                        })
+                }
+
+                runTransition(enteringViews, container) {
+                    controller =
+                        transitionImpl.controlDelayedTransition(container, mergedTransition)
+                    // If we fail to create a controller, it must be because of the container or
+                    // the transition so we should throw an error.
+                    check(controller != null) {
+                        "Unable to start transition $mergedTransition for container $container."
+                    }
+                    seekCancelLambda = { transitionImpl.animateToStart(controller!!) }
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(FragmentManager.TAG,
+                            "Started executing operations from $firstOut to $lastIn")
+                    }
+                }
+            }
+        }
+
+        override fun onProgress(backEvent: BackEventCompat, container: ViewGroup) {
+            controller?.let { transitionImpl.setCurrentPlayTime(it, backEvent.progress) }
+        }
+
         override fun onCommit(container: ViewGroup) {
+            // If the container has never been laid out, transitions will not start so
+            // so lets instantly complete them.
+            if (!ViewCompat.isLaidOut(container)) {
+                transitionInfos.forEach { transitionInfo: TransitionInfo ->
+                    val operation: Operation = transitionInfo.operation
+                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                        Log.v(FragmentManager.TAG,
+                            "SpecialEffectsController: Container $container has not been " +
+                                "laid out. Completing operation $operation")
+                    }
+                    transitionInfo.operation.completeEffect(this)
+                }
+                return
+            }
+            if (controller != null) {
+                transitionImpl.animateToEnd(controller!!)
+                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(FragmentManager.TAG,
+                        "Ending execution of operations from $firstOut to $lastIn")
+                }
+            } else {
+                val (enteringViews, mergedTransition) =
+                    createMergedTransition(container, lastIn, firstOut)
+                // Now set up our completion signal on the completely merged transition set
+                transitionInfos.map { it.operation }.forEach { operation ->
+                    transitionImpl.setListenerForTransitionEnd(
+                        operation.fragment,
+                        mergedTransition,
+                        transitionSignal,
+                        Runnable {
+                            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                                Log.v(FragmentManager.TAG,
+                                    "Transition for operation $operation has completed")
+                            }
+                            operation.completeEffect(this)
+                        })
+                }
+                runTransition(enteringViews, container) {
+                    transitionImpl.beginDelayedTransition(container, mergedTransition)
+                }
+                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(FragmentManager.TAG,
+                        "Completed executing operations from $firstOut to $lastIn")
+                }
+            }
+        }
+
+        private fun createMergedTransition(
+            container: ViewGroup,
+            lastIn: Operation?,
+            firstOut: Operation?
+        ): Pair<ArrayList<View>, Any> {
             // Every transition needs to target at least one View so that they
             // don't interfere with one another. This is the view we use
             // in cases where there are no real views to target
@@ -772,26 +902,13 @@
             // Now iterate through the set of transitions and merge them together
             for (transitionInfo: TransitionInfo in transitionInfos) {
                 val operation: Operation = transitionInfo.operation
-                if (transitionInfo.isVisibilityUnchanged) {
-                    // No change in visibility, so we can immediately complete the transition
-                    transitionInfo.operation.completeEffect(this)
-                    continue
-                }
                 val transition = transitionImpl.cloneTransition(transitionInfo.transition)
-                val involvedInSharedElementTransition = (sharedElementTransition != null &&
-                    (operation === firstOut || operation === lastIn))
-                if (transition == null) {
-                    // Nothing more to do if the transition is null
-                    if (!involvedInSharedElementTransition) {
-                        // Only complete the transition if this fragment isn't involved
-                        // in the shared element transition (as otherwise we need to wait
-                        // for that to finish)
-                        transitionInfo.operation.completeEffect(this)
-                    }
-                } else {
+                if (transition != null) {
                     // Target the Transition to *only* the set of transitioning views
                     val transitioningViews = ArrayList<View>()
                     captureTransitioningViews(transitioningViews, operation.fragment.mView)
+                    val involvedInSharedElementTransition = (sharedElementTransition != null &&
+                        (operation === firstOut || operation === lastIn))
                     if (involvedInSharedElementTransition) {
                         // Remove all of the shared element views from the transition
                         if (operation === firstOut) {
@@ -804,8 +921,10 @@
                         transitionImpl.addTarget(transition, nonExistentView)
                     } else {
                         transitionImpl.addTargets(transition, transitioningViews)
-                        transitionImpl.scheduleRemoveTargets(transition, transition,
-                            transitioningViews, null, null, null, null)
+                        transitionImpl.scheduleRemoveTargets(
+                            transition, transition,
+                            transitioningViews, null, null, null, null
+                        )
                         if (operation.finalState === Operation.State.GONE) {
                             // We're hiding the Fragment. This requires a bit of extra work
                             // First, we need to avoid immediately applying the container change as
@@ -815,8 +934,10 @@
                             // essentially doing what applyState() would do for us
                             val transitioningViewsToHide = ArrayList(transitioningViews)
                             transitioningViewsToHide.remove(operation.fragment.mView)
-                            transitionImpl.scheduleHideFragmentView(transition,
-                                operation.fragment.mView, transitioningViewsToHide)
+                            transitionImpl.scheduleHideFragmentView(
+                                transition,
+                                operation.fragment.mView, transitioningViewsToHide
+                            )
                             // This OneShotPreDrawListener gets fired before the delayed start of
                             // the Transition and changes the visibility of any exiting child views
                             // that *ARE NOT* shared element transitions. The TransitionManager then
@@ -840,11 +961,13 @@
                     if (transitionInfo.isOverlapAllowed) {
                         // Overlap is allowed, so add them to the mergeTransition set
                         mergedTransition = transitionImpl.mergeTransitionsTogether(
-                            mergedTransition, transition, null)
+                            mergedTransition, transition, null
+                        )
                     } else {
                         // Overlap is not allowed, add them to the mergedNonOverlappingTransition
                         mergedNonOverlappingTransition = transitionImpl.mergeTransitionsTogether(
-                            mergedNonOverlappingTransition, transition, null)
+                            mergedNonOverlappingTransition, transition, null
+                        )
                     }
                 }
             }
@@ -854,51 +977,14 @@
             mergedTransition = transitionImpl.mergeTransitionsInSequence(mergedTransition,
                 mergedNonOverlappingTransition, sharedElementTransition)
 
-            // If there's no transitions playing together, no non-overlapping transitions,
-            // and no shared element transitions, mergedTransition will be null and
-            // there's nothing else we need to do
-            if (mergedTransition == null) {
-                return
-            }
-            // Now set up our completion signal on the completely merged transition set
-            transitionInfos.filterNot { transitionInfo ->
-                // If there's change in visibility, we've already completed the transition
-                transitionInfo.isVisibilityUnchanged
-            }.forEach { transitionInfo: TransitionInfo ->
-                val transition: Any? = transitionInfo.transition
-                val operation: Operation = transitionInfo.operation
-                val involvedInSharedElementTransition = sharedElementTransition != null &&
-                    (operation === firstOut || operation === lastIn)
-                if (transition != null || involvedInSharedElementTransition) {
-                    // If the container has never been laid out, transitions will not start so
-                    // so lets instantly complete them.
-                    if (!ViewCompat.isLaidOut(container)) {
-                        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                            Log.v(FragmentManager.TAG,
-                                "SpecialEffectsController: Container $container has not been " +
-                                    "laid out. Completing operation $operation")
-                        }
-                        transitionInfo.operation.completeEffect(this)
-                    } else {
-                        transitionImpl.setListenerForTransitionEnd(
-                            transitionInfo.operation.fragment,
-                            mergedTransition,
-                            transitionSignal,
-                            Runnable {
-                                transitionInfo.operation.completeEffect(this)
-                                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                                    Log.v(FragmentManager.TAG,
-                                        "Transition for operation $operation has completed")
-                                }
-                            })
-                    }
-                }
-            }
-            // Transitions won't run if the container isn't laid out so
-            // we can return early here to avoid doing unnecessary work.
-            if (!ViewCompat.isLaidOut(container)) {
-                return
-            }
+            return Pair(enteringViews, mergedTransition)
+        }
+
+        private fun runTransition(
+            enteringViews: ArrayList<View>,
+            container: ViewGroup,
+            executeTransition: (() -> Unit)
+        ) {
             // First, hide all of the entering views so they're in
             // the correct initial state
             setViewVisibility(enteringViews, View.INVISIBLE)
@@ -917,7 +1003,7 @@
                 }
             }
             // Now actually start the transition
-            transitionImpl.beginDelayedTransition(container, mergedTransition)
+            executeTransition.invoke()
             transitionImpl.setNameOverridesReordered(container, sharedElementFirstOutViews,
                 sharedElementLastInViews, inNames, sharedElementNameMapping)
             // Then, show all of the entering views, putting them into
@@ -925,11 +1011,6 @@
             setViewVisibility(enteringViews, View.VISIBLE)
             transitionImpl.swapSharedElementTargets(sharedElementTransition,
                 sharedElementFirstOutViews, sharedElementLastInViews)
-
-            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                Log.v(FragmentManager.TAG,
-                    "Completed executing operations from $firstOut to $lastIn")
-            }
         }
 
         override fun onCancel(container: ViewGroup) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java
index 0f0145d..8c7d538 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java
@@ -21,6 +21,7 @@
 import android.transition.Transition;
 import android.transition.TransitionManager;
 import android.transition.TransitionSet;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -221,6 +222,27 @@
     }
 
     @Override
+    public boolean isSeekingSupported() {
+        if (FragmentManager.isLoggingEnabled(Log.INFO)) {
+            Log.i(FragmentManager.TAG,
+                    "Predictive back not available using Framework Transitions. Please switch"
+                            + " to AndroidX Transition 1.5.0 or higher to enable seeking.");
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isSeekingSupported(@NonNull Object transition) {
+        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+            Log.v(FragmentManager.TAG,
+                    "Predictive back not available for framework transition "
+                            + transition + ". Please switch to AndroidX Transition 1.5.0 or higher "
+                            + "to enable seeking.");
+        }
+        return false;
+    }
+
+    @Override
     public void scheduleRemoveTargets(@NonNull final Object overallTransitionObj,
             @Nullable final Object enterTransition, @Nullable final ArrayList<View> enteringViews,
             @Nullable final Object exitTransition, @Nullable final ArrayList<View> exitingViews,
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java
index e9a7514..6a8d444 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java
@@ -21,6 +21,7 @@
 import android.annotation.SuppressLint;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewParent;
@@ -150,6 +151,49 @@
             @Nullable Object transition);
 
     /**
+     * Returns {@code true} if the Transition is seekable.
+     */
+    public boolean isSeekingSupported() {
+        if (FragmentManager.isLoggingEnabled(Log.INFO)) {
+            Log.i(FragmentManager.TAG,
+                    "Older versions of AndroidX Transition do not support seeking. Add dependency "
+                            + "on AndroidX Transition 1.5.0 or higher to enable seeking.");
+        }
+        return false;
+    }
+
+    /**
+     * Returns {@code true} if the Transition is seekable.
+     */
+    public boolean isSeekingSupported(@NonNull Object transition) {
+        return false;
+    }
+
+    /**
+     * Allows for controlling a seekable transition
+     */
+    @Nullable
+    public Object controlDelayedTransition(@NonNull ViewGroup sceneRoot,
+            @NonNull Object transition) {
+        return null;
+    }
+
+    /**
+     * Uses given progress to set the current play time of the transition.
+     */
+    public void setCurrentPlayTime(@NonNull Object transitionController, float progress) { }
+
+    /**
+     * Animate the transition to end.
+     */
+    public void animateToEnd(@NonNull Object transitionController) { }
+
+    /**
+     * Animate the transition to start.
+     */
+    public void animateToStart(@NonNull Object transitionController) { }
+
+    /**
      * Prepares for setting the shared element names by gathering the names of the incoming
      * shared elements and clearing them. {@link #setNameOverridesReordered(View, ArrayList,
      * ArrayList, ArrayList, Map)} must be called after this to complete setting the shared element
@@ -230,6 +274,32 @@
     public void setListenerForTransitionEnd(@NonNull final Fragment outFragment,
             @NonNull Object transition, @NonNull CancellationSignal signal,
             @NonNull Runnable transitionCompleteRunnable) {
+        setListenerForTransitionEnd(
+                outFragment, transition, signal, null, transitionCompleteRunnable
+        );
+    }
+
+    /**
+     * Set a listener for Transition end events. The default behavior immediately completes the
+     * transition.
+     *
+     * Use this when the given transition is seeking. The cancelRunnable should handle
+     * cleaning up the transition when seeking is cancelled.
+     *
+     * If the transition is not seeking, you should use
+     * {@link #setListenerForTransitionEnd(Fragment, Object, CancellationSignal, Runnable)}.
+     *
+     * @param outFragment The first fragment that is exiting
+     * @param transition all transitions to be executed on a single container
+     * @param signal used indicate the desired behavior on transition cancellation
+     * @param cancelRunnable runnable to handle the logic when the signal is cancelled
+     * @param transitionCompleteRunnable used to notify the FragmentManager when a transition is
+     *                                   complete
+     */
+    public void setListenerForTransitionEnd(@NonNull final Fragment outFragment,
+            @NonNull Object transition, @NonNull CancellationSignal signal,
+            @Nullable Runnable cancelRunnable,
+            @NonNull Runnable transitionCompleteRunnable) {
         transitionCompleteRunnable.run();
     }
 
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index e22d9a9..3444a5e 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -208,6 +208,21 @@
 
 }
 
+package androidx.glance.appwidget.component {
+
+  public final class ButtonsKt {
+    method @androidx.compose.runtime.Composable public static void CircleIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider? backgroundColor, optional androidx.glance.unit.ColorProvider contentColor);
+    method @androidx.compose.runtime.Composable public static void CircleIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider? backgroundColor, optional androidx.glance.unit.ColorProvider contentColor, optional String? key);
+    method @androidx.compose.runtime.Composable public static void FilledButton(String text, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional androidx.glance.ButtonColors colors, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void FilledButton(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional androidx.glance.ButtonColors colors, optional int maxLines, optional String? key);
+    method @androidx.compose.runtime.Composable public static void OutlineButton(String text, androidx.glance.unit.ColorProvider contentColor, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void OutlineButton(String text, androidx.glance.unit.ColorProvider contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional int maxLines, optional String? key);
+    method @androidx.compose.runtime.Composable public static void SquareIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider backgroundColor, optional androidx.glance.unit.ColorProvider contentColor);
+    method @androidx.compose.runtime.Composable public static void SquareIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider backgroundColor, optional androidx.glance.unit.ColorProvider contentColor, optional String? key);
+  }
+
+}
+
 package androidx.glance.appwidget.lazy {
 
   public abstract sealed class GridCells {
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index e22d9a9..3444a5e 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -208,6 +208,21 @@
 
 }
 
+package androidx.glance.appwidget.component {
+
+  public final class ButtonsKt {
+    method @androidx.compose.runtime.Composable public static void CircleIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider? backgroundColor, optional androidx.glance.unit.ColorProvider contentColor);
+    method @androidx.compose.runtime.Composable public static void CircleIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider? backgroundColor, optional androidx.glance.unit.ColorProvider contentColor, optional String? key);
+    method @androidx.compose.runtime.Composable public static void FilledButton(String text, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional androidx.glance.ButtonColors colors, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void FilledButton(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional androidx.glance.ButtonColors colors, optional int maxLines, optional String? key);
+    method @androidx.compose.runtime.Composable public static void OutlineButton(String text, androidx.glance.unit.ColorProvider contentColor, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void OutlineButton(String text, androidx.glance.unit.ColorProvider contentColor, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.ImageProvider? icon, optional int maxLines, optional String? key);
+    method @androidx.compose.runtime.Composable public static void SquareIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider backgroundColor, optional androidx.glance.unit.ColorProvider contentColor);
+    method @androidx.compose.runtime.Composable public static void SquareIconButton(androidx.glance.ImageProvider imageProvider, String? contentDescription, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.unit.ColorProvider backgroundColor, optional androidx.glance.unit.ColorProvider contentColor, optional String? key);
+  }
+
+}
+
 package androidx.glance.appwidget.lazy {
 
   public abstract sealed class GridCells {
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
index 9107a59..8eb950b 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
@@ -254,5 +254,32 @@
                 android:name="android.appwidget.provider"
                 android:resource="@xml/default_app_widget_info" />
         </receiver>
+
+        <receiver
+            android:name="androidx.glance.appwidget.demos.ButtonsWidgetBroadcastReceiver"
+            android:enabled="@bool/glance_appwidget_available"
+            android:exported="false"
+            android:label="@string/buttons_widget_name">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="android.intent.action.LOCALE_CHANGED" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
+
+        <receiver
+            android:name="androidx.glance.appwidget.demos.BackgroundTintWidgetBroadcastReceiver"
+            android:label="@string/tint_widget"
+            android:enabled="@bool/glance_appwidget_available"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
     </application>
 </manifest>
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/BackgroundTintWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/BackgroundTintWidget.kt
new file mode 100644
index 0000000..58f22cc
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/BackgroundTintWidget.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.demos
+
+import android.content.Context
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.ImageProvider
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.size
+import androidx.glance.unit.ColorProvider
+
+class BackgroundTintWidgetBroadcastReceiver() : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget
+        get() = BackgroundTintWidget()
+}
+
+/**
+ * Demonstrates tinting background drawables with [ColorFilter].
+ */
+class BackgroundTintWidget : GlanceAppWidget() {
+    override val sizeMode: SizeMode
+        get() = SizeMode.Exact
+
+    override suspend fun provideGlance(context: Context, id: GlanceId) {
+        provideContent {
+            GlanceTheme {
+                Column {
+                    Box(
+                        // Tint a <shape>
+                        modifier = GlanceModifier
+                            .size(width = 100.dp, height = 50.dp)
+                            .background(
+                                ImageProvider(R.drawable.shape_btn_demo),
+                                tint = ColorFilter.tint(GlanceTheme.colors.primary)
+                            ),
+                        content = {})
+                    Box(
+                        // tint an AVD
+                        modifier = GlanceModifier
+                            .size(width = 100.dp, height = 50.dp)
+                            .background(
+                                ImageProvider(R.drawable.ic_android),
+                                tint = ColorFilter.tint(ColorProvider(Color.Cyan))
+                            ),
+                        content = {}
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ButtonsWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ButtonsWidget.kt
new file mode 100644
index 0000000..a9d63be
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ButtonsWidget.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.demos
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.glance.Button
+import androidx.glance.ButtonColors
+import androidx.glance.ButtonDefaults
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.ImageProvider
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.component.CircleIconButton
+import androidx.glance.appwidget.component.FilledButton
+import androidx.glance.appwidget.component.OutlineButton
+import androidx.glance.appwidget.component.SquareIconButton
+import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.LazyItemScope
+import androidx.glance.appwidget.lazy.LazyListScope
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+
+class ButtonsWidgetBroadcastReceiver() : GlanceAppWidgetReceiver() {
+
+    override val glanceAppWidget: GlanceAppWidget
+        get() = ButtonsWidget()
+}
+
+/**
+ * Demonstrates different button styles. Outline buttons will render as standard buttons on
+ * apis <31.
+ */
+class ButtonsWidget() : GlanceAppWidget() {
+    override val sizeMode: SizeMode
+        get() = SizeMode.Exact // one callback each time widget resized
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    override suspend fun provideGlance(context: Context, id: GlanceId) {
+
+        provideContent {
+            val primary = GlanceTheme.colors.primary
+            val onPrimary = GlanceTheme.colors.onPrimary
+            val colors = ButtonDefaults.buttonColors(
+                backgroundColor = primary,
+                contentColor = onPrimary
+            )
+
+            LazyColumn(
+                modifier = GlanceModifier.fillMaxSize()
+                    .background(Color.DarkGray)
+                    .padding(16.dp)
+            ) {
+
+                paddedItem {
+                    Button(
+                        text = "Standard Button",
+                        onClick = {},
+                        modifier = GlanceModifier,
+                        colors = colors,
+                        maxLines = 1
+                    )
+                }
+
+                paddedItem {
+                    FilledButton(
+                        text = "Filled Button",
+                        colors = colors,
+                        modifier = GlanceModifier,
+                        onClick = {},
+                    )
+                }
+
+                paddedItem {
+                    FilledButton(
+                        text = "Filled Button",
+                        icon = ImageProvider(R.drawable.baseline_add_24),
+                        colors = colors,
+                        modifier = GlanceModifier,
+                        onClick = {},
+                    )
+                }
+
+                paddedItem {
+                    OutlineButton(
+                        text = "Outline Button",
+                        contentColor = primary,
+                        modifier = GlanceModifier,
+                        onClick = {},
+                    )
+                }
+
+                paddedItem {
+                    OutlineButton(
+                        text = "Outline Button",
+                        icon = ImageProvider(R.drawable.baseline_add_24),
+                        contentColor = primary,
+                        modifier = GlanceModifier,
+                        onClick = {},
+                    )
+                }
+
+                paddedItem {
+                    LongTextButtons(GlanceModifier, colors)
+                }
+
+                paddedItem {
+                    IconButtons()
+                }
+            } // end lazy column
+        }
+    }
+}
+
+private fun LazyListScope.paddedItem(content: @Composable LazyItemScope.() -> Unit) {
+    this.item {
+        Column {
+            content()
+            Space()
+        }
+    }
+}
+
+@Composable
+private fun LongTextButtons(modifier: GlanceModifier, colors: ButtonColors) {
+    Row(modifier = modifier) {
+        FilledButton(
+            text = "Three\nLines\nof text",
+            icon = ImageProvider(R.drawable.baseline_add_24),
+            colors = colors,
+            modifier = GlanceModifier,
+            onClick = {},
+        )
+
+        Space()
+
+        FilledButton(
+            text = "Two\nLines\nof text",
+            icon = ImageProvider(R.drawable.baseline_add_24),
+            colors = colors,
+            modifier = GlanceModifier,
+            onClick = {},
+            maxLines = 2
+        )
+    }
+}
+
+@Composable
+private fun IconButtons() {
+    Row(
+        modifier = GlanceModifier.height(80.dp).padding(vertical = 8.dp),
+        verticalAlignment = Alignment.Vertical.CenterVertically
+    ) {
+        SquareIconButton(
+            imageProvider = ImageProvider(R.drawable.baseline_add_24),
+            contentDescription = "Add Button",
+            onClick = { }
+        )
+        Space()
+
+        CircleIconButton(
+            imageProvider = ImageProvider(R.drawable.baseline_local_phone_24),
+            contentDescription = "Call Button",
+            backgroundColor = GlanceTheme.colors.surfaceVariant,
+            contentColor = GlanceTheme.colors.onSurfaceVariant,
+            onClick = { }
+        )
+        Space()
+
+        CircleIconButton(
+            imageProvider = ImageProvider(R.drawable.baseline_local_phone_24),
+            contentDescription = "Call Button",
+            backgroundColor = null, // empty background
+            contentColor = GlanceTheme.colors.primary,
+            onClick = { }
+        )
+    }
+}
+
+@Composable
+private fun Space() = Spacer(GlanceModifier.size(8.dp))
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_add_24.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_add_24.xml
new file mode 100644
index 0000000..03d6cd3
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_add_24.xml
@@ -0,0 +1,21 @@
+<!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<vector android:height="24dp" android:tint="#000000"
+    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="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_local_phone_24.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_local_phone_24.xml
new file mode 100644
index 0000000..95c5f8a
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/baseline_local_phone_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+    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="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
+</vector>
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/shape_btn_demo.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/shape_btn_demo.xml
new file mode 100644
index 0000000..12bdd47
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/drawable/shape_btn_demo.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <stroke android:color="#FF00FF"/>
+    <corners android:radius="16dp"/>
+
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
index e74682e..04943bf 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/res/values/strings.xml
@@ -37,5 +37,6 @@
     <string name="default_state_widget_name">Default State Widget</string>
     <string name="progress_indicator_widget_name">ProgressBar Widget</string>
     <string name="default_color_widget_name">Theme and Color Widget</string>
-
+    <string name="buttons_widget_name">Buttons Demo Widget</string>
+    <string name="tint_widget">Tint widget</string>
 </resources>
diff --git a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
index ffb947e..fb842fb 100644
--- a/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
+++ b/glance/glance-appwidget/src/androidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
@@ -26,14 +26,20 @@
 import androidx.glance.Button
 import androidx.glance.ButtonDefaults
 import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
 import androidx.glance.Image
 import androidx.glance.ImageProvider
 import androidx.glance.LocalContext
 import androidx.glance.action.actionStartActivity
+import androidx.glance.appwidget.component.CircleIconButton
+import androidx.glance.appwidget.component.FilledButton
+import androidx.glance.appwidget.component.OutlineButton
+import androidx.glance.appwidget.component.SquareIconButton
 import androidx.glance.appwidget.lazy.LazyColumn
 import androidx.glance.appwidget.test.R
 import androidx.glance.background
 import androidx.glance.color.ColorProvider
+import androidx.glance.color.colorProviders
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Box
 import androidx.glance.layout.Column
@@ -589,6 +595,43 @@
         mHostRule.waitForListViewChildCount(count)
         mScreenshotRule.checkScreenshot(mHostRule.mHostView, "lazyColumn_alignment_start")
     }
+
+    @Test
+    fun buttonTests_createFilledButton() {
+        TestGlanceAppWidget.uiDefinition = { ButtonComponentsScreenshotTests.FilledButtonTest() }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_createFilledButton")
+    }
+
+    @Test
+    fun buttonTests_createOutlineButton() {
+        TestGlanceAppWidget.uiDefinition = { ButtonComponentsScreenshotTests.OutlineButtonTest() }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_createOutlineButton")
+    }
+
+    @Test
+    fun buttonTests_createSquareButton() {
+        TestGlanceAppWidget.uiDefinition = { ButtonComponentsScreenshotTests.SquareButtonTest() }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_createSquareButton")
+    }
+
+    @Test
+    fun buttonTests_createCircleButton() {
+        TestGlanceAppWidget.uiDefinition = { ButtonComponentsScreenshotTests.CircleButtonTest() }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_createCircleButton")
+    }
+
+    @Test
+    fun buttonTests_buttonDefaultColors() {
+        TestGlanceAppWidget.uiDefinition = {
+            ButtonComponentsScreenshotTests.ButtonDefaultColorsTest()
+        }
+        mHostRule.startHost()
+        mScreenshotRule.checkScreenshot(mHostRule.mHostView, "buttonTests_buttonDefaultColors")
+    }
 }
 
 @Composable
@@ -872,3 +915,166 @@
         )
     }
 }
+
+// Tests the opinionated button components
+private object ButtonComponentsScreenshotTests {
+
+    private val onClick: () -> Unit = {}
+
+    // a filled square
+    private val icon = ImageProvider(R.drawable.filled_oval)
+
+    private val buttonBg = ColorProvider(Color.Black)
+    private val buttonFg = ColorProvider(Color.White)
+
+    @Composable
+    private fun colors() = ButtonDefaults.buttonColors(
+        backgroundColor = buttonBg,
+        contentColor = buttonFg
+    )
+
+    @Composable
+    private fun Space() = Spacer(GlanceModifier.size(16.dp))
+
+    /**
+     * A rectangular magenta background
+     */
+    @Composable
+    private fun Background(content: @Composable () -> Unit) {
+        Box(
+            modifier = GlanceModifier.wrapContentSize().padding(16.dp).background(Color.Magenta),
+            content = content
+        )
+    }
+
+    @Composable
+    fun FilledButtonTest() {
+        Background {
+            Column {
+                FilledButton(
+                    text = "Filled button\nbg 0x00, fg 0xff",
+                    onClick = onClick,
+                    icon = null,
+                    colors = colors()
+                )
+                Space()
+                FilledButton(
+                    text = "Filled btn + icon\nbg 0x00, fg 0xff",
+                    onClick = onClick,
+                    icon = icon,
+                    colors = colors()
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun OutlineButtonTest() {
+        Background {
+            Column {
+                OutlineButton(
+                    text = "Outline Button\nfg 0xff",
+                    onClick = onClick,
+                    icon = null,
+                    contentColor = buttonFg
+                )
+                Space()
+                OutlineButton(
+                    text = "Outline btn + icon\nfg 0xff",
+                    onClick = onClick,
+                    icon = icon,
+                    contentColor = buttonFg
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun SquareButtonTest() {
+        Background {
+            // square button with rounded corners, icon (a square w/sharp corners)  in center.
+            SquareIconButton(
+                imageProvider = icon,
+                contentDescription = null,
+                onClick = onClick,
+                backgroundColor = buttonBg,
+                contentColor = buttonFg
+            )
+        }
+    }
+
+    @Composable
+    fun CircleButtonTest() {
+        Background {
+            Column {
+                // Circle button with icon
+                CircleIconButton(
+                    imageProvider = icon,
+                    contentDescription = null,
+                    onClick = onClick,
+                    backgroundColor = buttonBg,
+                    contentColor = buttonFg
+                )
+                Space()
+                // Icon only, no background
+                CircleIconButton(
+                    imageProvider = icon,
+                    contentDescription = null,
+                    onClick = onClick,
+                    backgroundColor = null,
+                    contentColor = buttonFg
+                )
+            }
+        }
+    }
+
+    /**
+     * Tests that buttons inherit the expected colors from their theme.
+     */
+    @Composable
+    fun ButtonDefaultColorsTest() {
+        val unused = ColorProvider(Color.Cyan)
+
+        val colors = colorProviders(
+            primary = ColorProvider(Color.Green),
+            onPrimary = ColorProvider(Color.Black),
+            surface = ColorProvider(Color.Gray),
+            onSurface = ColorProvider(Color.Red),
+            background = ColorProvider(Color.DarkGray),
+            error = unused,
+            errorContainer = unused,
+            inverseOnSurface = unused,
+            inversePrimary = unused,
+            inverseSurface = unused,
+            onBackground = unused,
+            onError = unused,
+            onErrorContainer = unused,
+            onPrimaryContainer = unused,
+            onSecondary = unused,
+            onSecondaryContainer = unused,
+            onSurfaceVariant = unused,
+            onTertiary = unused,
+            onTertiaryContainer = unused,
+            outline = unused,
+            primaryContainer = unused,
+            secondary = unused,
+            secondaryContainer = unused,
+            surfaceVariant = unused,
+            tertiary = unused,
+            tertiaryContainer = unused,
+        )
+
+        GlanceTheme(
+            colors = colors
+        ) {
+            Column {
+                FilledButton("Filled button", icon = icon, onClick = onClick)
+                // [OutlineButton] does not have a default color, so not important to test here
+                Space()
+                SquareIconButton(imageProvider = icon, contentDescription = null, onClick = onClick)
+                Space()
+                CircleIconButton(imageProvider = icon, contentDescription = null, onClick = onClick)
+            }
+        }
+    }
+}
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/ApplyModifiers.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/ApplyModifiers.kt
index 51d1774..0c43fc1 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/ApplyModifiers.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/ApplyModifiers.kt
@@ -79,28 +79,33 @@
                 }
                 actionModifier = modifier
             }
+
             is WidthModifier -> widthModifier = modifier
             is HeightModifier -> heightModifier = modifier
             is BackgroundModifier -> applyBackgroundModifier(context, rv, modifier, viewDef)
             is PaddingModifier -> {
                 paddingModifiers = paddingModifiers?.let { it + modifier } ?: modifier
             }
+
             is VisibilityModifier -> visibility = modifier.visibility
             is CornerRadiusModifier -> cornerRadius = modifier.radius
             is AppWidgetBackgroundModifier -> {
                 // This modifier is handled somewhere else.
             }
+
             is SelectableGroupModifier -> {
                 if (!translationContext.canUseSelectableGroup) {
                     error(
                         "GlanceModifier.selectableGroup() can only be used on Row or Column " +
-                        "composables."
+                            "composables."
                     )
                 }
             }
+
             is AlignmentModifier -> {
                 // This modifier is handled somewhere else.
             }
+
             is ClipToOutlineModifier -> clipToOutline = modifier
             is EnabledModifier -> enabled = modifier
             is SemanticsModifier -> semanticsModifier = modifier
@@ -226,7 +231,8 @@
     }
     // Wrap and Expand are done in XML on Android S & Sv2
     if (Build.VERSION.SDK_INT < 33 &&
-        width in listOf(Dimension.Wrap, Dimension.Expand)) return
+        width in listOf(Dimension.Wrap, Dimension.Expand)
+    ) return
     ApplyModifiersApi31Impl.setViewWidth(rv, viewId, width)
 }
 
@@ -256,7 +262,8 @@
     }
     // Wrap and Expand are done in XML on Android S & Sv2
     if (Build.VERSION.SDK_INT < 33 &&
-        height in listOf(Dimension.Wrap, Dimension.Expand)) return
+        height in listOf(Dimension.Wrap, Dimension.Expand)
+    ) return
     ApplyModifiersApi31Impl.setViewHeight(rv, viewId, height)
 }
 
@@ -267,8 +274,9 @@
     viewDef: InsertedViewInfo
 ) {
     val viewId = viewDef.mainViewId
-    val imageProvider = modifier.imageProvider
-    if (imageProvider != null) {
+
+    fun applyBackgroundImageModifier(modifier: BackgroundModifier.Image) {
+        val imageProvider = modifier.imageProvider
         if (imageProvider is AndroidResourceImageProvider) {
             rv.setViewBackgroundResource(viewId, imageProvider.resId)
         }
@@ -276,24 +284,41 @@
         // (removing modifiers is not really possible).
         return
     }
-    when (val colorProvider = modifier.colorProvider) {
-        is FixedColorProvider -> rv.setViewBackgroundColor(viewId, colorProvider.color.toArgb())
-        is ResourceColorProvider -> rv.setViewBackgroundColorResource(
-            viewId,
-            colorProvider.resId
-        )
-        is DayNightColorProvider -> {
-            if (Build.VERSION.SDK_INT >= 31) {
-                rv.setViewBackgroundColor(
-                    viewId,
-                    colorProvider.day.toArgb(),
-                    colorProvider.night.toArgb()
-                )
-            } else {
-                rv.setViewBackgroundColor(viewId, colorProvider.getColor(context).toArgb())
+
+    fun applyBackgroundColorModifier(modifier: BackgroundModifier.Color) {
+        when (val colorProvider = modifier.colorProvider) {
+            is FixedColorProvider -> rv.setViewBackgroundColor(
+                viewId,
+                colorProvider.color.toArgb()
+            )
+
+            is ResourceColorProvider -> rv.setViewBackgroundColorResource(
+                viewId,
+                colorProvider.resId
+            )
+
+            is DayNightColorProvider -> {
+                if (Build.VERSION.SDK_INT >= 31) {
+                    rv.setViewBackgroundColor(
+                        viewId,
+                        colorProvider.day.toArgb(),
+                        colorProvider.night.toArgb()
+                    )
+                } else {
+                    rv.setViewBackgroundColor(viewId, colorProvider.getColor(context).toArgb())
+                }
             }
+
+            else -> Log.w(
+                GlanceAppWidgetTag,
+                "Unexpected background color modifier: $colorProvider"
+            )
         }
-        else -> Log.w(GlanceAppWidgetTag, "Unexpected background color modifier: $colorProvider")
+    }
+
+    when (modifier) {
+        is BackgroundModifier.Image -> applyBackgroundImageModifier(modifier)
+        is BackgroundModifier.Color -> applyBackgroundColorModifier(modifier)
     }
 }
 
@@ -319,6 +344,7 @@
             is Dimension.Wrap -> {
                 rv.setViewLayoutWidth(viewId, WRAP_CONTENT.toFloat(), COMPLEX_UNIT_PX)
             }
+
             is Dimension.Expand -> rv.setViewLayoutWidth(viewId, 0f, COMPLEX_UNIT_PX)
             is Dimension.Dp -> rv.setViewLayoutWidth(viewId, width.dp.value, COMPLEX_UNIT_DIP)
             is Dimension.Resource -> rv.setViewLayoutWidthDimen(viewId, width.res)
@@ -334,6 +360,7 @@
             is Dimension.Wrap -> {
                 rv.setViewLayoutHeight(viewId, WRAP_CONTENT.toFloat(), COMPLEX_UNIT_PX)
             }
+
             is Dimension.Expand -> rv.setViewLayoutHeight(viewId, 0f, COMPLEX_UNIT_PX)
             is Dimension.Dp -> rv.setViewLayoutHeight(viewId, height.dp.value, COMPLEX_UNIT_DIP)
             is Dimension.Resource -> rv.setViewLayoutHeightDimen(viewId, height.res)
@@ -350,9 +377,11 @@
             is Dimension.Dp -> {
                 rv.setViewOutlinePreferredRadius(viewId, radius.dp.value, COMPLEX_UNIT_DIP)
             }
+
             is Dimension.Resource -> {
                 rv.setViewOutlinePreferredRadiusDimen(viewId, radius.res)
             }
+
             else -> error("Rounded corners should not be ${radius.javaClass.canonicalName}")
         }
     }
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt
index 2b86729..7a1f57e 100644
--- a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/NormalizeCompositionTree.kt
@@ -28,6 +28,9 @@
 import androidx.glance.ImageProvider
 import androidx.glance.action.ActionModifier
 import androidx.glance.action.LambdaAction
+import androidx.glance.action.NoRippleOverride
+import androidx.glance.addChild
+import androidx.glance.addChildIfNotNull
 import androidx.glance.appwidget.action.CompoundButtonAction
 import androidx.glance.extractModifier
 import androidx.glance.findModifier
@@ -141,7 +144,8 @@
             child.modifier.extractLambdaAction()
         if (action != null &&
             child !is EmittableSizeBox &&
-            child !is EmittableLazyItemWithChildren) {
+            child !is EmittableLazyItemWithChildren
+        ) {
             val newKey = action.key + "+$index"
             val newAction = LambdaAction(newKey, action.block)
             actions.getOrPut(newKey) { mutableListOf() }.add(newAction)
@@ -162,6 +166,7 @@
             action is LambdaAction -> action to modifiers
             action is CompoundButtonAction && action.innerAction is LambdaAction ->
                 action.innerAction to modifiers
+
             else -> null to modifiers
         }
     }
@@ -198,11 +203,11 @@
         // before the target in the wrapper box. This allows us to support content scale as well as
         // can help support additional processing on background images. Note: Button's don't support
         // bg image modifier.
-        (it is BackgroundModifier && it.imageProvider != null) ||
-        // R- buttons are implemented using box, images and text.
-        (isButton && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) ||
-         // Ripples are implemented by placing a drawable after the target in the wrapper box.
-        (it is ActionModifier && !hasBuiltinRipple())
+        (it is BackgroundModifier.Image) ||
+            // R- buttons are implemented using box, images and text.
+            (isButton && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) ||
+            // Ripples are implemented by placing a drawable after the target in the wrapper box.
+            (it is ActionModifier && !hasBuiltinRipple())
     }
     if (!shouldWrapTargetInABox) return target
 
@@ -227,7 +232,7 @@
                 // drawable's base was white/none, applying transparent tint will lead to black
                 // color. This shouldn't be issue for icon type drawables, but in this case we are
                 // emulating colored outline. So, we apply tint as well as alpha.
-                bgModifier.colorProvider?.let {
+                (bgModifier as? BackgroundModifier.Color)?.colorProvider?.let {
                     colorFilterParams = TintAndAlphaColorFilterParams(it)
                 }
                 contentScale = ContentScale.FillBounds
@@ -237,14 +242,19 @@
             // is applied back to the target. Note: We could have hoisted the bg color to box
             // instead of adding it back to the target, but for buttons, we also add an outline
             // background to the box.
-            if (bgModifier.imageProvider != null) {
-                backgroundImage = EmittableImage().apply {
-                    modifier = GlanceModifier.fillMaxSize()
-                    provider = bgModifier.imageProvider
-                    contentScale = bgModifier.contentScale
+            when (bgModifier) {
+                is BackgroundModifier.Image -> {
+                    backgroundImage = EmittableImage().apply {
+                        modifier = GlanceModifier.fillMaxSize()
+                        provider = bgModifier.imageProvider
+                        contentScale = bgModifier.contentScale
+                        colorFilterParams = bgModifier.colorFilter?.colorFilterParams
+                    }
                 }
-            } else { // is a background color modifier
-                targetModifiers += bgModifier
+
+                is BackgroundModifier.Color -> {
+                    targetModifiers += bgModifier
+                }
             }
         }
     }
@@ -256,9 +266,15 @@
         targetModifiersMinusBg.extractModifier<ActionModifier>()
     boxModifiers += actionModifier
     if (actionModifier != null && !hasBuiltinRipple()) {
+        val maybeRippleOverride = actionModifier.rippleOverride
         val rippleImageProvider =
-            if (isButton) ImageProvider(R.drawable.glance_button_ripple)
-            else ImageProvider(R.drawable.glance_ripple)
+            if (maybeRippleOverride != NoRippleOverride) {
+                ImageProvider(maybeRippleOverride)
+            } else if (isButton) {
+                ImageProvider(R.drawable.glance_button_ripple)
+            } else {
+                ImageProvider(R.drawable.glance_ripple)
+            }
         rippleImage = EmittableImage().apply {
             modifier = GlanceModifier.fillMaxSize()
             provider = rippleImageProvider
@@ -282,22 +298,24 @@
 
     return EmittableBox().apply {
         modifier = boxModifiers.collect()
+        target.modifier = targetModifiers.collect()
+
         if (isButton) contentAlignment = Alignment.Center
 
-        backgroundImage?.let { children += it }
-        children += target.apply { modifier = targetModifiers.collect() }
-        rippleImage?.let { children += it }
+        addChildIfNotNull(backgroundImage)
+        addChild(target)
+        addChildIfNotNull(rippleImage)
     }
 }
 
 private fun Emittable.hasBuiltinRipple() =
     this is EmittableSwitch ||
-    this is EmittableRadioButton ||
-    this is EmittableCheckBox ||
-     // S+ versions use a native button with fixed rounded corners and matching ripple set in
-     // layout xml. In R- versions, buttons are implemented using a background drawable with
-     // rounded corners and an EmittableText in R- versions.
-    (this is EmittableButton && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+        this is EmittableRadioButton ||
+        this is EmittableCheckBox ||
+        // S+ versions use a native button with fixed rounded corners and matching ripple set in
+        // layout xml. In R- versions, buttons are implemented using a background drawable with
+        // rounded corners and an EmittableText in R- versions.
+        (this is EmittableButton && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
 
 private data class ExtractedSizeModifiers(
     val sizeModifiers: GlanceModifier = GlanceModifier,
@@ -313,7 +331,8 @@
         foldIn(ExtractedSizeModifiers()) { acc, modifier ->
             if (modifier is WidthModifier ||
                 modifier is HeightModifier ||
-                modifier is CornerRadiusModifier) {
+                modifier is CornerRadiusModifier
+            ) {
                 acc.copy(sizeModifiers = acc.sizeModifiers.then(modifier))
             } else {
                 acc.copy(nonSizeModifiers = acc.nonSizeModifiers.then(modifier))
diff --git a/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/component/Buttons.kt b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/component/Buttons.kt
new file mode 100644
index 0000000..e2c6559
--- /dev/null
+++ b/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/component/Buttons.kt
@@ -0,0 +1,467 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.glance.appwidget.component
+
+import android.os.Build
+import androidx.annotation.DimenRes
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.Button
+import androidx.glance.ButtonColors
+import androidx.glance.ButtonDefaults
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.action.Action
+import androidx.glance.action.NoRippleOverride
+import androidx.glance.action.action
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.R
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.enabled
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.layout.width
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
+
+/**
+ * A button styled per Material3. It has a filled background. It is more opinionated than [Button]
+ * and suitable for uses where M3 is preferred.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param icon An optional leading icon placed before the text.
+ * @param colors The colors to use for the background and content of the button.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ * @param key A stable and unique key that identifies the action for this button. This ensures
+ * that the correct action is triggered, especially in cases of items that change order. If not
+ * provided we use the key that is automatically generated by the Compose runtime, which is unique
+ * for every exact code location in the composition tree.
+ */
+@Composable
+fun FilledButton(
+    text: String,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider? = null,
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
+    maxLines: Int = Int.MAX_VALUE,
+    key: String? = null
+) = FilledButton(
+    text = text,
+    onClick = action(block = onClick, key = key),
+    modifier = modifier,
+    enabled = enabled,
+    icon = icon,
+    colors = colors,
+    maxLines = maxLines,
+)
+
+/**
+ * A button styled per Material3. It has a filled background. It is more opinionated than [Button]
+ * and suitable for uses where M3 is preferred.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param icon An optional leading icon placed before the text.
+ * @param colors The colors to use for the background and content of the button.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ */
+@Composable
+fun FilledButton(
+    text: String,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider? = null,
+    colors: ButtonColors = ButtonDefaults.buttonColors(),
+    maxLines: Int = Int.MAX_VALUE,
+) = M3TextButton(
+    text = text,
+    modifier = modifier,
+    enabled = enabled,
+    icon = icon,
+    contentColor = colors.contentColor,
+    backgroundTint = colors.backgroundColor,
+    backgroundResource = R.drawable.glance_component_btn_filled,
+    onClick = onClick,
+    maxLines = maxLines,
+)
+
+/**
+ * An outline button styled per Material3. It has a transparent background. It is more opinionated
+ * than [Button] and suitable for uses where M3 is preferred.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param icon An optional leading icon placed before the text.
+ * @param contentColor The color used for the text, optional icon tint, and outline.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ * @param key A stable and unique key that identifies the action for this button. This ensures
+ * that the correct action is triggered, especially in cases of items that change order. If not
+ * provided we use the key that is automatically generated by the Compose runtime, which is unique
+ * for every exact code location in the composition tree.
+ */
+@Composable
+fun OutlineButton(
+    text: String,
+    contentColor: ColorProvider,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider? = null,
+    maxLines: Int = Int.MAX_VALUE,
+    key: String? = null
+) = OutlineButton(
+    text = text,
+    contentColor = contentColor,
+    onClick = action(block = onClick, key = key),
+    modifier = modifier,
+    enabled = enabled,
+    icon = icon,
+    maxLines = maxLines,
+)
+
+/**
+ * An outline button styled per Material3. It has a transparent background. It is more opinionated
+ * than [Button] and suitable for uses where M3 is preferred.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param icon An optional leading icon placed before the text.
+ * @param contentColor The color used for the text, optional icon tint, and outline.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ */
+@Composable
+fun OutlineButton(
+    text: String,
+    contentColor: ColorProvider,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider? = null,
+    maxLines: Int = Int.MAX_VALUE,
+) {
+    val bg: ColorProvider = contentColor
+    val fg: ColorProvider = contentColor
+
+    M3TextButton(
+        text = text,
+        onClick = onClick,
+        modifier = modifier,
+        enabled = enabled,
+        icon = icon,
+        contentColor = fg,
+        backgroundResource = R.drawable.glance_component_btn_outline,
+        backgroundTint = bg,
+        maxLines = maxLines,
+    )
+}
+
+/**
+ * Intended to fill the role of primary icon button or fab.
+ *
+ * @param imageProvider the icon to be drawn in the button
+ * @param contentDescription Text used by accessibility services to describe what this image
+ * represents. This text should be localized, such as by using
+ * androidx.compose.ui.res.stringResource or similar
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param backgroundColor The color to tint the button's background.
+ * @param contentColor The color to tint the button's icon.
+ * @param key A stable and unique key that identifies the action for this button. This ensures
+ * that the correct action is triggered, especially in cases of items that change order. If not
+ * provided we use the key that is automatically generated by the Compose runtime, which is unique
+ * for every exact code location in the composition tree.
+ */
+@Composable
+fun SquareIconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    backgroundColor: ColorProvider = GlanceTheme.colors.primary,
+    contentColor: ColorProvider = GlanceTheme.colors.onPrimary,
+    key: String? = null
+) = SquareIconButton(
+    imageProvider = imageProvider,
+    contentDescription = contentDescription,
+    onClick = action(block = onClick, key = key),
+    modifier = modifier,
+    enabled = enabled,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+)
+
+/**
+ * Intended to fill the role of primary icon button or fab.
+ *
+ * @param imageProvider the icon to be drawn in the button
+ * @param contentDescription Text used by accessibility services to describe what this image
+ * represents. This text should be localized, such as by using
+ * androidx.compose.ui.res.stringResource or similar
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param backgroundColor The color to tint the button's background.
+ * @param contentColor The color to tint the button's icon.
+ */
+@Composable
+fun SquareIconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    backgroundColor: ColorProvider = GlanceTheme.colors.primary,
+    contentColor: ColorProvider = GlanceTheme.colors.onPrimary,
+) = M3IconButton(
+    imageProvider = imageProvider,
+    contentDescription = contentDescription,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+    shape = IconButtonShape.Square,
+    modifier = modifier,
+    enabled = enabled,
+    onClick = onClick,
+)
+
+/**
+ * Intended to fill the role of secondary icon button.
+ * Background color may be null to have the button display as an icon with a 48x48dp hit area.
+ *
+ * @param imageProvider the icon to be drawn in the button
+ * @param contentDescription Text used by accessibility services to describe what this image
+ * represents. This text should be localized, such as by using
+ * androidx.compose.ui.res.stringResource or similar
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param backgroundColor The color to tint the button's background. May be null to make background
+ * transparent.
+ * @param contentColor The color to tint the button's icon.
+ * @param key A stable and unique key that identifies the action for this button. This ensures
+ * that the correct action is triggered, especially in cases of items that change order. If not
+ * provided we use the key that is automatically generated by the Compose runtime, which is unique
+ * for every exact code location in the composition tree.
+ */
+@Composable
+fun CircleIconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    backgroundColor: ColorProvider? = GlanceTheme.colors.background,
+    contentColor: ColorProvider = GlanceTheme.colors.onSurface,
+    key: String? = null
+) = CircleIconButton(
+    imageProvider = imageProvider,
+    contentDescription = contentDescription,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+    modifier = modifier,
+    enabled = enabled,
+    onClick = action(block = onClick, key = key)
+)
+
+/**
+ * Intended to fill the role of secondary icon button.
+ * Background color may be null to have the button display as an icon with a 48x48dp hit area.
+ *
+ * @param imageProvider the icon to be drawn in the button
+ * @param contentDescription Text used by accessibility services to describe what this image
+ * represents. This text should be localized, such as by using
+ * androidx.compose.ui.res.stringResource or similar
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param backgroundColor The color to tint the button's background. May be null to make background
+ * transparent.
+ * @param contentColor The color to tint the button's icon.
+ */
+@Composable
+fun CircleIconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    backgroundColor: ColorProvider? = GlanceTheme.colors.background,
+    contentColor: ColorProvider = GlanceTheme.colors.onSurface,
+) = M3IconButton(
+    imageProvider = imageProvider,
+    contentDescription = contentDescription,
+    backgroundColor = backgroundColor,
+    contentColor = contentColor,
+    shape = IconButtonShape.Circle,
+    modifier = modifier,
+    enabled = enabled,
+    onClick = onClick,
+)
+
+private enum class IconButtonShape(
+    @DrawableRes val shape: Int,
+    @DimenRes val cornerRadius: Int,
+    @DrawableRes val ripple: Int,
+    val defaultSize: Dp
+) {
+    Square(
+        R.drawable.glance_component_btn_square,
+        R.dimen.glance_component_square_icon_button_corners,
+        ripple = if (isAtLeastApi31) NoRippleOverride
+            else R.drawable.glance_component_square_button_ripple,
+        defaultSize = 60.dp
+    ),
+    Circle(
+        R.drawable.glance_component_btn_circle,
+        R.dimen.glance_component_circle_icon_button_corners,
+        ripple = if (isAtLeastApi31) NoRippleOverride
+            else R.drawable.glance_component_circle_button_ripple,
+        defaultSize = 48.dp
+    )
+}
+
+@Composable
+private fun M3IconButton(
+    imageProvider: ImageProvider,
+    contentDescription: String?,
+    contentColor: ColorProvider,
+    backgroundColor: ColorProvider?,
+    shape: IconButtonShape,
+    onClick: Action,
+    modifier: GlanceModifier,
+    enabled: Boolean,
+) {
+
+    val backgroundModifier = if (backgroundColor == null)
+        GlanceModifier
+    else GlanceModifier.background(
+        ImageProvider(shape.shape),
+        tint = ColorFilter.tint(backgroundColor)
+    )
+
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = GlanceModifier
+            .size(shape.defaultSize) // acts as a default if not overridden by [modifier]
+            .then(modifier)
+            .then(backgroundModifier)
+            .clickable(onClick = onClick, rippleOverride = shape.ripple)
+            .enabled(enabled)
+            .then(maybeRoundCorners(shape.cornerRadius))
+    ) {
+        Image(
+            provider = imageProvider,
+            contentDescription = contentDescription,
+            colorFilter = ColorFilter.tint(contentColor),
+            modifier = GlanceModifier.size(24.dp)
+        )
+    }
+}
+
+@Composable
+private fun M3TextButton(
+    text: String,
+    onClick: Action,
+    modifier: GlanceModifier,
+    enabled: Boolean = true,
+    icon: ImageProvider?,
+    contentColor: ColorProvider,
+    @DrawableRes backgroundResource: Int,
+    backgroundTint: ColorProvider,
+    maxLines: Int,
+) {
+    val iconSize = 18.dp
+    val totalHorizontalPadding = if (icon != null) 24.dp else 16.dp
+
+    val Text = @Composable {
+        Text(
+            text = text,
+            style = TextStyle(color = contentColor, fontSize = 14.sp, FontWeight.Medium),
+            maxLines = maxLines
+        )
+    }
+
+    Box(
+        modifier = modifier
+            .padding(start = 16.dp, end = totalHorizontalPadding, top = 10.dp, bottom = 10.dp)
+            .background(ImageProvider(backgroundResource), tint = ColorFilter.tint(backgroundTint))
+            .enabled(enabled)
+            .clickable(
+                onClick = onClick,
+                rippleOverride = if (isAtLeastApi31) NoRippleOverride
+                else R.drawable.glance_component_m3_button_ripple
+            )
+            .then(maybeRoundCorners(R.dimen.glance_component_button_corners)),
+        contentAlignment = Alignment.Center
+    ) {
+
+        if (icon != null) {
+            Row(verticalAlignment = Alignment.Vertical.CenterVertically) {
+                Image(
+                    provider = icon,
+                    contentDescription = null,
+                    colorFilter = ColorFilter.tint(contentColor),
+                    modifier = GlanceModifier.size(iconSize)
+                ) // TODO: do we need a content description for a button icon?
+                Spacer(GlanceModifier.width(8.dp))
+                Text()
+            }
+        } else {
+            Box(GlanceModifier.size(iconSize)) {
+                // for accessibility only: force button to be the same min height as the icon
+                // version.
+                // remove once b/290677181 is addressed
+            }
+            Text()
+        }
+    }
+}
+
+private val isAtLeastApi31 get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+private fun maybeRoundCorners(@DimenRes radius: Int) =
+    if (isAtLeastApi31)
+        GlanceModifier.cornerRadius(radius)
+    else GlanceModifier
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_circle.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_circle.xml
new file mode 100644
index 0000000..efdd739
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_circle.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <size
+        android:width="48dp"
+        android:height="48dp" />
+    <solid android:color="@android:color/black" />
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_filled.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_filled.xml
new file mode 100644
index 0000000..d267cc2
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_filled.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@android:color/black" />
+    <corners android:radius="@dimen/glance_component_button_corners"/>
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_outline.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_outline.xml
new file mode 100644
index 0000000..e8b905f
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_outline.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="@dimen/glance_component_button_corners"/>
+    <stroke android:width="1dp" android:color="@android:color/black" />
+    <solid android:color="@android:color/transparent" />
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_square.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_square.xml
new file mode 100644
index 0000000..df423d0
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_btn_square.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="@dimen/glance_component_square_icon_button_corners" />
+    <solid android:color="@android:color/black" />
+</shape>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_circle_button_ripple.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_circle_button_ripple.xml
new file mode 100644
index 0000000..5c3c1e4
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_circle_button_ripple.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
+<!-- Fixed radius ripple matching the button's outline -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/glance_component_circle_icon_button_corners"/>
+            <solid android:color="@android:color/white"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_m3_button_ripple.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_m3_button_ripple.xml
new file mode 100644
index 0000000..8150c36
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_m3_button_ripple.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
+<!-- Fixed radius ripple matching the button's outline -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/glance_component_button_corners"/>
+            <solid android:color="@android:color/white"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/drawable/glance_component_square_button_ripple.xml b/glance/glance-appwidget/src/main/res/drawable/glance_component_square_button_ripple.xml
new file mode 100644
index 0000000..db64c61
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/drawable/glance_component_square_button_ripple.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  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.
+-->
+<!-- Fixed radius ripple matching the button's outline -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/glance_component_square_icon_button_corners"/>
+            <solid android:color="@android:color/white"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/main/res/values/glance_component_dimens.xml b/glance/glance-appwidget/src/main/res/values/glance_component_dimens.xml
new file mode 100644
index 0000000..5a4c16b
--- /dev/null
+++ b/glance/glance-appwidget/src/main/res/values/glance_component_dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2023 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <dimen name="glance_component_button_corners">24dp</dimen>
+    <dimen name="glance_component_square_icon_button_corners">16dp</dimen>
+    <dimen name="glance_component_circle_icon_button_corners">9999.dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 3d90666..72071c9 100644
--- a/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/main/java/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
+@file:Suppress("deprecation")
 package androidx.glance.wear.tiles
 
 import android.content.Context
@@ -76,6 +76,7 @@
 import androidx.glance.wear.tiles.curved.SweepAngleModifier
 import androidx.glance.wear.tiles.curved.ThicknessModifier
 import androidx.glance.wear.tiles.curved.findModifier
+import androidx.wear.tiles.ColorBuilders
 import java.io.ByteArrayOutputStream
 import java.util.Arrays
 
@@ -126,12 +127,13 @@
 @Suppress("deprecation") // for backward compatibility
 private fun BackgroundModifier.toProto(
     context: Context
-): androidx.wear.tiles.ModifiersBuilders.Background? =
-    this.colorProvider?.let { provider ->
+): androidx.wear.tiles.ModifiersBuilders.Background? = when (this) {
+    is BackgroundModifier.Color ->
         androidx.wear.tiles.ModifiersBuilders.Background.Builder()
-            .setColor(androidx.wear.tiles.ColorBuilders.argb(provider.getColorAsArgb(context)))
+            .setColor(ColorBuilders.argb(this.colorProvider.getColorAsArgb(context)))
             .build()
-    }
+    else -> error("Unexpected modifier $this")
+}
 
 @Suppress("deprecation") // for backward compatibility
 private fun BorderModifier.toProto(context: Context): androidx.wear.tiles.ModifiersBuilders.Border =
diff --git a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/BackgroundTest.kt b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/BackgroundTest.kt
index 8018dd2..6b9a108 100644
--- a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/BackgroundTest.kt
+++ b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/BackgroundTest.kt
@@ -19,9 +19,13 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
 import androidx.glance.BackgroundModifier
+import androidx.glance.ColorFilter
 import androidx.glance.GlanceModifier
+import androidx.glance.ImageProvider
+import androidx.glance.TintColorFilterParams
 import androidx.glance.background
 import androidx.glance.findModifier
+import androidx.glance.unit.ColorProvider
 import androidx.glance.unit.FixedColorProvider
 import androidx.glance.unit.ResourceColorProvider
 import androidx.glance.wear.tiles.test.R
@@ -32,9 +36,10 @@
 class BackgroundTest {
     @Test
     fun canUseBackgroundModifier() {
-        val modifier = GlanceModifier.background(color = Color(0xFF223344))
+        val modifier: GlanceModifier = GlanceModifier.background(color = Color(0xFF223344))
 
-        val addedModifier = requireNotNull(modifier.findModifier<BackgroundModifier>())
+        val addedModifier: BackgroundModifier.Color =
+            requireNotNull(modifier.findModifier<BackgroundModifier.Color>())
 
         val modifierColors = addedModifier.colorProvider
         assertIs<FixedColorProvider>(modifierColors)
@@ -48,10 +53,30 @@
     fun canUseBackgroundModifier_resId() {
         val modifier = GlanceModifier.background(color = R.color.color1)
 
-        val addedModifier = requireNotNull(modifier.findModifier<BackgroundModifier>())
+        val addedModifier: BackgroundModifier.Color =
+            requireNotNull(modifier.findModifier<BackgroundModifier.Color>())
 
         val modifierColors = addedModifier.colorProvider
         assertIs<ResourceColorProvider>(modifierColors)
         assertThat(modifierColors.resId).isEqualTo(R.color.color1)
     }
+
+    @Test
+    fun canUseBackgroundModifier_colorFilteredImage() {
+        fun tintColor() = ColorProvider(Color.Magenta)
+
+        val modifier = GlanceModifier.background(
+            ImageProvider(R.drawable.oval), ColorFilter.tint(
+                tintColor()
+            )
+        )
+
+        val addedModifier: BackgroundModifier.Image =
+            requireNotNull(modifier.findModifier<BackgroundModifier.Image>())
+
+        assertThat(
+            (addedModifier.colorFilter?.colorFilterParams as TintColorFilterParams).colorProvider)
+            .isEqualTo(tintColor()
+        )
+    }
 }
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 818bc2f..e87350d 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -2,6 +2,7 @@
 package androidx.glance {
 
   public final class BackgroundKt {
+    method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.ImageProvider imageProvider, androidx.glance.ColorFilter? tint, optional int contentScale);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.ImageProvider imageProvider, optional int contentScale);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.unit.ColorProvider colorProvider);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, @ColorRes int color);
@@ -128,8 +129,11 @@
 
   public final class ActionKt {
     method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.glance.ExperimentalGlanceApi public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional String? key, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick, optional @DrawableRes int rippleOverride);
+    method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional @DrawableRes int rippleOverride, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.glance.ExperimentalGlanceApi public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional String? key, optional @DrawableRes int rippleOverride, kotlin.jvm.functions.Function0<kotlin.Unit> block);
     method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    field @DrawableRes public static final int NoRippleOverride = 0; // 0x0
   }
 
   public abstract class ActionParameters {
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 818bc2f..e87350d 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -2,6 +2,7 @@
 package androidx.glance {
 
   public final class BackgroundKt {
+    method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.ImageProvider imageProvider, androidx.glance.ColorFilter? tint, optional int contentScale);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.ImageProvider imageProvider, optional int contentScale);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, androidx.glance.unit.ColorProvider colorProvider);
     method public static androidx.glance.GlanceModifier background(androidx.glance.GlanceModifier, @ColorRes int color);
@@ -128,8 +129,11 @@
 
   public final class ActionKt {
     method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.glance.ExperimentalGlanceApi public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional String? key, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick, optional @DrawableRes int rippleOverride);
+    method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional @DrawableRes int rippleOverride, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.glance.ExperimentalGlanceApi public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, optional String? key, optional @DrawableRes int rippleOverride, kotlin.jvm.functions.Function0<kotlin.Unit> block);
     method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    field @DrawableRes public static final int NoRippleOverride = 0; // 0x0
   }
 
   public abstract class ActionParameters {
diff --git a/glance/glance/src/main/java/androidx/glance/Background.kt b/glance/glance/src/main/java/androidx/glance/Background.kt
index 67e1195..4b254af 100644
--- a/glance/glance/src/main/java/androidx/glance/Background.kt
+++ b/glance/glance/src/main/java/androidx/glance/Background.kt
@@ -23,26 +23,22 @@
 import androidx.glance.unit.ColorProvider
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class BackgroundModifier private constructor(
-    val colorProvider: ColorProvider?,
-    val imageProvider: ImageProvider?,
-    val contentScale: ContentScale = ContentScale.FillBounds,
-) : GlanceModifier.Element {
-    init {
-        require((colorProvider != null) xor (imageProvider != null)) {
-            "Exactly one of colorProvider and imageProvider must be non-null"
-        }
+sealed interface BackgroundModifier : GlanceModifier.Element {
+
+    class Color(val colorProvider: ColorProvider) : BackgroundModifier {
+        override fun toString() =
+            "BackgroundModifier(colorProvider=$colorProvider)"
     }
 
-    constructor(colorProvider: ColorProvider) :
-        this(colorProvider = colorProvider, imageProvider = null)
-
-    constructor(imageProvider: ImageProvider, contentScale: ContentScale) :
-        this(colorProvider = null, imageProvider = imageProvider, contentScale = contentScale)
-
-    override fun toString() =
-        "BackgroundModifier(colorProvider=$colorProvider, imageProvider=$imageProvider, " +
-            "contentScale=$contentScale)"
+    class Image(
+        val imageProvider: ImageProvider?,
+        val contentScale: ContentScale = ContentScale.FillBounds,
+        val colorFilter: ColorFilter? = null
+    ) : BackgroundModifier {
+        override fun toString() =
+            "BackgroundModifier(colorFilter=$colorFilter, imageProvider=$imageProvider, " +
+                "contentScale=$contentScale)"
+    }
 }
 
 /**
@@ -67,7 +63,7 @@
  * the element.
  */
 fun GlanceModifier.background(colorProvider: ColorProvider): GlanceModifier =
-    this.then(BackgroundModifier(colorProvider))
+    this.then(BackgroundModifier.Color(colorProvider))
 
 /**
  * Apply a background image to the element this modifier is attached to.
@@ -76,4 +72,20 @@
     imageProvider: ImageProvider,
     contentScale: ContentScale = ContentScale.FillBounds
 ): GlanceModifier =
-    this.then(BackgroundModifier(imageProvider, contentScale))
+    this.then(BackgroundModifier.Image(imageProvider, contentScale))
+
+/**
+ * Apply a background image to the element this modifier is attached to.
+ */
+fun GlanceModifier.background(
+    imageProvider: ImageProvider,
+    tint: ColorFilter?,
+    contentScale: ContentScale = ContentScale.FillBounds,
+    ): GlanceModifier =
+    this.then(
+        BackgroundModifier.Image(
+            imageProvider = imageProvider,
+            contentScale = contentScale,
+            colorFilter = tint
+        )
+    )
diff --git a/glance/glance/src/main/java/androidx/glance/Emittables.kt b/glance/glance/src/main/java/androidx/glance/Emittables.kt
index b9cf2f2..90a8168 100644
--- a/glance/glance/src/main/java/androidx/glance/Emittables.kt
+++ b/glance/glance/src/main/java/androidx/glance/Emittables.kt
@@ -38,6 +38,16 @@
 }
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun EmittableWithChildren.addChild(e: Emittable) {
+    this.children += e
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun EmittableWithChildren.addChildIfNotNull(e: Emittable?) {
+    if (e != null) this.children += e
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 abstract class EmittableLazyItemWithChildren : EmittableWithChildren() {
     var alignment: Alignment = Alignment.CenterStart
 }
diff --git a/glance/glance/src/main/java/androidx/glance/Image.kt b/glance/glance/src/main/java/androidx/glance/Image.kt
index ae5f43e..4011c8c 100644
--- a/glance/glance/src/main/java/androidx/glance/Image.kt
+++ b/glance/glance/src/main/java/androidx/glance/Image.kt
@@ -86,7 +86,9 @@
 /**
  * Effects used to modify the color of an image.
  */
-class ColorFilter internal constructor(internal val colorFilterParams: ColorFilterParams) {
+class ColorFilter internal constructor(
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val colorFilterParams: ColorFilterParams
+) {
     companion object {
         /**
          * Set a tinting option for the image using the platform-specific default blending mode.
diff --git a/glance/glance/src/main/java/androidx/glance/action/Action.kt b/glance/glance/src/main/java/androidx/glance/action/Action.kt
index d214afe..91028c5e 100644
--- a/glance/glance/src/main/java/androidx/glance/action/Action.kt
+++ b/glance/glance/src/main/java/androidx/glance/action/Action.kt
@@ -17,6 +17,7 @@
 package androidx.glance.action
 
 import android.app.Activity
+import androidx.annotation.DrawableRes
 import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
 import androidx.glance.ExperimentalGlanceApi
@@ -31,12 +32,16 @@
 
 /**
  * Apply an [Action], to be executed in response to a user click.
+ *
+ * @param onClick The action to run.
  */
 fun GlanceModifier.clickable(onClick: Action): GlanceModifier =
     this.then(ActionModifier(onClick))
 
 /**
  * Run [block] in response to a user click.
+ *
+ * @param block The action to run.
  */
 @Composable
 fun GlanceModifier.clickable(
@@ -47,7 +52,36 @@
 /**
  * Run [block] in response to a user click.
  *
+ * @param rippleOverride A drawable resource to use as the onClick ripple. Use [NoRippleOverride]
+ * if no custom behavior is needed.
+ * @param block The action to run.v
+ */
+@Composable
+fun GlanceModifier.clickable(
+    @DrawableRes rippleOverride: Int = NoRippleOverride,
+    block: () -> Unit
+): GlanceModifier =
+    this.then(ActionModifier(action = action(block = block), rippleOverride = rippleOverride))
+
+/**
+ * Apply an [Action], to be executed in response to a user click.
+ *
+ * @param rippleOverride A drawable resource to use as the onClick ripple. Use [NoRippleOverride]
+ * if no custom behavior is needed.
+ * @param onClick The action to run.
+ */
+fun GlanceModifier.clickable(
+    onClick: Action,
+    @DrawableRes rippleOverride: Int = NoRippleOverride
+): GlanceModifier =
+    this.then(ActionModifier(action = onClick, rippleOverride = rippleOverride))
+
+/**
+ * Run [block] in response to a user click.
+ *
  * @param block The action to run.
+ * @param rippleOverride A drawable resource to use as the onClick ripple. Use [NoRippleOverride]
+ * if no custom behavior is needed.
  * @param key A stable and unique key that identifies the action for this element. This ensures
  * that the correct action is triggered, especially in cases of items that change order. If not
  * provided we use the key that is automatically generated by the Compose runtime, which is unique
@@ -57,13 +91,24 @@
 @Composable
 fun GlanceModifier.clickable(
     key: String? = null,
+    @DrawableRes rippleOverride: Int = NoRippleOverride,
     block: () -> Unit
 ): GlanceModifier =
-    this.then(ActionModifier(action(key, block)))
+    this.then(ActionModifier(action = action(key, block), rippleOverride = rippleOverride))
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class ActionModifier(val action: Action) : GlanceModifier.Element {
+class ActionModifier(
+    val action: Action,
+    @DrawableRes val rippleOverride: Int = NoRippleOverride
+) : GlanceModifier.Element {
     override fun toString(): String {
-        return "ActionModifier(action=$action)"
+        return "ActionModifier(action=$action, rippleOverride=$rippleOverride)"
     }
 }
+
+/**
+ * Constant. Tells the system that there is no ripple override. When this is passed, the system
+ * will use default behavior for the ripple.
+ */
+@DrawableRes
+const val NoRippleOverride = 0
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ea083dd..bffdc0b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -57,7 +57,7 @@
 paparazzi = "1.0.0"
 paparazziNative = "2022.1.1-canary-f5f9f71"
 skiko = "0.7.7"
-spdxGradlePlugin = "0.1.0"
+spdxGradlePlugin = "0.2.0"
 sqldelight = "1.3.0"
 retrofit = "2.7.2"
 wire = "4.7.0"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 0b3b82b..a869424 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -27,7 +27,7 @@
       <trusted-keys>
          <trusted-key id="00089ee8c3afa95a854d0f1df800dd0933ecf7f7" group="com.google.guava"/>
          <trusted-key id="019082bc00e0324e2aef4cf00d3b328562a119a7" group="org.openjdk.jmh"/>
-         <trusted-key id="03c123038c20aae9e286c857479d601f3a7b5c1a" group="com.github.ajalt.clikt" name="clikt-jvm" version="3.5.3"/>
+         <trusted-key id="03c123038c20aae9e286c857479d601f3a7b5c1a" group="com.github.ajalt.clikt" name="clikt-jvm" />
          <trusted-key id="042b29e928995b9db963c636c7ca19b7b620d787" group="org.apache.maven"/>
          <trusted-key id="04543577d6a9cc626239c50c7ecbd740ff06aeb5">
             <trusting group="com.sun.activation"/>
@@ -221,7 +221,7 @@
             <trusting group="com.sun.activation"/>
             <trusting group="jakarta.activation"/>
          </trusted-key>
-         <trusted-key id="6ead752b3e2b38e8e2236d7ba9321edaa5cb3202" group="ch.randelshofer" name="fastdoubleparser" version="0.8.0"/>
+         <trusted-key id="6ead752b3e2b38e8e2236d7ba9321edaa5cb3202" group="ch.randelshofer" name="fastdoubleparser" />
          <trusted-key id="6f538074ccebf35f28af9b066a0975f8b1127b83">
             <trusting group="org.jetbrains.kotlin"/>
             <trusting group="org.jetbrains.kotlin.jvm"/>
@@ -245,9 +245,9 @@
             <trusting group="org.jvnet.staxex"/>
             <trusting group="^com[.]sun($|([.].*))" regex="true"/>
          </trusted-key>
-         <trusted-key id="713da88be50911535fe716f5208b0ab1d63011c7" group="org.apache.tomcat" name="annotations-api" version="6.0.53"/>
+         <trusted-key id="713da88be50911535fe716f5208b0ab1d63011c7" group="org.apache.tomcat" name="annotations-api" />
          <trusted-key id="720746177725a89207a7075bfd5dea07fcb690a8" group="org.codehaus.mojo"/>
-         <trusted-key id="73976c9c39c1479b84e2641a5a68a2249128e2c6" group="com.google.crypto.tink" name="tink-android" version="1.8.0"/>
+         <trusted-key id="73976c9c39c1479b84e2641a5a68a2249128e2c6" group="com.google.crypto.tink" name="tink-android" />
          <trusted-key id="748f15b2cf9ba8f024155e6ed7c92b70fa1c814d" group="org.apache.logging.log4j"/>
          <trusted-key id="7615ad56144df2376f49d98b1669c4bb543e0445" group="com.google.errorprone"/>
          <trusted-key id="7616eb882daf57a11477aaf559a252fb1199d873" group="com.google.code.findbugs"/>
@@ -261,11 +261,11 @@
          <trusted-key id="7e22d50a7ebd9d2cd269b2d4056aca74d46000bf" group="io.netty"/>
          <trusted-key id="7f36e793ae3252e5d9e9b98fee9e7dc9d92fc896" group="com.google.errorprone"/>
          <trusted-key id="7faa0f2206de228f0db01ad741321490758aad6f" group="org.codehaus.groovy"/>
-         <trusted-key id="7fe5e98df3a5c0dc34663ab7c1add37ca0069309" group="org.spdx" name="spdx-gradle-plugin" version="0.1.0"/>
+         <trusted-key id="7fe5e98df3a5c0dc34663ab7c1add37ca0069309" group="org.spdx" name="spdx-gradle-plugin"/>
          <trusted-key id="808d78b17a5a2d7c3668e31fbffc9b54721244ad" group="org.apache.commons"/>
          <trusted-key id="80f6d6b0d90c6747753344cab5a9e81b565e89e0" group="org.tomlj"/>
          <trusted-key id="8254180bfc943b816e0b5e2e5e2f2b3d474efe6b" group="it.unimi.dsi"/>
-         <trusted-key id="82c9ec0e52c47a936a849e0113d979595e6d01e1" group="org.apache.maven.shared" name="maven-shared-utils" version="3.3.4"/>
+         <trusted-key id="82c9ec0e52c47a936a849e0113d979595e6d01e1" group="org.apache.maven.shared" name="maven-shared-utils" />
          <trusted-key id="82f833963889d7ed06f1e4dc6525fd70cc303655" group="org.codehaus.mojo"/>
          <trusted-key id="835a685c8c6f49c54980e5caf406f31bc1468eba" group="org.jcodec"/>
          <trusted-key id="842afb86375d805422835bfd82b5574242c20d6f" group="org.antlr"/>
@@ -274,7 +274,7 @@
             <trusting group="org.apache.maven" name="maven-parent"/>
          </trusted-key>
          <trusted-key id="8569c95cadc508b09fe90f3002216ed811210daa" group="io.github.detekt.sarif4k"/>
-         <trusted-key id="86616cd3c4f0803e73374a434dbf5995d492505d" group="org.json" name="json" version="20230227"/>
+         <trusted-key id="86616cd3c4f0803e73374a434dbf5995d492505d" group="org.json" name="json" />
          <trusted-key id="8756c4f765c9ac3cb6b85d62379ce192d401ab61">
             <trusting group="com.github.ajalt"/>
             <trusting group="com.github.javaparser"/>
@@ -342,7 +342,7 @@
          <trusted-key id="ae9e53fc28ff2ab1012273d0bf1518e0160788a2" group="org.apache" name="apache"/>
          <trusted-key id="afa2b1823fc021bfd08c211fd5f4c07a434ab3da" group="com.squareup"/>
          <trusted-key id="afcc4c7594d09e2182c60e0f7a01b0f236e5430f" group="com.google.code.gson"/>
-         <trusted-key id="b02137d875d833d9b23392ecae5a7fb608a0221c" group="org.codehaus.plexus" name="plexus-classworlds" version="2.6.0"/>
+         <trusted-key id="b02137d875d833d9b23392ecae5a7fb608a0221c" group="org.codehaus.plexus" name="plexus-classworlds" />
          <trusted-key id="b02335aa54ccf21e52bbf9abd9c565aa72ba2fdd">
             <trusting group="com.google.protobuf"/>
             <trusting group="io.grpc"/>
@@ -448,7 +448,7 @@
             <trusting group="org.codehaus.plexus"/>
          </trusted-key>
          <trusted-key id="f3184bcd55f4d016e30d4c9bf42e87f9665015c9" group="org.jsoup"/>
-         <trusted-key id="f3d15b8ff9902805de4be6b18dc6f3d0abdbd017" group="org.codehaus.plexus" name="plexus-sec-dispatcher" version="2.0"/>
+         <trusted-key id="f3d15b8ff9902805de4be6b18dc6f3d0abdbd017" group="org.codehaus.plexus" name="plexus-sec-dispatcher" />
          <trusted-key id="f42b96b8648b5c4a1c43a62fbb2914c1fa0811c3" group="net.bytebuddy"/>
          <trusted-key id="fa1703b1d287caea3a60f931e0130a3ed5a2079e" group="org.webjars"/>
          <trusted-key id="fa77dcfef2ee6eb2debedd2c012579464d01c06a">
diff --git a/libraryversions.toml b/libraryversions.toml
index 55a1d9a..1f1cb09 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -21,7 +21,7 @@
 COLLECTION = "1.4.0-alpha01"
 COMPOSE = "1.6.0-alpha05"
 COMPOSE_COMPILER = "1.5.3"
-COMPOSE_MATERIAL3 = "1.2.0-alpha07"
+COMPOSE_MATERIAL3 = "1.2.0-alpha08"
 COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha01"
 COMPOSE_RUNTIME_TRACING = "1.0.0-alpha05"
 CONSTRAINTLAYOUT = "2.2.0-alpha13"
@@ -111,7 +111,7 @@
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
 REMOTECALLBACK = "1.0.0-alpha02"
 RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.6.0-rc01"
+ROOM = "2.7.0-alpha01"
 SAFEPARCEL = "1.0.0-alpha01"
 SAVEDSTATE = "1.3.0-alpha01"
 SECURITY = "1.1.0-alpha07"
@@ -125,7 +125,7 @@
 SLICE_BUILDERS_KTX = "1.0.0-alpha08"
 SLICE_REMOTECALLBACK = "1.0.0-alpha01"
 SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.4.0-rc01"
+SQLITE = "2.5.0-alpha01"
 SQLITE_INSPECTOR = "2.1.0-alpha01"
 STABLE_AIDL = "1.0.0-alpha01"
 STARTUP = "1.2.0-alpha03"
diff --git a/mediarouter/mediarouter/src/main/res/values-ne/strings.xml b/mediarouter/mediarouter/src/main/res/values-ne/strings.xml
index 490503ae..7cdafb4 100644
--- a/mediarouter/mediarouter/src/main/res/values-ne/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-ne/strings.xml
@@ -39,7 +39,7 @@
     <string name="mr_controller_no_info_available" msgid="855271725131981086">"कुनै पनि जानकारी उपलब्ध छैन"</string>
     <string name="mr_controller_casting_screen" msgid="9171231064758955152">"स्क्रिन Cast गरिँदै छ"</string>
     <string name="mr_dialog_default_group_name" msgid="4115858704575247342">"समूह"</string>
-    <string name="mr_dialog_groupable_header" msgid="4307018456678388936">"डिभाइस थप्नुहोस्"</string>
+    <string name="mr_dialog_groupable_header" msgid="4307018456678388936">"डिभाइस कनेक्ट गर्नुहोस्"</string>
     <string name="mr_dialog_transferable_header" msgid="6068257520605505468">"कुनै समूहमा प्ले गर्नुहोस्"</string>
     <string name="mr_cast_dialog_title_view_placeholder" msgid="2175930138959078155">"कुनै पनि जानकारी उपलब्ध छैन"</string>
     <string name="mr_chooser_zero_routes_found_title" msgid="5213435473397442608">"कुनै पनि डिभाइस उपलब्ध छैन"</string>
diff --git a/metrics/integration-tests/janktest/build.gradle b/metrics/integration-tests/janktest/build.gradle
index 476a3ae..3aa5881 100644
--- a/metrics/integration-tests/janktest/build.gradle
+++ b/metrics/integration-tests/janktest/build.gradle
@@ -25,6 +25,9 @@
         viewBinding true
     }
     namespace "androidx.metrics.performance.janktest"
+    defaultConfig {
+        multiDexEnabled true
+    }
 }
 
 dependencies {
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
index dc60ee0..5ae8346 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorTest.kt
@@ -171,6 +171,7 @@
         assertWithMessage("Replacement Fragment should be the primary navigation Fragment")
             .that(fragmentManager.primaryNavigationFragment)
             .isSameInstanceAs(replacementFragment)
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
     }
 
     @UiThreadTest
@@ -359,6 +360,7 @@
         assertWithMessage("Fragment should be the primary navigation Fragment after pop")
             .that(fragmentManager.primaryNavigationFragment)
             .isSameInstanceAs(fragment)
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
     }
 
     @UiThreadTest
@@ -528,6 +530,26 @@
         assertWithMessage("PrimaryFragment should be the correct type")
             .that(fragmentManager.primaryNavigationFragment)
             .isNotInstanceOf(EmptyFragment::class.java)
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+    }
+
+    @UiThreadTest
+    @Test
+    fun testMultipleNavigateFragmentTransactionsThenPopMultiple() {
+        val entry = createBackStackEntry()
+        val secondEntry = createBackStackEntry(SECOND_FRAGMENT, clazz = Fragment::class)
+        val thirdEntry = createBackStackEntry(THIRD_FRAGMENT)
+
+        // Push 3 fragments
+        fragmentNavigator.navigate(listOf(entry, secondEntry, thirdEntry), null, null)
+        fragmentManager.executePendingTransactions()
+
+        // Now pop multiple fragments with savedState so that the secondEntry does not get
+        // marked complete by clear viewModel
+        fragmentNavigator.popBackStack(secondEntry, true)
+        fragmentManager.executePendingTransactions()
+        assertThat(navigatorState.backStack.value).containsExactly(entry)
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
     }
 
     @UiThreadTest
diff --git a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
index 52c924c..b7e0009 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
@@ -77,6 +77,9 @@
         assertWithMessage("New Entry should be RESUMED")
             .that(navController.currentBackStackEntry!!.lifecycle.currentState)
             .isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry!!
+        )
     }
 
     @Test
@@ -217,6 +220,7 @@
         // ensure original Fragment is dismissed and backStacks are in sync
         assertThat(navigator.backStack.value.size).isEqualTo(1)
         assertThat(fm.fragments.size).isEqualTo(2) // start + dialog fragment
+        assertThat(navController.visibleEntries.value.size).isEqualTo(2)
     }
 
     @Test
@@ -310,6 +314,9 @@
         fm?.executePendingTransactions()
 
         assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("first")
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @LargeTest
@@ -335,6 +342,9 @@
         onBackPressedDispatcher.onBackPressed()
 
         assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("third")
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry!!
+        )
     }
 
     @LargeTest
@@ -364,6 +374,9 @@
         onBackPressedDispatcher.onBackPressed()
 
         assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo("fourth")
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry!!
+        )
     }
 
     private fun withNavigationActivity(
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
index 3fb3248..0c8b113 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.kt
@@ -85,16 +85,14 @@
                 entry.id == fragment.tag
             }
             if (entry != null) {
-                if (!state.backStack.value.contains(entry)) {
-                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                        Log.v(
-                            TAG,
-                            "Marking transition complete for entry $entry " +
-                                "due to fragment $source lifecycle reaching DESTROYED"
-                        )
-                    }
-                    state.markTransitionComplete(entry)
+                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(
+                        TAG,
+                        "Marking transition complete for entry $entry " +
+                            "due to fragment $source lifecycle reaching DESTROYED"
+                    )
                 }
+                state.markTransitionComplete(entry)
             }
         }
     }
@@ -113,19 +111,16 @@
                 }
                 state.markTransitionComplete(entry)
             }
-            // Once the lifecycle reaches DESTROYED, if the entry is not in the back stack, we can
-            // mark the transition complete
+            // Once the lifecycle reaches DESTROYED, we can mark the transition complete
             if (event == Lifecycle.Event.ON_DESTROY) {
-                if (!state.backStack.value.contains(entry)) {
-                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                        Log.v(
-                            TAG,
-                            "Marking transition complete for entry $entry due " +
-                                "to fragment $owner view lifecycle reaching DESTROYED"
-                        )
-                    }
-                    state.markTransitionComplete(entry)
+                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(
+                        TAG,
+                        "Marking transition complete for entry $entry due " +
+                            "to fragment $owner view lifecycle reaching DESTROYED"
+                    )
                 }
+                state.markTransitionComplete(entry)
             }
         }
     }
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index 3b662ee..2ddc413 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -186,6 +186,9 @@
         assertThat(navigator.backStack.size)
             .isEqualTo(1)
         assertThat(originalViewModel.isCleared).isTrue()
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -215,6 +218,7 @@
         val newViewModel = ViewModelProvider(newBackStackEntry).get<TestAndroidViewModel>()
         assertThat(newBackStackEntry.id).isSameInstanceAs(originalBackStackEntry.id)
         assertThat(newViewModel).isSameInstanceAs(originalViewModel)
+        assertThat(navController.visibleEntries.value).containsExactly(newBackStackEntry)
     }
 
     @UiThreadTest
@@ -606,6 +610,9 @@
         navController.navigate(R.id.second_test)
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
         assertThat(navigator.backStack.size).isEqualTo(2)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2028,6 +2035,7 @@
             .isFalse()
         assertThat(navController.currentDestination).isNull()
         assertThat(navigator.backStack.size).isEqualTo(0)
+        assertThat(navController.visibleEntries.value).isEmpty()
     }
 
     @UiThreadTest
@@ -2074,6 +2082,9 @@
             .isTrue()
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
         assertThat(navigator.backStack.size).isEqualTo(1)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2092,6 +2103,9 @@
         navigator.popCurrent()
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
         assertThat(navigator.backStack.size).isEqualTo(1)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2132,6 +2146,9 @@
         )
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
         assertThat(navigator.backStack.size).isEqualTo(1)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2172,6 +2189,9 @@
             .isTrue()
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.start_test)
         assertThat(navigator.backStack.size).isEqualTo(1)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
@@ -2229,6 +2249,9 @@
         navController.navigate(R.id.self)
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
         assertThat(navigator.backStack.size).isEqualTo(2)
+        assertThat(navController.visibleEntries.value).containsExactly(
+            navController.currentBackStackEntry
+        )
     }
 
     @UiThreadTest
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 51d0426..87d7074 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -126,8 +126,9 @@
      * whenever they change. If there is no visible [NavBackStackEntry], this will be set to an
      * empty list.
      *
-     * - `CREATED` entries are listed first and include all entries that have been popped from
-     * the back stack and are in the process of completing their exit transition
+     * - `CREATED` entries are listed first and include all entries that are in the process of
+     * completing their exit transition. Note that this can include entries that have been
+     * popped off the Navigation back stack.
      * - `STARTED` entries on the back stack are next and include all entries that are running
      * their enter transition and entries whose destination is partially covered by a
      * `FloatingWindow` destination
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
index 5de128a..e8985b4 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
+++ b/privacysandbox/sdkruntime/sdkruntime-client/build.gradle
@@ -92,7 +92,6 @@
 dependencies {
     api(libs.kotlinStdlib)
     api(libs.kotlinCoroutinesCore)
-    implementation(libs.multidex)
     implementation("androidx.core:core-ktx:1.12.0-alpha05")
 
     api project(path: ':privacysandbox:sdkruntime:sdkruntime-core')
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
index de5b048..70ceae1 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -133,6 +133,7 @@
 
             override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
                 surfaceView.setZOrderOnTop(isZOrderOnTop)
+                remoteSessionController.notifyZOrderChanged(isZOrderOnTop)
             }
 
             override fun close() {
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
index 1e2ff92..c856d33 100644
--- a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
@@ -21,4 +21,5 @@
     void close();
     void notifyConfigurationChanged(in Configuration configuration);
     void notifyResized(int width, int height);
+    void notifyZOrderChanged(boolean isZOrderOnTop);
 }
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
index 6639148..66e1da8 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
@@ -185,6 +185,10 @@
                 }
             }
 
+            override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
+                session.notifyZOrderChanged(isZOrderOnTop)
+            }
+
             override fun close() {
                 val mHandler = Handler(Looper.getMainLooper())
                 mHandler.post {
diff --git a/privacysandbox/ui/ui-tests/build.gradle b/privacysandbox/ui/ui-tests/build.gradle
index cf66642..77e6e30 100644
--- a/privacysandbox/ui/ui-tests/build.gradle
+++ b/privacysandbox/ui/ui-tests/build.gradle
@@ -44,6 +44,9 @@
 
 android {
     namespace "androidx.privacysandbox.ui.tests"
+    defaultConfig {
+        multiDexEnabled true
+    }
 }
 
 androidx {
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
index e1ba4e8..3333303 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
@@ -203,6 +203,44 @@
         assertTrue(configChangedLatch.count == 0.toLong())
     }
 
+    /**
+     * Tests that the provider receives Z-order change updates.
+     */
+    @Test
+    fun testZOrderChanged() {
+        val openSessionLatch = CountDownLatch(1)
+        val adapter = TestSandboxedUiAdapter(
+            openSessionLatch,
+            null,
+            /* hasFailingTestSession=*/false
+        )
+        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val adapterFromCoreLibInfo = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
+        view.setAdapter(adapterFromCoreLibInfo)
+        assertThat(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        view.setZOrderOnTopAndEnableUserInteraction(!adapter.initialZOrderOnTop)
+        assertThat(adapter.zOrderLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    /**
+     * Tests that the provider does not receive Z-order updates if the Z-order is unchanged.
+     */
+    @Test
+    fun testZOrderUnchanged() {
+        val openSessionLatch = CountDownLatch(1)
+        val adapter = TestSandboxedUiAdapter(
+            openSessionLatch,
+            null,
+            /* hasFailingTestSession=*/false
+        )
+        val coreLibInfo = adapter.toCoreLibInfo(context)
+        val adapterFromCoreLibInfo = SandboxedUiAdapterFactory.createFromCoreLibInfo(coreLibInfo)
+        view.setAdapter(adapterFromCoreLibInfo)
+        assertThat(openSessionLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        view.setZOrderOnTopAndEnableUserInteraction(adapter.initialZOrderOnTop)
+        assertThat(adapter.zOrderLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+    }
+
     @Test
     fun testSessionError() {
         val adapter = TestSandboxedUiAdapter(
@@ -240,6 +278,8 @@
     ) : SandboxedUiAdapter {
 
         var isOpenSessionCalled = false
+        var initialZOrderOnTop = false
+        var zOrderLatch = CountDownLatch(1)
         lateinit var session: SandboxedUiAdapter.Session
         lateinit var internalClient: SandboxedUiAdapter.SessionClient
 
@@ -254,6 +294,7 @@
         ) {
             internalClient = client
             isOpenSessionCalled = true
+            initialZOrderOnTop = isZOrderOnTop
             session = if (hasFailingTestSession) {
                 FailingTestSession(context)
             } else {
@@ -301,6 +342,7 @@
             }
 
             override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
+                zOrderLatch.countDown()
             }
 
             override fun notifyConfigurationChanged(configuration: Configuration) {
diff --git a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt
index 6f0f335..f212956 100644
--- a/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt
+++ b/room/room-compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/compiler/SourceSet.kt
@@ -18,9 +18,11 @@
 
 import androidx.room.compiler.processing.util.Source
 import java.io.File
+import java.util.regex.Pattern
 
 private val BY_ROUNDS_PATH_PATTERN =
-    "(byRounds${File.separator}[0-9]+${File.separator})?(.*)".toPattern()
+    ("(byRounds${Pattern.quote(File.separator)}[0-9]+" +
+        "${Pattern.quote(File.separator)})?(.*)").toPattern()
 
 /**
  * Represents sources that are positioned in the [root] folder.
diff --git a/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics.java b/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics.java
index 7158a10..cc3df0a 100644
--- a/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics.java
+++ b/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceSerializeMetrics.java
@@ -59,6 +59,7 @@
 @RunWith(AndroidJUnit4.class)
 @LargeTest
 @SdkSuppress(minSdkVersion = 19)
+@SuppressWarnings("deprecation")
 public class SliceSerializeMetrics {
 
     private static final boolean WRITE_SAMPLE_FILE = false;
diff --git a/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics.java b/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics.java
index 05482e8..b892033 100644
--- a/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics.java
+++ b/slice/slice-benchmark/src/androidTest/java/androidx/slice/SliceViewMetrics.java
@@ -39,6 +39,7 @@
 @RunWith(Parameterized.class)
 @SmallTest
 @SdkSuppress(minSdkVersion = 19)
+@SuppressWarnings("deprecation")
 public class SliceViewMetrics {
 
     private final int mMode;
diff --git a/slice/slice-builders-ktx/api/current.txt b/slice/slice-builders-ktx/api/current.txt
index 05623ec..6d9933f 100644
--- a/slice/slice-builders-ktx/api/current.txt
+++ b/slice/slice-builders-ktx/api/current.txt
@@ -1,49 +1,49 @@
 // Signature format: 4.0
 package androidx.slice.builders {
 
-  public final class CellBuilderDsl extends androidx.slice.builders.GridRowBuilder.CellBuilder {
-    ctor public CellBuilderDsl();
+  @Deprecated public final class CellBuilderDsl extends androidx.slice.builders.GridRowBuilder.CellBuilder {
+    ctor @Deprecated public CellBuilderDsl();
   }
 
-  public final class GridRowBuilderDsl extends androidx.slice.builders.GridRowBuilder {
-    ctor public GridRowBuilderDsl();
+  @Deprecated public final class GridRowBuilderDsl extends androidx.slice.builders.GridRowBuilder {
+    ctor @Deprecated public GridRowBuilderDsl();
   }
 
   public final class GridRowBuilderKt {
-    method public static inline androidx.slice.builders.GridRowBuilder cell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
-    method public static inline androidx.slice.builders.GridRowBuilder seeMoreCell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
+    method @Deprecated public static inline androidx.slice.builders.GridRowBuilder cell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
+    method @Deprecated public static inline androidx.slice.builders.GridRowBuilder seeMoreCell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
   }
 
-  public final class HeaderBuilderDsl extends androidx.slice.builders.ListBuilder.HeaderBuilder {
-    ctor public HeaderBuilderDsl();
+  @Deprecated public final class HeaderBuilderDsl extends androidx.slice.builders.ListBuilder.HeaderBuilder {
+    ctor @Deprecated public HeaderBuilderDsl();
   }
 
-  public final class InputRangeBuilderDsl extends androidx.slice.builders.ListBuilder.InputRangeBuilder {
-    ctor public InputRangeBuilderDsl();
+  @Deprecated public final class InputRangeBuilderDsl extends androidx.slice.builders.ListBuilder.InputRangeBuilder {
+    ctor @Deprecated public InputRangeBuilderDsl();
   }
 
-  public final class ListBuilderDsl extends androidx.slice.builders.ListBuilder {
-    ctor public ListBuilderDsl(android.content.Context context, android.net.Uri uri, long ttl);
+  @Deprecated public final class ListBuilderDsl extends androidx.slice.builders.ListBuilder {
+    ctor @Deprecated public ListBuilderDsl(android.content.Context context, android.net.Uri uri, long ttl);
   }
 
   public final class ListBuilderKt {
-    method public static inline androidx.slice.builders.ListBuilder gridRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.GridRowBuilderDsl,kotlin.Unit> buildGrid);
-    method public static inline androidx.slice.builders.ListBuilder header(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.HeaderBuilderDsl,kotlin.Unit> buildHeader);
-    method public static inline androidx.slice.builders.ListBuilder inputRange(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.InputRangeBuilderDsl,kotlin.Unit> buildInputRange);
-    method public static inline androidx.slice.Slice list(android.content.Context context, android.net.Uri uri, long ttl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.ListBuilderDsl,kotlin.Unit> addRows);
-    method public static inline androidx.slice.builders.ListBuilder range(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RangeBuilderDsl,kotlin.Unit> buildRange);
-    method public static inline androidx.slice.builders.ListBuilder row(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
-    method public static inline androidx.slice.builders.ListBuilder seeMoreRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
-    method public static androidx.slice.builders.SliceAction tapSliceAction(android.app.PendingIntent pendingIntent, androidx.core.graphics.drawable.IconCompat icon, optional int imageMode, CharSequence title);
-    method public static androidx.slice.builders.SliceAction toggleSliceAction(android.app.PendingIntent pendingIntent, optional androidx.core.graphics.drawable.IconCompat? icon, CharSequence title, boolean isChecked);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder gridRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.GridRowBuilderDsl,kotlin.Unit> buildGrid);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder header(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.HeaderBuilderDsl,kotlin.Unit> buildHeader);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder inputRange(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.InputRangeBuilderDsl,kotlin.Unit> buildInputRange);
+    method @Deprecated public static inline androidx.slice.Slice list(android.content.Context context, android.net.Uri uri, long ttl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.ListBuilderDsl,kotlin.Unit> addRows);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder range(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RangeBuilderDsl,kotlin.Unit> buildRange);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder row(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder seeMoreRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
+    method @Deprecated public static androidx.slice.builders.SliceAction tapSliceAction(android.app.PendingIntent pendingIntent, androidx.core.graphics.drawable.IconCompat icon, optional int imageMode, CharSequence title);
+    method @Deprecated public static androidx.slice.builders.SliceAction toggleSliceAction(android.app.PendingIntent pendingIntent, optional androidx.core.graphics.drawable.IconCompat? icon, CharSequence title, boolean isChecked);
   }
 
-  public final class RangeBuilderDsl extends androidx.slice.builders.ListBuilder.RangeBuilder {
-    ctor public RangeBuilderDsl();
+  @Deprecated public final class RangeBuilderDsl extends androidx.slice.builders.ListBuilder.RangeBuilder {
+    ctor @Deprecated public RangeBuilderDsl();
   }
 
-  public final class RowBuilderDsl extends androidx.slice.builders.ListBuilder.RowBuilder {
-    ctor public RowBuilderDsl();
+  @Deprecated public final class RowBuilderDsl extends androidx.slice.builders.ListBuilder.RowBuilder {
+    ctor @Deprecated public RowBuilderDsl();
   }
 
 }
diff --git a/slice/slice-builders-ktx/api/restricted_current.txt b/slice/slice-builders-ktx/api/restricted_current.txt
index 05623ec..6d9933f 100644
--- a/slice/slice-builders-ktx/api/restricted_current.txt
+++ b/slice/slice-builders-ktx/api/restricted_current.txt
@@ -1,49 +1,49 @@
 // Signature format: 4.0
 package androidx.slice.builders {
 
-  public final class CellBuilderDsl extends androidx.slice.builders.GridRowBuilder.CellBuilder {
-    ctor public CellBuilderDsl();
+  @Deprecated public final class CellBuilderDsl extends androidx.slice.builders.GridRowBuilder.CellBuilder {
+    ctor @Deprecated public CellBuilderDsl();
   }
 
-  public final class GridRowBuilderDsl extends androidx.slice.builders.GridRowBuilder {
-    ctor public GridRowBuilderDsl();
+  @Deprecated public final class GridRowBuilderDsl extends androidx.slice.builders.GridRowBuilder {
+    ctor @Deprecated public GridRowBuilderDsl();
   }
 
   public final class GridRowBuilderKt {
-    method public static inline androidx.slice.builders.GridRowBuilder cell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
-    method public static inline androidx.slice.builders.GridRowBuilder seeMoreCell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
+    method @Deprecated public static inline androidx.slice.builders.GridRowBuilder cell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
+    method @Deprecated public static inline androidx.slice.builders.GridRowBuilder seeMoreCell(androidx.slice.builders.GridRowBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.CellBuilderDsl,kotlin.Unit> buildCell);
   }
 
-  public final class HeaderBuilderDsl extends androidx.slice.builders.ListBuilder.HeaderBuilder {
-    ctor public HeaderBuilderDsl();
+  @Deprecated public final class HeaderBuilderDsl extends androidx.slice.builders.ListBuilder.HeaderBuilder {
+    ctor @Deprecated public HeaderBuilderDsl();
   }
 
-  public final class InputRangeBuilderDsl extends androidx.slice.builders.ListBuilder.InputRangeBuilder {
-    ctor public InputRangeBuilderDsl();
+  @Deprecated public final class InputRangeBuilderDsl extends androidx.slice.builders.ListBuilder.InputRangeBuilder {
+    ctor @Deprecated public InputRangeBuilderDsl();
   }
 
-  public final class ListBuilderDsl extends androidx.slice.builders.ListBuilder {
-    ctor public ListBuilderDsl(android.content.Context context, android.net.Uri uri, long ttl);
+  @Deprecated public final class ListBuilderDsl extends androidx.slice.builders.ListBuilder {
+    ctor @Deprecated public ListBuilderDsl(android.content.Context context, android.net.Uri uri, long ttl);
   }
 
   public final class ListBuilderKt {
-    method public static inline androidx.slice.builders.ListBuilder gridRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.GridRowBuilderDsl,kotlin.Unit> buildGrid);
-    method public static inline androidx.slice.builders.ListBuilder header(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.HeaderBuilderDsl,kotlin.Unit> buildHeader);
-    method public static inline androidx.slice.builders.ListBuilder inputRange(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.InputRangeBuilderDsl,kotlin.Unit> buildInputRange);
-    method public static inline androidx.slice.Slice list(android.content.Context context, android.net.Uri uri, long ttl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.ListBuilderDsl,kotlin.Unit> addRows);
-    method public static inline androidx.slice.builders.ListBuilder range(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RangeBuilderDsl,kotlin.Unit> buildRange);
-    method public static inline androidx.slice.builders.ListBuilder row(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
-    method public static inline androidx.slice.builders.ListBuilder seeMoreRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
-    method public static androidx.slice.builders.SliceAction tapSliceAction(android.app.PendingIntent pendingIntent, androidx.core.graphics.drawable.IconCompat icon, optional int imageMode, CharSequence title);
-    method public static androidx.slice.builders.SliceAction toggleSliceAction(android.app.PendingIntent pendingIntent, optional androidx.core.graphics.drawable.IconCompat? icon, CharSequence title, boolean isChecked);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder gridRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.GridRowBuilderDsl,kotlin.Unit> buildGrid);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder header(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.HeaderBuilderDsl,kotlin.Unit> buildHeader);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder inputRange(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.InputRangeBuilderDsl,kotlin.Unit> buildInputRange);
+    method @Deprecated public static inline androidx.slice.Slice list(android.content.Context context, android.net.Uri uri, long ttl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.ListBuilderDsl,kotlin.Unit> addRows);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder range(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RangeBuilderDsl,kotlin.Unit> buildRange);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder row(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
+    method @Deprecated public static inline androidx.slice.builders.ListBuilder seeMoreRow(androidx.slice.builders.ListBuilderDsl, kotlin.jvm.functions.Function1<? super androidx.slice.builders.RowBuilderDsl,kotlin.Unit> buildRow);
+    method @Deprecated public static androidx.slice.builders.SliceAction tapSliceAction(android.app.PendingIntent pendingIntent, androidx.core.graphics.drawable.IconCompat icon, optional int imageMode, CharSequence title);
+    method @Deprecated public static androidx.slice.builders.SliceAction toggleSliceAction(android.app.PendingIntent pendingIntent, optional androidx.core.graphics.drawable.IconCompat? icon, CharSequence title, boolean isChecked);
   }
 
-  public final class RangeBuilderDsl extends androidx.slice.builders.ListBuilder.RangeBuilder {
-    ctor public RangeBuilderDsl();
+  @Deprecated public final class RangeBuilderDsl extends androidx.slice.builders.ListBuilder.RangeBuilder {
+    ctor @Deprecated public RangeBuilderDsl();
   }
 
-  public final class RowBuilderDsl extends androidx.slice.builders.ListBuilder.RowBuilder {
-    ctor public RowBuilderDsl();
+  @Deprecated public final class RowBuilderDsl extends androidx.slice.builders.ListBuilder.RowBuilder {
+    ctor @Deprecated public RowBuilderDsl();
   }
 
 }
diff --git a/slice/slice-builders-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest.kt b/slice/slice-builders-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest.kt
index a05802a..6faebd4 100644
--- a/slice/slice-builders-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest.kt
+++ b/slice/slice-builders-ktx/src/androidTest/java/androidx/slice/builders/SliceBuildersKtxTest.kt
@@ -33,6 +33,7 @@
 
 @SdkSuppress(minSdkVersion = 19)
 @MediumTest
+@Suppress("DEPRECATION")
 class SliceBuildersKtxTest {
     private val testUri = Uri.parse("content://com.example.android.sliceuri")
     private val context = ApplicationProvider.getApplicationContext() as android.content.Context
diff --git a/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/GridRowBuilder.kt b/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/GridRowBuilder.kt
index 504b13b..9188617 100644
--- a/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/GridRowBuilder.kt
+++ b/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/GridRowBuilder.kt
@@ -20,6 +20,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class GridRowBuilderDsl : GridRowBuilder()
 
@@ -28,17 +36,41 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class CellBuilderDsl : GridRowBuilder.CellBuilder()
 
 /**
  * @see GridRowBuilder.addCell
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun GridRowBuilderDsl.cell(buildCell: CellBuilderDsl.() -> Unit) =
     addCell(CellBuilderDsl().apply { buildCell() })
 
 /**
  * @see GridRowBuilder.setSeeMoreCell
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun GridRowBuilderDsl.seeMoreCell(buildCell: CellBuilderDsl.() -> Unit) =
     setSeeMoreCell(CellBuilderDsl().apply { buildCell() })
diff --git a/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/ListBuilder.kt b/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/ListBuilder.kt
index 101ed21..a782bc4 100644
--- a/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/ListBuilder.kt
+++ b/slice/slice-builders-ktx/src/main/java/androidx/slice/builders/ListBuilder.kt
@@ -32,6 +32,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class ListBuilderDsl(context: Context, uri: Uri, ttl: Long) : ListBuilder(context, uri, ttl)
 
@@ -40,6 +48,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class RowBuilderDsl : ListBuilder.RowBuilder()
 
@@ -48,6 +64,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class InputRangeBuilderDsl : ListBuilder.InputRangeBuilder()
 
@@ -56,6 +80,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class RangeBuilderDsl : ListBuilder.RangeBuilder()
 
@@ -64,6 +96,14 @@
  * Two implicit receivers that are annotated with @SliceMarker are not accessible in the same scope,
  * ensuring a type-safe DSL.
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 @SliceMarker
 class HeaderBuilderDsl : ListBuilder.HeaderBuilder()
 
@@ -94,6 +134,14 @@
  * </pre>
  * @see ListBuilder.build
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun list(
     context: Context,
     uri: Uri,
@@ -104,42 +152,98 @@
 /**
  * @see ListBuilder.setHeader
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.header(buildHeader: HeaderBuilderDsl.() -> Unit) =
     setHeader(HeaderBuilderDsl().apply { buildHeader() })
 
 /**
  * @see ListBuilder.addGridRow
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.gridRow(buildGrid: GridRowBuilderDsl.() -> Unit) =
     addGridRow(GridRowBuilderDsl().apply { buildGrid() })
 
 /**
  * @see ListBuilder.addRow
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.row(buildRow: RowBuilderDsl.() -> Unit) =
     addRow(RowBuilderDsl().apply { buildRow() })
 
 /**
  * @see ListBuilder.setSeeMoreRow
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.seeMoreRow(buildRow: RowBuilderDsl.() -> Unit) =
     setSeeMoreRow(RowBuilderDsl().apply { buildRow() })
 
 /**
  * @see ListBuilder.addInputRange
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.inputRange(buildInputRange: InputRangeBuilderDsl.() -> Unit) =
     addInputRange(InputRangeBuilderDsl().apply { buildInputRange() })
 
 /**
  * @see ListBuilder.addRange
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 inline fun ListBuilderDsl.range(buildRange: RangeBuilderDsl.() -> Unit) =
     addRange(RangeBuilderDsl().apply { buildRange() })
 
 /**
  * Factory method to build a tappable [SliceAction].
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 fun tapSliceAction(
     pendingIntent: PendingIntent,
     icon: IconCompat,
@@ -150,6 +254,14 @@
 /**
  * Factory method to build a toggleable [SliceAction].
  */
+@Deprecated(
+    """
+        Slice framework has been deprecated, it will not receive any updates moving forward.
+        If you are looking for a framework that handles communication across apps, 
+        consider using AppSearchManager.
+    """,
+    ReplaceWith("AppSearchManager", "android.app.appsearch"))
+@Suppress("DEPRECATION")
 fun toggleSliceAction(
     pendingIntent: PendingIntent,
     icon: IconCompat? = null,
diff --git a/slice/slice-builders/api/current.txt b/slice/slice-builders/api/current.txt
index 1351608..6801791 100644
--- a/slice/slice-builders/api/current.txt
+++ b/slice/slice-builders/api/current.txt
@@ -1,190 +1,190 @@
 // Signature format: 4.0
 package androidx.slice.builders {
 
-  @RequiresApi(19) public class GridRowBuilder {
-    ctor public GridRowBuilder();
-    method public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
-    method public androidx.slice.builders.GridRowBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreAction(android.app.PendingIntent);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+  @Deprecated @RequiresApi(19) public class GridRowBuilder {
+    ctor @Deprecated public GridRowBuilder();
+    method @Deprecated public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
   }
 
-  public static class GridRowBuilder.CellBuilder {
-    ctor public GridRowBuilder.CellBuilder();
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setSliceAction(androidx.slice.builders.SliceAction);
+  @Deprecated public static class GridRowBuilder.CellBuilder {
+    ctor @Deprecated public GridRowBuilder.CellBuilder();
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setSliceAction(androidx.slice.builders.SliceAction);
   }
 
-  @RequiresApi(19) public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
-    ctor @RequiresApi(26) public ListBuilder(android.content.Context, android.net.Uri, java.time.Duration?);
-    ctor public ListBuilder(android.content.Context, android.net.Uri, long);
-    method public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
-    method public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
-    method public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
-    method public androidx.slice.builders.ListBuilder addRating(androidx.slice.builders.ListBuilder.RatingBuilder);
-    method public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
-    method public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
-    method public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
-    method public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
-    method @RequiresApi(21) public androidx.slice.builders.ListBuilder setHostExtras(android.os.PersistableBundle);
-    method public androidx.slice.builders.ListBuilder setIsError(boolean);
-    method public androidx.slice.builders.ListBuilder setKeywords(java.util.Set<java.lang.String!>);
-    method public androidx.slice.builders.ListBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder setSeeMoreAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder setSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
-    field public static final int ACTION_WITH_LABEL = 6; // 0x6
-    field public static final int ICON_IMAGE = 0; // 0x0
-    field public static final long INFINITY = -1L; // 0xffffffffffffffffL
-    field public static final int LARGE_IMAGE = 2; // 0x2
-    field public static final int RANGE_MODE_DETERMINATE = 0; // 0x0
-    field public static final int RANGE_MODE_INDETERMINATE = 1; // 0x1
-    field public static final int RANGE_MODE_STAR_RATING = 2; // 0x2
-    field public static final int RAW_IMAGE_LARGE = 4; // 0x4
-    field public static final int RAW_IMAGE_SMALL = 3; // 0x3
-    field public static final int SMALL_IMAGE = 1; // 0x1
-    field public static final int UNKNOWN_IMAGE = 5; // 0x5
+  @Deprecated @RequiresApi(19) public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+    ctor @Deprecated @RequiresApi(26) public ListBuilder(android.content.Context, android.net.Uri, java.time.Duration?);
+    ctor @Deprecated public ListBuilder(android.content.Context, android.net.Uri, long);
+    method @Deprecated public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRating(androidx.slice.builders.ListBuilder.RatingBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
+    method @Deprecated public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
+    method @Deprecated @RequiresApi(21) public androidx.slice.builders.ListBuilder setHostExtras(android.os.PersistableBundle);
+    method @Deprecated public androidx.slice.builders.ListBuilder setIsError(boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder setKeywords(java.util.Set<java.lang.String!>);
+    method @Deprecated public androidx.slice.builders.ListBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    field @Deprecated public static final int ACTION_WITH_LABEL = 6; // 0x6
+    field @Deprecated public static final int ICON_IMAGE = 0; // 0x0
+    field @Deprecated public static final long INFINITY = -1L; // 0xffffffffffffffffL
+    field @Deprecated public static final int LARGE_IMAGE = 2; // 0x2
+    field @Deprecated public static final int RANGE_MODE_DETERMINATE = 0; // 0x0
+    field @Deprecated public static final int RANGE_MODE_INDETERMINATE = 1; // 0x1
+    field @Deprecated public static final int RANGE_MODE_STAR_RATING = 2; // 0x2
+    field @Deprecated public static final int RAW_IMAGE_LARGE = 4; // 0x4
+    field @Deprecated public static final int RAW_IMAGE_SMALL = 3; // 0x3
+    field @Deprecated public static final int SMALL_IMAGE = 1; // 0x1
+    field @Deprecated public static final int UNKNOWN_IMAGE = 5; // 0x5
   }
 
-  public static class ListBuilder.HeaderBuilder {
-    ctor public ListBuilder.HeaderBuilder();
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence, boolean);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence, boolean);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence, boolean);
+  @Deprecated public static class ListBuilder.HeaderBuilder {
+    ctor @Deprecated public ListBuilder.HeaderBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence, boolean);
   }
 
-  public static class ListBuilder.InputRangeBuilder {
-    ctor public ListBuilder.InputRangeBuilder();
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setMin(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setThumb(androidx.core.graphics.drawable.IconCompat);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setValue(int);
+  @Deprecated public static class ListBuilder.InputRangeBuilder {
+    ctor @Deprecated public ListBuilder.InputRangeBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setMin(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setThumb(androidx.core.graphics.drawable.IconCompat);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setValue(int);
   }
 
-  public static class ListBuilder.RangeBuilder {
-    ctor public ListBuilder.RangeBuilder();
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setMode(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setValue(int);
+  @Deprecated public static class ListBuilder.RangeBuilder {
+    ctor @Deprecated public ListBuilder.RangeBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setMode(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setValue(int);
   }
 
-  public static final class ListBuilder.RatingBuilder {
-    ctor public ListBuilder.RatingBuilder();
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setMin(int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setValue(float);
+  @Deprecated public static final class ListBuilder.RatingBuilder {
+    ctor @Deprecated public ListBuilder.RatingBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setMin(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setValue(float);
   }
 
-  public static class ListBuilder.RowBuilder {
-    ctor public ListBuilder.RowBuilder();
-    ctor public ListBuilder.RowBuilder(android.net.Uri);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(long);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setEndOfSection(boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence?, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence?, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(long);
+  @Deprecated public static class ListBuilder.RowBuilder {
+    ctor @Deprecated public ListBuilder.RowBuilder();
+    ctor @Deprecated public ListBuilder.RowBuilder(android.net.Uri);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(long);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setEndOfSection(boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(long);
   }
 
-  @RequiresApi(19) public class SelectionBuilder {
-    ctor public SelectionBuilder();
-    method public androidx.slice.builders.SelectionBuilder! addOption(String!, CharSequence!);
-    method public androidx.slice.builders.SelectionBuilder! setContentDescription(CharSequence?);
-    method public androidx.slice.builders.SelectionBuilder! setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.SelectionBuilder! setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.SelectionBuilder! setLayoutDirection(int);
-    method public androidx.slice.builders.SelectionBuilder! setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.SelectionBuilder! setSelectedOption(String!);
-    method public androidx.slice.builders.SelectionBuilder! setSubtitle(CharSequence?);
-    method public androidx.slice.builders.SelectionBuilder! setTitle(CharSequence?);
+  @Deprecated @RequiresApi(19) public class SelectionBuilder {
+    ctor @Deprecated public SelectionBuilder();
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! addOption(String!, CharSequence!);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setContentDescription(CharSequence?);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setSelectedOption(String!);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setSubtitle(CharSequence?);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setTitle(CharSequence?);
   }
 
-  @RequiresApi(19) public class SliceAction implements androidx.slice.core.SliceAction {
-    method public static androidx.slice.builders.SliceAction! create(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! create(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createDeeplink(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createDeeplink(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, CharSequence, boolean);
-    method public android.app.PendingIntent getAction();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method public androidx.slice.builders.SliceAction setChecked(boolean);
-    method public androidx.slice.core.SliceAction setContentDescription(CharSequence);
-    method public androidx.slice.builders.SliceAction setKey(String);
-    method public androidx.slice.builders.SliceAction setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) public class SliceAction implements androidx.slice.core.SliceAction {
+    method @Deprecated public static androidx.slice.builders.SliceAction! create(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! create(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createDeeplink(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createDeeplink(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, CharSequence, boolean);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated public androidx.slice.builders.SliceAction setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.SliceAction setKey(String);
+    method @Deprecated public androidx.slice.builders.SliceAction setPriority(@IntRange(from=0) int);
   }
 
-  @RequiresApi(19) public abstract class TemplateSliceBuilder {
-    method public androidx.slice.Slice build();
+  @Deprecated @RequiresApi(19) public abstract class TemplateSliceBuilder {
+    method @Deprecated public androidx.slice.Slice build();
   }
 
 }
diff --git a/slice/slice-builders/api/restricted_current.txt b/slice/slice-builders/api/restricted_current.txt
index 84c8575..fb90065 100644
--- a/slice/slice-builders/api/restricted_current.txt
+++ b/slice/slice-builders/api/restricted_current.txt
@@ -1,210 +1,210 @@
 // Signature format: 4.0
 package androidx.slice.builders {
 
-  @RequiresApi(19) public class GridRowBuilder {
-    ctor public GridRowBuilder();
-    method public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
-    method public androidx.slice.builders.GridRowBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreAction(android.app.PendingIntent);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.GridRowBuilder setSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+  @Deprecated @RequiresApi(19) public class GridRowBuilder {
+    ctor @Deprecated public GridRowBuilder();
+    method @Deprecated public androidx.slice.builders.GridRowBuilder addCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder setSeeMoreCell(androidx.slice.builders.GridRowBuilder.CellBuilder);
   }
 
-  public static class GridRowBuilder.CellBuilder {
-    ctor public GridRowBuilder.CellBuilder();
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence?, boolean);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.GridRowBuilder.CellBuilder setSliceAction(androidx.slice.builders.SliceAction);
+  @Deprecated public static class GridRowBuilder.CellBuilder {
+    ctor @Deprecated public GridRowBuilder.CellBuilder();
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addImage(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addOverlayText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder addTitleText(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setContentIntent(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.GridRowBuilder.CellBuilder setSliceAction(androidx.slice.builders.SliceAction);
   }
 
-  @RequiresApi(19) public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
-    ctor @RequiresApi(26) public ListBuilder(android.content.Context, android.net.Uri, java.time.Duration?);
-    ctor public ListBuilder(android.content.Context, android.net.Uri, long);
-    method public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
-    method public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
-    method public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
-    method public androidx.slice.builders.ListBuilder addRating(androidx.slice.builders.ListBuilder.RatingBuilder);
-    method public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
-    method public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
-    method public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
-    method public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
-    method @RequiresApi(21) public androidx.slice.builders.ListBuilder setHostExtras(android.os.PersistableBundle);
-    method public androidx.slice.builders.ListBuilder setIsError(boolean);
-    method public androidx.slice.builders.ListBuilder setKeywords(java.util.Set<java.lang.String!>);
-    method public androidx.slice.builders.ListBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder setSeeMoreAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder setSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
-    field public static final int ACTION_WITH_LABEL = 6; // 0x6
-    field public static final int ICON_IMAGE = 0; // 0x0
-    field public static final long INFINITY = -1L; // 0xffffffffffffffffL
-    field public static final int LARGE_IMAGE = 2; // 0x2
-    field public static final int RANGE_MODE_DETERMINATE = 0; // 0x0
-    field public static final int RANGE_MODE_INDETERMINATE = 1; // 0x1
-    field public static final int RANGE_MODE_STAR_RATING = 2; // 0x2
-    field public static final int RAW_IMAGE_LARGE = 4; // 0x4
-    field public static final int RAW_IMAGE_SMALL = 3; // 0x3
-    field public static final int SMALL_IMAGE = 1; // 0x1
-    field public static final int UNKNOWN_IMAGE = 5; // 0x5
+  @Deprecated @RequiresApi(19) public class ListBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+    ctor @Deprecated @RequiresApi(26) public ListBuilder(android.content.Context, android.net.Uri, java.time.Duration?);
+    ctor @Deprecated public ListBuilder(android.content.Context, android.net.Uri, long);
+    method @Deprecated public androidx.slice.builders.ListBuilder addAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder addGridRow(androidx.slice.builders.GridRowBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addInputRange(androidx.slice.builders.ListBuilder.InputRangeBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRange(androidx.slice.builders.ListBuilder.RangeBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRating(androidx.slice.builders.ListBuilder.RatingBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder addSelection(androidx.slice.builders.SelectionBuilder);
+    method @Deprecated public androidx.slice.builders.ListBuilder setAccentColor(@ColorInt int);
+    method @Deprecated public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
+    method @Deprecated @RequiresApi(21) public androidx.slice.builders.ListBuilder setHostExtras(android.os.PersistableBundle);
+    method @Deprecated public androidx.slice.builders.ListBuilder setIsError(boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder setKeywords(java.util.Set<java.lang.String!>);
+    method @Deprecated public androidx.slice.builders.ListBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder setSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
+    field @Deprecated public static final int ACTION_WITH_LABEL = 6; // 0x6
+    field @Deprecated public static final int ICON_IMAGE = 0; // 0x0
+    field @Deprecated public static final long INFINITY = -1L; // 0xffffffffffffffffL
+    field @Deprecated public static final int LARGE_IMAGE = 2; // 0x2
+    field @Deprecated public static final int RANGE_MODE_DETERMINATE = 0; // 0x0
+    field @Deprecated public static final int RANGE_MODE_INDETERMINATE = 1; // 0x1
+    field @Deprecated public static final int RANGE_MODE_STAR_RATING = 2; // 0x2
+    field @Deprecated public static final int RAW_IMAGE_LARGE = 4; // 0x4
+    field @Deprecated public static final int RAW_IMAGE_SMALL = 3; // 0x3
+    field @Deprecated public static final int SMALL_IMAGE = 1; // 0x1
+    field @Deprecated public static final int UNKNOWN_IMAGE = 5; // 0x5
   }
 
-  public static class ListBuilder.HeaderBuilder {
-    ctor public ListBuilder.HeaderBuilder();
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public ListBuilder.HeaderBuilder(android.net.Uri);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence, boolean);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence, boolean);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence, boolean);
+  @Deprecated public static class ListBuilder.HeaderBuilder {
+    ctor @Deprecated public ListBuilder.HeaderBuilder();
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public ListBuilder.HeaderBuilder(android.net.Uri);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSubtitle(CharSequence, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setSummary(CharSequence, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.HeaderBuilder setTitle(CharSequence, boolean);
   }
 
-  public static class ListBuilder.InputRangeBuilder {
-    ctor public ListBuilder.InputRangeBuilder();
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setMin(int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setThumb(androidx.core.graphics.drawable.IconCompat);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.InputRangeBuilder setValue(int);
+  @Deprecated public static class ListBuilder.InputRangeBuilder {
+    ctor @Deprecated public ListBuilder.InputRangeBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setMin(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setThumb(androidx.core.graphics.drawable.IconCompat);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.InputRangeBuilder setValue(int);
   }
 
-  public static class ListBuilder.RangeBuilder {
-    ctor public ListBuilder.RangeBuilder();
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setMode(int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RangeBuilder setValue(int);
+  @Deprecated public static class ListBuilder.RangeBuilder {
+    ctor @Deprecated public ListBuilder.RangeBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setMode(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RangeBuilder setValue(int);
   }
 
-  public static final class ListBuilder.RatingBuilder {
-    ctor public ListBuilder.RatingBuilder();
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setMax(int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setMin(int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RatingBuilder setValue(float);
+  @Deprecated public static final class ListBuilder.RatingBuilder {
+    ctor @Deprecated public ListBuilder.RatingBuilder();
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setMax(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setMin(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RatingBuilder setValue(float);
   }
 
-  public static class ListBuilder.RowBuilder {
-    ctor public ListBuilder.RowBuilder();
-    ctor public ListBuilder.RowBuilder(android.net.Uri);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(long);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setContentDescription(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setEndOfSection(boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setLayoutDirection(int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence?, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence?, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction, boolean);
-    method public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(long);
+  @Deprecated public static class ListBuilder.RowBuilder {
+    ctor @Deprecated public ListBuilder.RowBuilder();
+    ctor @Deprecated public ListBuilder.RowBuilder(android.net.Uri);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder addEndItem(long);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setEndOfSection(boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setSubtitle(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitle(CharSequence?, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat, int);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.core.graphics.drawable.IconCompat?, int, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(androidx.slice.builders.SliceAction, boolean);
+    method @Deprecated public androidx.slice.builders.ListBuilder.RowBuilder setTitleItem(long);
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MessagingSliceBuilder extends androidx.slice.builders.TemplateSliceBuilder {
-    ctor public MessagingSliceBuilder(android.content.Context, android.net.Uri);
-    method public androidx.slice.builders.MessagingSliceBuilder! add(androidx.core.util.Consumer<androidx.slice.builders.MessagingSliceBuilder.MessageBuilder!>!);
-    method public androidx.slice.builders.MessagingSliceBuilder! add(androidx.slice.builders.MessagingSliceBuilder.MessageBuilder!);
-    field public static final int MAXIMUM_RETAINED_MESSAGES = 50; // 0x32
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class MessagingSliceBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+    ctor @Deprecated public MessagingSliceBuilder(android.content.Context, android.net.Uri);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder! add(androidx.core.util.Consumer<androidx.slice.builders.MessagingSliceBuilder.MessageBuilder!>!);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder! add(androidx.slice.builders.MessagingSliceBuilder.MessageBuilder!);
+    field @Deprecated public static final int MAXIMUM_RETAINED_MESSAGES = 50; // 0x32
   }
 
-  public static final class MessagingSliceBuilder.MessageBuilder extends androidx.slice.builders.TemplateSliceBuilder {
-    ctor public MessagingSliceBuilder.MessageBuilder(androidx.slice.builders.MessagingSliceBuilder!);
-    method @RequiresApi(23) public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addSource(android.graphics.drawable.Icon!);
-    method public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addSource(androidx.core.graphics.drawable.IconCompat!);
-    method public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addText(CharSequence!);
-    method public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addTimestamp(long);
+  @Deprecated public static final class MessagingSliceBuilder.MessageBuilder extends androidx.slice.builders.TemplateSliceBuilder {
+    ctor @Deprecated public MessagingSliceBuilder.MessageBuilder(androidx.slice.builders.MessagingSliceBuilder!);
+    method @Deprecated @RequiresApi(23) public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addSource(android.graphics.drawable.Icon!);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addSource(androidx.core.graphics.drawable.IconCompat!);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addText(CharSequence!);
+    method @Deprecated public androidx.slice.builders.MessagingSliceBuilder.MessageBuilder! addTimestamp(long);
   }
 
-  @RequiresApi(19) public class SelectionBuilder {
-    ctor public SelectionBuilder();
-    method public androidx.slice.builders.SelectionBuilder! addOption(String!, CharSequence!);
-    method public androidx.slice.builders.SelectionBuilder! setContentDescription(CharSequence?);
-    method public androidx.slice.builders.SelectionBuilder! setInputAction(android.app.PendingIntent);
-    method public androidx.slice.builders.SelectionBuilder! setInputAction(androidx.remotecallback.RemoteCallback);
-    method public androidx.slice.builders.SelectionBuilder! setLayoutDirection(int);
-    method public androidx.slice.builders.SelectionBuilder! setPrimaryAction(androidx.slice.builders.SliceAction);
-    method public androidx.slice.builders.SelectionBuilder! setSelectedOption(String!);
-    method public androidx.slice.builders.SelectionBuilder! setSubtitle(CharSequence?);
-    method public androidx.slice.builders.SelectionBuilder! setTitle(CharSequence?);
+  @Deprecated @RequiresApi(19) public class SelectionBuilder {
+    ctor @Deprecated public SelectionBuilder();
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! addOption(String!, CharSequence!);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setContentDescription(CharSequence?);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setInputAction(android.app.PendingIntent);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setInputAction(androidx.remotecallback.RemoteCallback);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setLayoutDirection(int);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setPrimaryAction(androidx.slice.builders.SliceAction);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setSelectedOption(String!);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setSubtitle(CharSequence?);
+    method @Deprecated public androidx.slice.builders.SelectionBuilder! setTitle(CharSequence?);
   }
 
-  @RequiresApi(19) public class SliceAction implements androidx.slice.core.SliceAction {
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, CharSequence, boolean);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, CharSequence, long, boolean);
-    method public static androidx.slice.builders.SliceAction! create(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! create(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createDeeplink(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createDeeplink(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
-    method public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    method public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, CharSequence, boolean);
-    method public android.app.PendingIntent getAction();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method public androidx.slice.builders.SliceAction setChecked(boolean);
-    method public androidx.slice.core.SliceAction setContentDescription(CharSequence);
-    method public androidx.slice.builders.SliceAction setKey(String);
-    method public androidx.slice.builders.SliceAction setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) public class SliceAction implements androidx.slice.core.SliceAction {
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, CharSequence, boolean);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceAction(android.app.PendingIntent, CharSequence, long, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! create(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! create(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createDeeplink(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createDeeplink(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, int, CharSequence);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(android.app.PendingIntent, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    method @Deprecated public static androidx.slice.builders.SliceAction! createToggle(androidx.remotecallback.RemoteCallback, CharSequence, boolean);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated public androidx.slice.builders.SliceAction setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.builders.SliceAction setKey(String);
+    method @Deprecated public androidx.slice.builders.SliceAction setPriority(@IntRange(from=0) int);
   }
 
-  @RequiresApi(19) public abstract class TemplateSliceBuilder {
-    method public androidx.slice.Slice build();
+  @Deprecated @RequiresApi(19) public abstract class TemplateSliceBuilder {
+    method @Deprecated public androidx.slice.Slice build();
   }
 
 }
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/GridRowBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/GridRowBuilder.java
index 4523685..dddc854 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/GridRowBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/GridRowBuilder.java
@@ -53,8 +53,13 @@
  * rest of the content, this will take up space as a cell item in a row if added.
  *
  * @see ListBuilder#addGridRow(GridRowBuilder)
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class GridRowBuilder {
 
     private final List<CellBuilder> mCells = new ArrayList<>();
@@ -256,7 +261,12 @@
      * @see ListBuilder#ICON_IMAGE
      * @see ListBuilder#SMALL_IMAGE
      * @see ListBuilder#ICON_IMAGE
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class CellBuilder {
         /**
          */
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/ListBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/ListBuilder.java
index 77d5f5b..1b27322 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/ListBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/ListBuilder.java
@@ -140,8 +140,13 @@
  * @see androidx.slice.SliceProvider
  * @see androidx.slice.SliceProvider#onBindSlice(Uri)
  * @see androidx.slice.widget.SliceView
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class ListBuilder extends TemplateSliceBuilder {
 
     private boolean mHasSeeMore;
@@ -565,7 +570,12 @@
      * A range row supports displaying a horizontal progress indicator.
      *
      * @see ListBuilder#addRange(RangeBuilder)
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class RangeBuilder {
 
         private int mValue;
@@ -817,8 +827,13 @@
      * An star rating row supports displaying a horizontal tappable stars allowing rating input.
      *
      * @see ListBuilder#addRating(RatingBuilder)
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
     @SuppressLint("MissingBuildMethod")
+    @Deprecated
     public static final class RatingBuilder {
         /**
          */
@@ -1085,7 +1100,12 @@
      * An input range row supports displaying a horizontal slider allowing slider input.
      *
      * @see ListBuilder#addInputRange(InputRangeBuilder)
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class InputRangeBuilder {
 
         private int mMin = 0;
@@ -1481,7 +1501,12 @@
      * </ul>
      *
      * @see ListBuilder#addRow(RowBuilder)
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class RowBuilder {
 
         private final Uri mUri;
@@ -2007,7 +2032,12 @@
      * @see ListBuilder#setHeader(HeaderBuilder)
      * @see ListBuilder#addAction(SliceAction)
      * @see SliceAction
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class HeaderBuilder {
         private final Uri mUri;
         private CharSequence mTitle;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/MessagingSliceBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/MessagingSliceBuilder.java
index 4041d50..b995566 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/MessagingSliceBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/MessagingSliceBuilder.java
@@ -41,6 +41,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class MessagingSliceBuilder extends TemplateSliceBuilder {
 
     /**
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/SelectionBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/SelectionBuilder.java
index 4e5550a..1e72a08 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/SelectionBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/SelectionBuilder.java
@@ -38,8 +38,13 @@
  *
  * A selection presents a list of options to the user and allows the user to select exactly one
  * option.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SelectionBuilder {
     private final List<Pair<String, CharSequence>> mOptions;
     private final Set<String> mOptionKeys;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/SliceAction.java b/slice/slice-builders/src/main/java/androidx/slice/builders/SliceAction.java
index 64263dd..bc06970 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/SliceAction.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/SliceAction.java
@@ -36,8 +36,13 @@
 /**
  * Class representing an action, supports tappable icons, custom toggle icons, and default
  * toggles, as well as date and time pickers.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceAction implements androidx.slice.core.SliceAction {
 
     private final SliceActionImpl mSliceAction;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
index 07accb6..f52b6d1 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/TemplateSliceBuilder.java
@@ -39,8 +39,13 @@
 
 /**
  * Base class of builders of various template types.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public abstract class TemplateSliceBuilder {
 
     private static final String TAG = "TemplateSliceBuilder";
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
index ec399bc..f7baa96 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
@@ -45,6 +45,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class GridRowBuilderListV1Impl extends TemplateBuilderImpl {
 
     private SliceAction mPrimaryAction;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
index 8718ac9..d9f47d6 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilder.java
@@ -42,6 +42,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public interface ListBuilder {
 
     /**
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
index faf5588..5848e3b 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderBasicImpl.java
@@ -60,6 +60,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ListBuilderBasicImpl extends TemplateBuilderImpl implements ListBuilder {
     boolean mIsError;
     private Set<String> mKeywords;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java
index 111bd0b..684f094 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/ListBuilderImpl.java
@@ -85,6 +85,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ListBuilderImpl extends TemplateBuilderImpl implements ListBuilder {
     private List<Slice> mSliceActions;
     private Set<String> mKeywords;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java
index 239e6fb..834b44a 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBasicImpl.java
@@ -37,6 +37,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class MessagingBasicImpl extends TemplateBuilderImpl implements
         MessagingBuilder {
     private MessageBuilder mLastMessage;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBuilder.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBuilder.java
index 03f48ce..3741273 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBuilder.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingBuilder.java
@@ -27,6 +27,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public interface MessagingBuilder {
     /**
      * Add a subslice to this builder.
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java
index 8dacf8f..90a82f7 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingListV1Impl.java
@@ -33,6 +33,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class MessagingListV1Impl extends TemplateBuilderImpl implements MessagingBuilder{
 
     private final ListBuilderImpl mListBuilder;
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java
index d913d79..0adf579 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/MessagingV1Impl.java
@@ -31,6 +31,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class MessagingV1Impl extends TemplateBuilderImpl implements MessagingBuilder {
 
     /**
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java
index 1c341f1..9dc3fca 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderBasicImpl.java
@@ -33,6 +33,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SelectionBuilderBasicImpl extends SelectionBuilderImpl {
     public SelectionBuilderBasicImpl(Slice.Builder sliceBuilder,
                                      SelectionBuilder selectionBuilder) {
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java
index f25d0a6..0647caf 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderImpl.java
@@ -28,6 +28,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public abstract class SelectionBuilderImpl extends TemplateBuilderImpl {
     private final SelectionBuilder mSelectionBuilder;
 
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java
index 0c25ad0..1d2d673 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/SelectionBuilderListV2Impl.java
@@ -41,6 +41,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SelectionBuilderListV2Impl extends SelectionBuilderImpl {
     public SelectionBuilderListV2Impl(Slice.Builder parentSliceBuilder,
                                   SelectionBuilder selectionBuilder) {
diff --git a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java
index 8cb26f66..a1be533 100644
--- a/slice/slice-builders/src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java
+++ b/slice/slice-builders/src/main/java/androidx/slice/builders/impl/TemplateBuilderImpl.java
@@ -43,6 +43,7 @@
  */
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public abstract class TemplateBuilderImpl {
 
     private Slice.Builder mSliceBuilder;
diff --git a/slice/slice-core/api/current.txt b/slice/slice-core/api/current.txt
index 6a6b471..bc4b051 100644
--- a/slice/slice-core/api/current.txt
+++ b/slice/slice-core/api/current.txt
@@ -1,87 +1,87 @@
 // Signature format: 4.0
 package androidx.slice {
 
-  @RequiresApi(19) public final class Slice implements androidx.versionedparcelable.VersionedParcelable {
-    method public java.util.List<java.lang.String!> getHints();
-    method public java.util.List<androidx.slice.SliceItem!> getItems();
-    method public android.net.Uri getUri();
-    field public static final String EXTRA_SELECTION = "android.app.slice.extra.SELECTION";
+  @Deprecated @RequiresApi(19) public final class Slice implements androidx.versionedparcelable.VersionedParcelable {
+    method @Deprecated public java.util.List<java.lang.String!> getHints();
+    method @Deprecated public java.util.List<androidx.slice.SliceItem!> getItems();
+    method @Deprecated public android.net.Uri getUri();
+    field @Deprecated public static final String EXTRA_SELECTION = "android.app.slice.extra.SELECTION";
   }
 
-  @RequiresApi(28) public class SliceConvert {
-    method public static android.app.slice.Slice? unwrap(androidx.slice.Slice?);
-    method public static androidx.slice.Slice? wrap(android.app.slice.Slice?, android.content.Context);
+  @Deprecated @RequiresApi(28) public class SliceConvert {
+    method @Deprecated public static android.app.slice.Slice? unwrap(androidx.slice.Slice?);
+    method @Deprecated public static androidx.slice.Slice? wrap(android.app.slice.Slice?, android.content.Context);
   }
 
-  @RequiresApi(19) public final class SliceItem implements androidx.versionedparcelable.VersionedParcelable {
-    method public static android.text.ParcelableSpan createSensitiveSpan();
-    method public void fireAction(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
-    method public android.app.PendingIntent? getAction();
-    method public String getFormat();
-    method public java.util.List<java.lang.String!> getHints();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getInt();
-    method public long getLong();
-    method public CharSequence? getRedactedText();
-    method public androidx.slice.Slice? getSlice();
-    method public String? getSubType();
-    method public CharSequence? getText();
-    method public boolean hasHint(String);
-    method public void onPostParceling();
-    method public void onPreParceling(boolean);
+  @Deprecated @RequiresApi(19) public final class SliceItem implements androidx.versionedparcelable.VersionedParcelable {
+    method @Deprecated public static android.text.ParcelableSpan createSensitiveSpan();
+    method @Deprecated public void fireAction(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated public android.app.PendingIntent? getAction();
+    method @Deprecated public String getFormat();
+    method @Deprecated public java.util.List<java.lang.String!> getHints();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getInt();
+    method @Deprecated public long getLong();
+    method @Deprecated public CharSequence? getRedactedText();
+    method @Deprecated public androidx.slice.Slice? getSlice();
+    method @Deprecated public String? getSubType();
+    method @Deprecated public CharSequence? getText();
+    method @Deprecated public boolean hasHint(String);
+    method @Deprecated public void onPostParceling();
+    method @Deprecated public void onPreParceling(boolean);
   }
 
-  @RequiresApi(19) public abstract class SliceManager {
-    method public abstract int checkSlicePermission(android.net.Uri, int, int);
-    method public static androidx.slice.SliceManager getInstance(android.content.Context);
-    method public abstract java.util.List<android.net.Uri!> getPinnedSlices();
-    method public abstract void grantSlicePermission(String, android.net.Uri);
-    method public abstract void revokeSlicePermission(String, android.net.Uri);
+  @Deprecated @RequiresApi(19) public abstract class SliceManager {
+    method @Deprecated public abstract int checkSlicePermission(android.net.Uri, int, int);
+    method @Deprecated public static androidx.slice.SliceManager getInstance(android.content.Context);
+    method @Deprecated public abstract java.util.List<android.net.Uri!> getPinnedSlices();
+    method @Deprecated public abstract void grantSlicePermission(String, android.net.Uri);
+    method @Deprecated public abstract void revokeSlicePermission(String, android.net.Uri);
   }
 
-  public abstract class SliceProvider extends android.content.ContentProvider {
-    ctor public SliceProvider();
-    ctor public SliceProvider(java.lang.String!...);
-    method public final int bulkInsert(android.net.Uri, android.content.ContentValues![]);
-    method @RequiresApi(19) public final android.net.Uri? canonicalize(android.net.Uri);
-    method public final int delete(android.net.Uri, String?, String![]?);
-    method @RequiresApi(19) public java.util.List<android.net.Uri!> getPinnedSlices();
-    method public final String? getType(android.net.Uri);
-    method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
-    method @RequiresApi(19) public abstract androidx.slice.Slice? onBindSlice(android.net.Uri);
-    method public final boolean onCreate();
-    method public android.app.PendingIntent? onCreatePermissionRequest(android.net.Uri, String);
-    method @RequiresApi(19) public abstract boolean onCreateSliceProvider();
-    method @RequiresApi(19) public java.util.Collection<android.net.Uri!> onGetSliceDescendants(android.net.Uri);
-    method @RequiresApi(19) public android.net.Uri onMapIntentToUri(android.content.Intent);
-    method @RequiresApi(19) public void onSlicePinned(android.net.Uri);
-    method @RequiresApi(19) public void onSliceUnpinned(android.net.Uri);
-    method @RequiresApi(28) public final android.database.Cursor? query(android.net.Uri, String![]?, android.os.Bundle?, android.os.CancellationSignal?);
-    method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
-    method @RequiresApi(16) public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?, android.os.CancellationSignal?);
-    method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  @Deprecated public abstract class SliceProvider extends android.content.ContentProvider {
+    ctor @Deprecated public SliceProvider();
+    ctor @Deprecated public SliceProvider(java.lang.String!...);
+    method @Deprecated public final int bulkInsert(android.net.Uri, android.content.ContentValues![]);
+    method @Deprecated @RequiresApi(19) public final android.net.Uri? canonicalize(android.net.Uri);
+    method @Deprecated public final int delete(android.net.Uri, String?, String![]?);
+    method @Deprecated @RequiresApi(19) public java.util.List<android.net.Uri!> getPinnedSlices();
+    method @Deprecated public final String? getType(android.net.Uri);
+    method @Deprecated public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method @Deprecated @RequiresApi(19) public abstract androidx.slice.Slice? onBindSlice(android.net.Uri);
+    method @Deprecated public final boolean onCreate();
+    method @Deprecated public android.app.PendingIntent? onCreatePermissionRequest(android.net.Uri, String);
+    method @Deprecated @RequiresApi(19) public abstract boolean onCreateSliceProvider();
+    method @Deprecated @RequiresApi(19) public java.util.Collection<android.net.Uri!> onGetSliceDescendants(android.net.Uri);
+    method @Deprecated @RequiresApi(19) public android.net.Uri onMapIntentToUri(android.content.Intent);
+    method @Deprecated @RequiresApi(19) public void onSlicePinned(android.net.Uri);
+    method @Deprecated @RequiresApi(19) public void onSliceUnpinned(android.net.Uri);
+    method @Deprecated @RequiresApi(28) public final android.database.Cursor? query(android.net.Uri, String![]?, android.os.Bundle?, android.os.CancellationSignal?);
+    method @Deprecated public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method @Deprecated @RequiresApi(16) public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?, android.os.CancellationSignal?);
+    method @Deprecated public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
   }
 
 }
 
 package androidx.slice.core {
 
-  @RequiresApi(19) public interface SliceAction {
-    method public android.app.PendingIntent getAction();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method public androidx.slice.core.SliceAction setChecked(boolean);
-    method public androidx.slice.core.SliceAction setContentDescription(CharSequence);
-    method public androidx.slice.core.SliceAction setKey(String);
-    method public androidx.slice.core.SliceAction setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) public interface SliceAction {
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated public androidx.slice.core.SliceAction setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.core.SliceAction setKey(String);
+    method @Deprecated public androidx.slice.core.SliceAction setPriority(@IntRange(from=0) int);
   }
 
 }
diff --git a/slice/slice-core/api/restricted_current.txt b/slice/slice-core/api/restricted_current.txt
index 332afef..b000782 100644
--- a/slice/slice-core/api/restricted_current.txt
+++ b/slice/slice-core/api/restricted_current.txt
@@ -1,307 +1,307 @@
 // Signature format: 4.0
 package androidx.slice {
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Clock {
-    method public long currentTimeMillis();
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface Clock {
+    method @Deprecated public long currentTimeMillis();
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CornerDrawable extends android.graphics.drawable.InsetDrawable {
-    ctor public CornerDrawable(android.graphics.drawable.Drawable?, float);
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CornerDrawable extends android.graphics.drawable.InsetDrawable {
+    ctor @Deprecated public CornerDrawable(android.graphics.drawable.Drawable?, float);
   }
 
-  @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, isCustom=true) public final class Slice extends androidx.versionedparcelable.CustomVersionedParcelable implements androidx.versionedparcelable.VersionedParcelable {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.Slice? bindSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>?);
-    method public java.util.List<java.lang.String!> getHints();
-    method public java.util.List<androidx.slice.SliceItem!> getItems();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceSpec? getSpec();
-    method public android.net.Uri getUri();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean hasHint(String);
-    field public static final String EXTRA_SELECTION = "android.app.slice.extra.SELECTION";
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String SUBTYPE_RANGE_MODE = "range_mode";
+  @Deprecated @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, isCustom=true) public final class Slice extends androidx.versionedparcelable.CustomVersionedParcelable implements androidx.versionedparcelable.VersionedParcelable {
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.Slice? bindSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>?);
+    method @Deprecated public java.util.List<java.lang.String!> getHints();
+    method @Deprecated public java.util.List<androidx.slice.SliceItem!> getItems();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceSpec? getSpec();
+    method @Deprecated public android.net.Uri getUri();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean hasHint(String);
+    field @Deprecated public static final String EXTRA_SELECTION = "android.app.slice.extra.SELECTION";
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final String SUBTYPE_RANGE_MODE = "range_mode";
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class Slice.Builder {
-    ctor public Slice.Builder(android.net.Uri);
-    ctor public Slice.Builder(androidx.slice.Slice.Builder);
-    method public androidx.slice.Slice.Builder addAction(android.app.PendingIntent, androidx.slice.Slice, String?);
-    method public androidx.slice.Slice.Builder addAction(androidx.slice.Slice, String?, androidx.slice.SliceItem.ActionHandler);
-    method public androidx.slice.Slice.Builder addHints(java.lang.String!...);
-    method public androidx.slice.Slice.Builder addHints(java.util.List<java.lang.String!>);
-    method public androidx.slice.Slice.Builder addIcon(androidx.core.graphics.drawable.IconCompat, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addIcon(androidx.core.graphics.drawable.IconCompat, String?, java.util.List<java.lang.String!>);
-    method public androidx.slice.Slice.Builder addInt(int, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addInt(int, String?, java.util.List<java.lang.String!>);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.slice.Slice.Builder addItem(androidx.slice.SliceItem);
-    method public androidx.slice.Slice.Builder addLong(long, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addLong(long, String?, java.util.List<java.lang.String!>);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, String?, java.lang.String!...);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, String?, java.util.List<java.lang.String!>);
-    method public androidx.slice.Slice.Builder addSubSlice(androidx.slice.Slice);
-    method public androidx.slice.Slice.Builder addSubSlice(androidx.slice.Slice, String?);
-    method public androidx.slice.Slice.Builder addText(CharSequence?, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addText(CharSequence?, String?, java.util.List<java.lang.String!>);
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class Slice.Builder {
+    ctor @Deprecated public Slice.Builder(android.net.Uri);
+    ctor @Deprecated public Slice.Builder(androidx.slice.Slice.Builder);
+    method @Deprecated public androidx.slice.Slice.Builder addAction(android.app.PendingIntent, androidx.slice.Slice, String?);
+    method @Deprecated public androidx.slice.Slice.Builder addAction(androidx.slice.Slice, String?, androidx.slice.SliceItem.ActionHandler);
+    method @Deprecated public androidx.slice.Slice.Builder addHints(java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addHints(java.util.List<java.lang.String!>);
+    method @Deprecated public androidx.slice.Slice.Builder addIcon(androidx.core.graphics.drawable.IconCompat, String?, java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addIcon(androidx.core.graphics.drawable.IconCompat, String?, java.util.List<java.lang.String!>);
+    method @Deprecated public androidx.slice.Slice.Builder addInt(int, String?, java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addInt(int, String?, java.util.List<java.lang.String!>);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.slice.Slice.Builder addItem(androidx.slice.SliceItem);
+    method @Deprecated public androidx.slice.Slice.Builder addLong(long, String?, java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addLong(long, String?, java.util.List<java.lang.String!>);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, String?, java.lang.String!...);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, String?, java.util.List<java.lang.String!>);
+    method @Deprecated public androidx.slice.Slice.Builder addSubSlice(androidx.slice.Slice);
+    method @Deprecated public androidx.slice.Slice.Builder addSubSlice(androidx.slice.Slice, String?);
+    method @Deprecated public androidx.slice.Slice.Builder addText(CharSequence?, String?, java.lang.String!...);
+    method @Deprecated public androidx.slice.Slice.Builder addText(CharSequence?, String?, java.util.List<java.lang.String!>);
     method @Deprecated public androidx.slice.Slice.Builder! addTimestamp(long, String?, java.lang.String!...);
-    method public androidx.slice.Slice.Builder addTimestamp(long, String?, java.util.List<java.lang.String!>);
-    method public androidx.slice.Slice build();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder setSpec(androidx.slice.SliceSpec?);
+    method @Deprecated public androidx.slice.Slice.Builder addTimestamp(long, String?, java.util.List<java.lang.String!>);
+    method @Deprecated public androidx.slice.Slice build();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.Slice.Builder setSpec(androidx.slice.SliceSpec?);
   }
 
-  @RequiresApi(28) public class SliceConvert {
-    method public static android.app.slice.Slice? unwrap(androidx.slice.Slice?);
-    method public static androidx.slice.Slice? wrap(android.app.slice.Slice?, android.content.Context);
+  @Deprecated @RequiresApi(28) public class SliceConvert {
+    method @Deprecated public static android.app.slice.Slice? unwrap(androidx.slice.Slice?);
+    method @Deprecated public static androidx.slice.Slice? wrap(android.app.slice.Slice?, android.content.Context);
   }
 
-  @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, ignoreParcelables=true, isCustom=true) public final class SliceItem extends androidx.versionedparcelable.CustomVersionedParcelable {
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem();
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(android.app.PendingIntent, androidx.slice.Slice?, String, String?, String![]);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(androidx.slice.SliceItem.ActionHandler, androidx.slice.Slice?, String, String?, String![]);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(Object!, String, String?, String![]);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(Object!, String, String?, java.util.List<java.lang.String!>);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void addHint(String);
-    method public static android.text.ParcelableSpan createSensitiveSpan();
-    method public void fireAction(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean fireActionInternal(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
-    method public android.app.PendingIntent? getAction();
-    method public String getFormat();
-    method public java.util.List<java.lang.String!> getHints();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method public int getInt();
-    method public long getLong();
-    method public CharSequence? getRedactedText();
-    method @RequiresApi(20) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.app.RemoteInput? getRemoteInput();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public CharSequence? getSanitizedText();
-    method public androidx.slice.Slice? getSlice();
-    method public String? getSubType();
-    method public CharSequence? getText();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public boolean hasAnyHints(java.lang.String!...);
-    method public boolean hasHint(String);
+  @Deprecated @RequiresApi(19) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true, ignoreParcelables=true, isCustom=true) public final class SliceItem extends androidx.versionedparcelable.CustomVersionedParcelable {
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem();
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(android.app.PendingIntent, androidx.slice.Slice?, String, String?, String![]);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(androidx.slice.SliceItem.ActionHandler, androidx.slice.Slice?, String, String?, String![]);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(Object!, String, String?, String![]);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceItem(Object!, String, String?, java.util.List<java.lang.String!>);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void addHint(String);
+    method @Deprecated public static android.text.ParcelableSpan createSensitiveSpan();
+    method @Deprecated public void fireAction(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean fireActionInternal(android.content.Context?, android.content.Intent?) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated public android.app.PendingIntent? getAction();
+    method @Deprecated public String getFormat();
+    method @Deprecated public java.util.List<java.lang.String!> getHints();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated public int getInt();
+    method @Deprecated public long getLong();
+    method @Deprecated public CharSequence? getRedactedText();
+    method @Deprecated @RequiresApi(20) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.app.RemoteInput? getRemoteInput();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public CharSequence? getSanitizedText();
+    method @Deprecated public androidx.slice.Slice? getSlice();
+    method @Deprecated public String? getSubType();
+    method @Deprecated public CharSequence? getText();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public boolean hasAnyHints(java.lang.String!...);
+    method @Deprecated public boolean hasHint(String);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static interface SliceItem.ActionHandler {
-    method public void onAction(androidx.slice.SliceItem, android.content.Context?, android.content.Intent?);
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static interface SliceItem.ActionHandler {
+    method @Deprecated public void onAction(androidx.slice.SliceItem, android.content.Context?, android.content.Intent?);
   }
 
-  @RequiresApi(19) public abstract class SliceManager {
-    method @androidx.core.content.PermissionChecker.PermissionResult public abstract int checkSlicePermission(android.net.Uri, int, int);
-    method public static androidx.slice.SliceManager getInstance(android.content.Context);
-    method public abstract java.util.List<android.net.Uri!> getPinnedSlices();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public abstract java.util.Set<androidx.slice.SliceSpec!> getPinnedSpecs(android.net.Uri);
-    method public abstract void grantSlicePermission(String, android.net.Uri);
-    method public abstract void revokeSlicePermission(String, android.net.Uri);
+  @Deprecated @RequiresApi(19) public abstract class SliceManager {
+    method @Deprecated @androidx.core.content.PermissionChecker.PermissionResult public abstract int checkSlicePermission(android.net.Uri, int, int);
+    method @Deprecated public static androidx.slice.SliceManager getInstance(android.content.Context);
+    method @Deprecated public abstract java.util.List<android.net.Uri!> getPinnedSlices();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public abstract java.util.Set<androidx.slice.SliceSpec!> getPinnedSpecs(android.net.Uri);
+    method @Deprecated public abstract void grantSlicePermission(String, android.net.Uri);
+    method @Deprecated public abstract void revokeSlicePermission(String, android.net.Uri);
   }
 
-  public abstract class SliceProvider extends android.content.ContentProvider implements androidx.core.app.CoreComponentFactory.CompatWrapped {
-    ctor public SliceProvider();
-    ctor public SliceProvider(java.lang.String!...);
-    method public final int bulkInsert(android.net.Uri, android.content.ContentValues![]);
-    method @RequiresApi(19) public final android.net.Uri? canonicalize(android.net.Uri);
-    method public final int delete(android.net.Uri, String?, String![]?);
-    method @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.slice.Clock? getClock();
-    method @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.Set<androidx.slice.SliceSpec!>? getCurrentSpecs();
-    method @RequiresApi(19) public java.util.List<android.net.Uri!> getPinnedSlices();
-    method public final String? getType(android.net.Uri);
+  @Deprecated public abstract class SliceProvider extends android.content.ContentProvider implements androidx.core.app.CoreComponentFactory.CompatWrapped {
+    ctor @Deprecated public SliceProvider();
+    ctor @Deprecated public SliceProvider(java.lang.String!...);
+    method @Deprecated public final int bulkInsert(android.net.Uri, android.content.ContentValues![]);
+    method @Deprecated @RequiresApi(19) public final android.net.Uri? canonicalize(android.net.Uri);
+    method @Deprecated public final int delete(android.net.Uri, String?, String![]?);
+    method @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.slice.Clock? getClock();
+    method @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static java.util.Set<androidx.slice.SliceSpec!>? getCurrentSpecs();
+    method @Deprecated @RequiresApi(19) public java.util.List<android.net.Uri!> getPinnedSlices();
+    method @Deprecated public final String? getType(android.net.Uri);
     method @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public Object? getWrapper();
-    method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
-    method @RequiresApi(19) public abstract androidx.slice.Slice? onBindSlice(android.net.Uri);
-    method public final boolean onCreate();
-    method public android.app.PendingIntent? onCreatePermissionRequest(android.net.Uri, String);
-    method @RequiresApi(19) public abstract boolean onCreateSliceProvider();
-    method @RequiresApi(19) public java.util.Collection<android.net.Uri!> onGetSliceDescendants(android.net.Uri);
-    method @RequiresApi(19) public android.net.Uri onMapIntentToUri(android.content.Intent);
-    method @RequiresApi(19) public void onSlicePinned(android.net.Uri);
-    method @RequiresApi(19) public void onSliceUnpinned(android.net.Uri);
-    method @RequiresApi(28) public final android.database.Cursor? query(android.net.Uri, String![]?, android.os.Bundle?, android.os.CancellationSignal?);
-    method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
-    method @RequiresApi(16) public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?, android.os.CancellationSignal?);
-    method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+    method @Deprecated public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method @Deprecated @RequiresApi(19) public abstract androidx.slice.Slice? onBindSlice(android.net.Uri);
+    method @Deprecated public final boolean onCreate();
+    method @Deprecated public android.app.PendingIntent? onCreatePermissionRequest(android.net.Uri, String);
+    method @Deprecated @RequiresApi(19) public abstract boolean onCreateSliceProvider();
+    method @Deprecated @RequiresApi(19) public java.util.Collection<android.net.Uri!> onGetSliceDescendants(android.net.Uri);
+    method @Deprecated @RequiresApi(19) public android.net.Uri onMapIntentToUri(android.content.Intent);
+    method @Deprecated @RequiresApi(19) public void onSlicePinned(android.net.Uri);
+    method @Deprecated @RequiresApi(19) public void onSliceUnpinned(android.net.Uri);
+    method @Deprecated @RequiresApi(28) public final android.database.Cursor? query(android.net.Uri, String![]?, android.os.Bundle?, android.os.CancellationSignal?);
+    method @Deprecated public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method @Deprecated @RequiresApi(16) public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?, android.os.CancellationSignal?);
+    method @Deprecated public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public final class SliceSpec implements androidx.versionedparcelable.VersionedParcelable {
-    ctor public SliceSpec(String, int);
-    method public boolean canRender(androidx.slice.SliceSpec);
-    method public int getRevision();
-    method public String getType();
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public final class SliceSpec implements androidx.versionedparcelable.VersionedParcelable {
+    ctor @Deprecated public SliceSpec(String, int);
+    method @Deprecated public boolean canRender(androidx.slice.SliceSpec);
+    method @Deprecated public int getRevision();
+    method @Deprecated public String getType();
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceSpecs {
-    field public static final androidx.slice.SliceSpec! BASIC;
-    field public static final androidx.slice.SliceSpec! LIST;
-    field public static final androidx.slice.SliceSpec! LIST_V2;
-    field public static final androidx.slice.SliceSpec! MESSAGING;
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceSpecs {
+    field @Deprecated public static final androidx.slice.SliceSpec! BASIC;
+    field @Deprecated public static final androidx.slice.SliceSpec! LIST;
+    field @Deprecated public static final androidx.slice.SliceSpec! LIST_V2;
+    field @Deprecated public static final androidx.slice.SliceSpec! MESSAGING;
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SystemClock implements androidx.slice.Clock {
-    ctor public SystemClock();
-    method public long currentTimeMillis();
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SystemClock implements androidx.slice.Clock {
+    ctor @Deprecated public SystemClock();
+    method @Deprecated public long currentTimeMillis();
   }
 
 }
 
 package androidx.slice.compat {
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CompatPermissionManager {
-    ctor public CompatPermissionManager(android.content.Context, String, int, String![]);
-    method public int checkSlicePermission(android.net.Uri, int, int);
-    method public void grantSlicePermission(android.net.Uri, String);
-    method public void revokeSlicePermission(android.net.Uri, String);
-    field public static final String ALL_SUFFIX = "_all";
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class CompatPermissionManager {
+    ctor @Deprecated public CompatPermissionManager(android.content.Context, String, int, String![]);
+    method @Deprecated public int checkSlicePermission(android.net.Uri, int, int);
+    method @Deprecated public void grantSlicePermission(android.net.Uri, String);
+    method @Deprecated public void revokeSlicePermission(android.net.Uri, String);
+    field @Deprecated public static final String ALL_SUFFIX = "_all";
   }
 
-  public static class CompatPermissionManager.PermissionState {
-    method public String getKey();
-    method public boolean hasAccess(java.util.List<java.lang.String!>);
-    method public boolean hasAllPermissions();
-    method public java.util.Set<java.lang.String!> toPersistable();
+  @Deprecated public static class CompatPermissionManager.PermissionState {
+    method @Deprecated public String getKey();
+    method @Deprecated public boolean hasAccess(java.util.List<java.lang.String!>);
+    method @Deprecated public boolean hasAllPermissions();
+    method @Deprecated public java.util.Set<java.lang.String!> toPersistable();
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class SliceProviderCompat {
-    ctor public SliceProviderCompat(androidx.slice.SliceProvider, androidx.slice.compat.CompatPermissionManager, android.content.Context);
-    method public static void addSpecs(android.os.Bundle, java.util.Set<androidx.slice.SliceSpec!>);
-    method public static androidx.slice.Slice? bindSlice(android.content.Context, android.content.Intent, java.util.Set<androidx.slice.SliceSpec!>);
-    method public static androidx.slice.Slice? bindSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
-    method public android.os.Bundle? call(String, String?, android.os.Bundle);
-    method public static int checkSlicePermission(android.content.Context, String?, android.net.Uri, int, int);
-    method public String? getCallingPackage();
-    method public static java.util.List<android.net.Uri!> getPinnedSlices(android.content.Context);
-    method public static java.util.Set<androidx.slice.SliceSpec!>? getPinnedSpecs(android.content.Context, android.net.Uri);
-    method public static java.util.Collection<android.net.Uri!> getSliceDescendants(android.content.Context, android.net.Uri);
-    method public static java.util.Set<androidx.slice.SliceSpec!> getSpecs(android.os.Bundle);
-    method public static void grantSlicePermission(android.content.Context, String?, String?, android.net.Uri);
-    method public static android.net.Uri? mapIntentToUri(android.content.Context, android.content.Intent);
-    method public static void pinSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
-    method public static void revokeSlicePermission(android.content.Context, String?, String?, android.net.Uri);
-    method public static void unpinSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
-    field public static final String ARG_SUPPORTS_VERSIONED_PARCELABLE = "supports_versioned_parcelable";
-    field public static final String EXTRA_BIND_URI = "slice_uri";
-    field public static final String EXTRA_INTENT = "slice_intent";
-    field public static final String EXTRA_PID = "pid";
-    field public static final String EXTRA_PKG = "pkg";
-    field public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
-    field public static final String EXTRA_RESULT = "result";
-    field public static final String EXTRA_SLICE = "slice";
-    field public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
-    field public static final String EXTRA_SUPPORTED_SPECS = "specs";
-    field public static final String EXTRA_SUPPORTED_SPECS_REVS = "revs";
-    field public static final String EXTRA_UID = "uid";
-    field public static final String METHOD_CHECK_PERMISSION = "check_perms";
-    field public static final String METHOD_GET_DESCENDANTS = "get_descendants";
-    field public static final String METHOD_GET_PINNED_SPECS = "get_specs";
-    field public static final String METHOD_GRANT_PERMISSION = "grant_perms";
-    field public static final String METHOD_MAP_INTENT = "map_slice";
-    field public static final String METHOD_MAP_ONLY_INTENT = "map_only";
-    field public static final String METHOD_PIN = "pin_slice";
-    field public static final String METHOD_REVOKE_PERMISSION = "revoke_perms";
-    field public static final String METHOD_SLICE = "bind_slice";
-    field public static final String METHOD_UNPIN = "unpin_slice";
-    field public static final String PERMS_PREFIX = "slice_perms_";
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class SliceProviderCompat {
+    ctor @Deprecated public SliceProviderCompat(androidx.slice.SliceProvider, androidx.slice.compat.CompatPermissionManager, android.content.Context);
+    method @Deprecated public static void addSpecs(android.os.Bundle, java.util.Set<androidx.slice.SliceSpec!>);
+    method @Deprecated public static androidx.slice.Slice? bindSlice(android.content.Context, android.content.Intent, java.util.Set<androidx.slice.SliceSpec!>);
+    method @Deprecated public static androidx.slice.Slice? bindSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
+    method @Deprecated public android.os.Bundle? call(String, String?, android.os.Bundle);
+    method @Deprecated public static int checkSlicePermission(android.content.Context, String?, android.net.Uri, int, int);
+    method @Deprecated public String? getCallingPackage();
+    method @Deprecated public static java.util.List<android.net.Uri!> getPinnedSlices(android.content.Context);
+    method @Deprecated public static java.util.Set<androidx.slice.SliceSpec!>? getPinnedSpecs(android.content.Context, android.net.Uri);
+    method @Deprecated public static java.util.Collection<android.net.Uri!> getSliceDescendants(android.content.Context, android.net.Uri);
+    method @Deprecated public static java.util.Set<androidx.slice.SliceSpec!> getSpecs(android.os.Bundle);
+    method @Deprecated public static void grantSlicePermission(android.content.Context, String?, String?, android.net.Uri);
+    method @Deprecated public static android.net.Uri? mapIntentToUri(android.content.Context, android.content.Intent);
+    method @Deprecated public static void pinSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
+    method @Deprecated public static void revokeSlicePermission(android.content.Context, String?, String?, android.net.Uri);
+    method @Deprecated public static void unpinSlice(android.content.Context, android.net.Uri, java.util.Set<androidx.slice.SliceSpec!>);
+    field @Deprecated public static final String ARG_SUPPORTS_VERSIONED_PARCELABLE = "supports_versioned_parcelable";
+    field @Deprecated public static final String EXTRA_BIND_URI = "slice_uri";
+    field @Deprecated public static final String EXTRA_INTENT = "slice_intent";
+    field @Deprecated public static final String EXTRA_PID = "pid";
+    field @Deprecated public static final String EXTRA_PKG = "pkg";
+    field @Deprecated public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
+    field @Deprecated public static final String EXTRA_RESULT = "result";
+    field @Deprecated public static final String EXTRA_SLICE = "slice";
+    field @Deprecated public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
+    field @Deprecated public static final String EXTRA_SUPPORTED_SPECS = "specs";
+    field @Deprecated public static final String EXTRA_SUPPORTED_SPECS_REVS = "revs";
+    field @Deprecated public static final String EXTRA_UID = "uid";
+    field @Deprecated public static final String METHOD_CHECK_PERMISSION = "check_perms";
+    field @Deprecated public static final String METHOD_GET_DESCENDANTS = "get_descendants";
+    field @Deprecated public static final String METHOD_GET_PINNED_SPECS = "get_specs";
+    field @Deprecated public static final String METHOD_GRANT_PERMISSION = "grant_perms";
+    field @Deprecated public static final String METHOD_MAP_INTENT = "map_slice";
+    field @Deprecated public static final String METHOD_MAP_ONLY_INTENT = "map_only";
+    field @Deprecated public static final String METHOD_PIN = "pin_slice";
+    field @Deprecated public static final String METHOD_REVOKE_PERMISSION = "revoke_perms";
+    field @Deprecated public static final String METHOD_SLICE = "bind_slice";
+    field @Deprecated public static final String METHOD_UNPIN = "unpin_slice";
+    field @Deprecated public static final String PERMS_PREFIX = "slice_perms_";
   }
 
 }
 
 package androidx.slice.core {
 
-  @RequiresApi(19) public interface SliceAction {
-    method public android.app.PendingIntent getAction();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method @androidx.slice.core.SliceHints.ImageMode public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method public androidx.slice.core.SliceAction setChecked(boolean);
-    method public androidx.slice.core.SliceAction setContentDescription(CharSequence);
-    method public androidx.slice.core.SliceAction setKey(String);
-    method public androidx.slice.core.SliceAction setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) public interface SliceAction {
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated @androidx.slice.core.SliceHints.ImageMode public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated public androidx.slice.core.SliceAction setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.core.SliceAction setKey(String);
+    method @Deprecated public androidx.slice.core.SliceAction setPriority(@IntRange(from=0) int);
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceActionImpl implements androidx.slice.core.SliceAction {
-    ctor public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, @androidx.slice.core.SliceHints.ImageMode int, CharSequence);
-    ctor public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence);
-    ctor public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
-    ctor public SliceActionImpl(android.app.PendingIntent, CharSequence, boolean);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceActionImpl(androidx.slice.SliceItem);
-    method public androidx.slice.Slice buildPrimaryActionSlice(androidx.slice.Slice.Builder);
-    method public androidx.slice.Slice buildSlice(androidx.slice.Slice.Builder);
-    method public android.app.PendingIntent getAction();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getActionItem();
-    method public CharSequence? getContentDescription();
-    method public androidx.core.graphics.drawable.IconCompat? getIcon();
-    method @androidx.slice.core.SliceHints.ImageMode public int getImageMode();
-    method public String? getKey();
-    method public int getPriority();
-    method public androidx.slice.SliceItem? getSliceItem();
-    method public String? getSubtype();
-    method public CharSequence getTitle();
-    method public boolean isActivity();
-    method public boolean isChecked();
-    method public boolean isDefaultToggle();
-    method public boolean isToggle();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int parseImageMode(androidx.slice.SliceItem);
-    method public void setActivity(boolean);
-    method public androidx.slice.core.SliceActionImpl setChecked(boolean);
-    method public androidx.slice.core.SliceAction? setContentDescription(CharSequence);
-    method public androidx.slice.core.SliceActionImpl setKey(String);
-    method public androidx.slice.core.SliceActionImpl setPriority(@IntRange(from=0) int);
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceActionImpl implements androidx.slice.core.SliceAction {
+    ctor @Deprecated public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, @androidx.slice.core.SliceHints.ImageMode int, CharSequence);
+    ctor @Deprecated public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence);
+    ctor @Deprecated public SliceActionImpl(android.app.PendingIntent, androidx.core.graphics.drawable.IconCompat, CharSequence, boolean);
+    ctor @Deprecated public SliceActionImpl(android.app.PendingIntent, CharSequence, boolean);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public SliceActionImpl(androidx.slice.SliceItem);
+    method @Deprecated public androidx.slice.Slice buildPrimaryActionSlice(androidx.slice.Slice.Builder);
+    method @Deprecated public androidx.slice.Slice buildSlice(androidx.slice.Slice.Builder);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getActionItem();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getIcon();
+    method @Deprecated @androidx.slice.core.SliceHints.ImageMode public int getImageMode();
+    method @Deprecated public String? getKey();
+    method @Deprecated public int getPriority();
+    method @Deprecated public androidx.slice.SliceItem? getSliceItem();
+    method @Deprecated public String? getSubtype();
+    method @Deprecated public CharSequence getTitle();
+    method @Deprecated public boolean isActivity();
+    method @Deprecated public boolean isChecked();
+    method @Deprecated public boolean isDefaultToggle();
+    method @Deprecated public boolean isToggle();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static int parseImageMode(androidx.slice.SliceItem);
+    method @Deprecated public void setActivity(boolean);
+    method @Deprecated public androidx.slice.core.SliceActionImpl setChecked(boolean);
+    method @Deprecated public androidx.slice.core.SliceAction? setContentDescription(CharSequence);
+    method @Deprecated public androidx.slice.core.SliceActionImpl setKey(String);
+    method @Deprecated public androidx.slice.core.SliceActionImpl setPriority(@IntRange(from=0) int);
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceHints {
-    field public static final int ACTION_WITH_LABEL = 6; // 0x6
-    field public static final int DETERMINATE_RANGE = 0; // 0x0
-    field public static final String HINT_ACTIVITY = "activity";
-    field public static final String HINT_CACHED = "cached";
-    field public static final String HINT_END_OF_SECTION = "end_of_section";
-    field public static final String HINT_OVERLAY = "overlay";
-    field public static final String HINT_RAW = "raw";
-    field public static final String HINT_SELECTION_OPTION = "selection_option";
-    field public static final String HINT_SHOW_LABEL = "show_label";
-    field public static final int ICON_IMAGE = 0; // 0x0
-    field public static final int INDETERMINATE_RANGE = 1; // 0x1
-    field public static final long INFINITY = -1L; // 0xffffffffffffffffL
-    field public static final int LARGE_IMAGE = 2; // 0x2
-    field public static final int RAW_IMAGE_LARGE = 4; // 0x4
-    field public static final int RAW_IMAGE_SMALL = 3; // 0x3
-    field public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
-    field public static final int SMALL_IMAGE = 1; // 0x1
-    field public static final int STAR_RATING = 2; // 0x2
-    field public static final String SUBTYPE_ACTION_KEY = "action_key";
-    field public static final String SUBTYPE_DATE_PICKER = "date_picker";
-    field public static final String SUBTYPE_HOST_EXTRAS = "host_extras";
-    field public static final String SUBTYPE_MILLIS = "millis";
-    field public static final String SUBTYPE_MIN = "min";
-    field public static final String SUBTYPE_SELECTION = "selection";
-    field public static final String SUBTYPE_SELECTION_OPTION_KEY = "selection_option_key";
-    field public static final String SUBTYPE_SELECTION_OPTION_VALUE = "selection_option_value";
-    field public static final String SUBTYPE_TIME_PICKER = "time_picker";
-    field public static final int UNKNOWN_IMAGE = 5; // 0x5
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceHints {
+    field @Deprecated public static final int ACTION_WITH_LABEL = 6; // 0x6
+    field @Deprecated public static final int DETERMINATE_RANGE = 0; // 0x0
+    field @Deprecated public static final String HINT_ACTIVITY = "activity";
+    field @Deprecated public static final String HINT_CACHED = "cached";
+    field @Deprecated public static final String HINT_END_OF_SECTION = "end_of_section";
+    field @Deprecated public static final String HINT_OVERLAY = "overlay";
+    field @Deprecated public static final String HINT_RAW = "raw";
+    field @Deprecated public static final String HINT_SELECTION_OPTION = "selection_option";
+    field @Deprecated public static final String HINT_SHOW_LABEL = "show_label";
+    field @Deprecated public static final int ICON_IMAGE = 0; // 0x0
+    field @Deprecated public static final int INDETERMINATE_RANGE = 1; // 0x1
+    field @Deprecated public static final long INFINITY = -1L; // 0xffffffffffffffffL
+    field @Deprecated public static final int LARGE_IMAGE = 2; // 0x2
+    field @Deprecated public static final int RAW_IMAGE_LARGE = 4; // 0x4
+    field @Deprecated public static final int RAW_IMAGE_SMALL = 3; // 0x3
+    field @Deprecated public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
+    field @Deprecated public static final int SMALL_IMAGE = 1; // 0x1
+    field @Deprecated public static final int STAR_RATING = 2; // 0x2
+    field @Deprecated public static final String SUBTYPE_ACTION_KEY = "action_key";
+    field @Deprecated public static final String SUBTYPE_DATE_PICKER = "date_picker";
+    field @Deprecated public static final String SUBTYPE_HOST_EXTRAS = "host_extras";
+    field @Deprecated public static final String SUBTYPE_MILLIS = "millis";
+    field @Deprecated public static final String SUBTYPE_MIN = "min";
+    field @Deprecated public static final String SUBTYPE_SELECTION = "selection";
+    field @Deprecated public static final String SUBTYPE_SELECTION_OPTION_KEY = "selection_option_key";
+    field @Deprecated public static final String SUBTYPE_SELECTION_OPTION_VALUE = "selection_option_value";
+    field @Deprecated public static final String SUBTYPE_TIME_PICKER = "time_picker";
+    field @Deprecated public static final int UNKNOWN_IMAGE = 5; // 0x5
   }
 
-  @IntDef({androidx.slice.core.SliceHints.LARGE_IMAGE, androidx.slice.core.SliceHints.SMALL_IMAGE, androidx.slice.core.SliceHints.ICON_IMAGE, androidx.slice.core.SliceHints.RAW_IMAGE_SMALL, androidx.slice.core.SliceHints.RAW_IMAGE_LARGE, androidx.slice.core.SliceHints.UNKNOWN_IMAGE, androidx.slice.core.SliceHints.ACTION_WITH_LABEL}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceHints.ImageMode {
+  @Deprecated @IntDef({androidx.slice.core.SliceHints.LARGE_IMAGE, androidx.slice.core.SliceHints.SMALL_IMAGE, androidx.slice.core.SliceHints.ICON_IMAGE, androidx.slice.core.SliceHints.RAW_IMAGE_SMALL, androidx.slice.core.SliceHints.RAW_IMAGE_LARGE, androidx.slice.core.SliceHints.UNKNOWN_IMAGE, androidx.slice.core.SliceHints.ACTION_WITH_LABEL}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceHints.ImageMode {
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceQuery {
-    method public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?, String?, String?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?, String![]?, String![]?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?, String?, String?);
-    method public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?, String![]?, String![]?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.Slice, String?, String?, String?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.Slice, String?, String![]?, String![]?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?, String?, String?);
-    method public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?, String![]?, String![]?);
-    method public static androidx.slice.SliceItem? findItem(androidx.slice.Slice, android.net.Uri);
-    method public static androidx.slice.SliceItem? findNotContaining(androidx.slice.SliceItem?, java.util.List<androidx.slice.SliceItem!>);
-    method public static androidx.slice.SliceItem? findSubtype(androidx.slice.Slice?, String?, String?);
-    method public static androidx.slice.SliceItem? findSubtype(androidx.slice.SliceItem?, String?, String?);
-    method public static androidx.slice.SliceItem? findTopLevelItem(androidx.slice.Slice, String?, String?, String![]?, String![]?);
-    method public static boolean hasAnyHints(androidx.slice.SliceItem, java.lang.String!...);
-    method public static boolean hasHints(androidx.slice.Slice, java.lang.String!...);
-    method public static boolean hasHints(androidx.slice.SliceItem, java.lang.String!...);
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class SliceQuery {
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?, String?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.Slice?, String?, String![]?, String![]?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?, String?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? find(androidx.slice.SliceItem?, String?, String![]?, String![]?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.Slice, String?, String?, String?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.Slice, String?, String![]?, String![]?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?, String?, String?);
+    method @Deprecated public static java.util.List<androidx.slice.SliceItem!> findAll(androidx.slice.SliceItem, String?, String![]?, String![]?);
+    method @Deprecated public static androidx.slice.SliceItem? findItem(androidx.slice.Slice, android.net.Uri);
+    method @Deprecated public static androidx.slice.SliceItem? findNotContaining(androidx.slice.SliceItem?, java.util.List<androidx.slice.SliceItem!>);
+    method @Deprecated public static androidx.slice.SliceItem? findSubtype(androidx.slice.Slice?, String?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? findSubtype(androidx.slice.SliceItem?, String?, String?);
+    method @Deprecated public static androidx.slice.SliceItem? findTopLevelItem(androidx.slice.Slice, String?, String?, String![]?, String![]?);
+    method @Deprecated public static boolean hasAnyHints(androidx.slice.SliceItem, java.lang.String!...);
+    method @Deprecated public static boolean hasHints(androidx.slice.Slice, java.lang.String!...);
+    method @Deprecated public static boolean hasHints(androidx.slice.SliceItem, java.lang.String!...);
   }
 
 }
diff --git a/slice/slice-core/src/main/java/androidx/slice/ArrayUtils.java b/slice/slice-core/src/main/java/androidx/slice/ArrayUtils.java
index e5dd970..82d9b0a 100644
--- a/slice/slice-core/src/main/java/androidx/slice/ArrayUtils.java
+++ b/slice/slice-core/src/main/java/androidx/slice/ArrayUtils.java
@@ -27,6 +27,7 @@
  */
 @RestrictTo(Scope.LIBRARY_GROUP)
 @RequiresApi(19)
+@Deprecated
 class ArrayUtils {
 
     public static <T> boolean contains(T[] array, T item) {
diff --git a/slice/slice-core/src/main/java/androidx/slice/Clock.java b/slice/slice-core/src/main/java/androidx/slice/Clock.java
index f68560c..fc5c059 100644
--- a/slice/slice-core/src/main/java/androidx/slice/Clock.java
+++ b/slice/slice-core/src/main/java/androidx/slice/Clock.java
@@ -25,6 +25,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public interface Clock {
     long currentTimeMillis();
 }
diff --git a/slice/slice-core/src/main/java/androidx/slice/CornerDrawable.java b/slice/slice-core/src/main/java/androidx/slice/CornerDrawable.java
index 9ccb915..740cb1c 100644
--- a/slice/slice-core/src/main/java/androidx/slice/CornerDrawable.java
+++ b/slice/slice-core/src/main/java/androidx/slice/CornerDrawable.java
@@ -32,6 +32,7 @@
  *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Deprecated
 public class CornerDrawable extends InsetDrawable {
     private float mCornerRadius;
     private final Path mPath = new Path();
diff --git a/slice/slice-core/src/main/java/androidx/slice/Slice.java b/slice/slice-core/src/main/java/androidx/slice/Slice.java
index 8044c1e..b9ce51d 100644
--- a/slice/slice-core/src/main/java/androidx/slice/Slice.java
+++ b/slice/slice-core/src/main/java/androidx/slice/Slice.java
@@ -90,9 +90,14 @@
  * <p>Slices are constructed using {@link androidx.slice.builders.TemplateSliceBuilder}s
  * in a tree structure that provides the OS some information about how the content should be
  * displayed.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @VersionedParcelize(allowSerialization = true, isCustom = true)
 @RequiresApi(19)
+@Deprecated
 public final class Slice extends CustomVersionedParcelable implements VersionedParcelable {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceConvert.java b/slice/slice-core/src/main/java/androidx/slice/SliceConvert.java
index 3711e65..abae809e 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceConvert.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceConvert.java
@@ -43,8 +43,13 @@
 /**
  * Convert between {@link androidx.slice.Slice androidx.slice.Slice} and
  * {@link android.app.slice.Slice android.app.slice.Slice}
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(28)
+@Deprecated
 public class SliceConvert {
 
     private static final String TAG = "SliceConvert";
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceItem.java b/slice/slice-core/src/main/java/androidx/slice/SliceItem.java
index c35fdef..47d353c 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceItem.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceItem.java
@@ -86,9 +86,14 @@
  * The hints that a {@link SliceItem} are a set of strings which annotate
  * the content. The hints that are guaranteed to be understood by the system
  * are defined on {@link Slice}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @VersionedParcelize(allowSerialization = true, ignoreParcelables = true, isCustom = true)
 @RequiresApi(19)
+@Deprecated
 public final class SliceItem extends CustomVersionedParcelable {
 
     private static final String HINTS = "hints";
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceItemHolder.java b/slice/slice-core/src/main/java/androidx/slice/SliceItemHolder.java
index ab2ebba..4e822a3 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceItemHolder.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceItemHolder.java
@@ -50,6 +50,7 @@
 @VersionedParcelize(allowSerialization = true, ignoreParcelables = true,
         factory = SliceItemHolder.SliceItemPool.class)
 @RequiresApi(19)
+@Deprecated
 public class SliceItemHolder implements VersionedParcelable {
 
     public static final Object sSerializeLock = new Object();
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceManager.java b/slice/slice-core/src/main/java/androidx/slice/SliceManager.java
index 390f09a..eff8508 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceManager.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceManager.java
@@ -33,8 +33,13 @@
  * Class to handle interactions with {@link Slice}s.
  * <p>
  * The SliceViewManager manages permissions and pinned state for slices.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public abstract class SliceManager {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceManagerCompat.java b/slice/slice-core/src/main/java/androidx/slice/SliceManagerCompat.java
index 549d939..89b8ba8 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -32,6 +32,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 class SliceManagerCompat extends SliceManager {
 
     private final Context mContext;
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceManagerWrapper.java b/slice/slice-core/src/main/java/androidx/slice/SliceManagerWrapper.java
index d1bd08e..7d86844 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -36,6 +36,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(api = 28)
+@Deprecated
 class SliceManagerWrapper extends SliceManager {
 
     private final android.app.slice.SliceManager mManager;
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java b/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
index 9cd8ddc..399ba9f 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceProvider.java
@@ -130,7 +130,12 @@
  * </pre>
  *
  * @see Slice
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public abstract class SliceProvider extends ContentProvider implements
         CoreComponentFactory.CompatWrapped {
 
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceSpec.java b/slice/slice-core/src/main/java/androidx/slice/SliceSpec.java
index 13bacd9..5c7b61f 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceSpec.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceSpec.java
@@ -45,6 +45,7 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @VersionedParcelize(allowSerialization = true)
 @RequiresApi(19)
+@Deprecated
 public final class SliceSpec implements VersionedParcelable {
 
     @ParcelField(1)
diff --git a/slice/slice-core/src/main/java/androidx/slice/SliceSpecs.java b/slice/slice-core/src/main/java/androidx/slice/SliceSpecs.java
index 436d24b..e07a46f 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SliceSpecs.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SliceSpecs.java
@@ -24,6 +24,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SliceSpecs {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/SystemClock.java b/slice/slice-core/src/main/java/androidx/slice/SystemClock.java
index 888d3e4..c3f5ac3 100644
--- a/slice/slice-core/src/main/java/androidx/slice/SystemClock.java
+++ b/slice/slice-core/src/main/java/androidx/slice/SystemClock.java
@@ -25,6 +25,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SystemClock implements Clock {
     @Override
     public long currentTimeMillis() {
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/CompatPermissionManager.java b/slice/slice-core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
index dc700dd..b5ee3db 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
@@ -40,6 +40,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @RequiresApi(19)
+@Deprecated
 public class CompatPermissionManager {
     public static final String ALL_SUFFIX = "_all";
 
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/CompatPinnedList.java b/slice/slice-core/src/main/java/androidx/slice/compat/CompatPinnedList.java
index f9de140..67dea43 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/CompatPinnedList.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/CompatPinnedList.java
@@ -41,6 +41,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class CompatPinnedList {
 
     private static final String LAST_BOOT = "last_boot";
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java b/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
index 9d704cc..87711b7 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -43,6 +43,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SlicePermissionActivity extends AppCompatActivity implements OnClickListener,
         OnDismissListener {
 
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
index a03e977..24a73f3 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -67,6 +67,7 @@
  */
 @RestrictTo(Scope.LIBRARY_GROUP)
 @RequiresApi(19)
+@Deprecated
 public class SliceProviderCompat {
     public static final String PERMS_PREFIX = "slice_perms_";
     private static final String TAG = "SliceProviderCompat";
diff --git a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
index b24671f..69207a4 100644
--- a/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/slice/slice-core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -43,6 +43,7 @@
 /**
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
+@Deprecated
 public class SliceProviderWrapperContainer {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceAction.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceAction.java
index 6c47667..0df5949 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceAction.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceAction.java
@@ -26,8 +26,13 @@
 
 /**
  * Interface for a slice action, supports tappable icons, custom toggle icons, and default toggles.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public interface SliceAction {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
index 503cdbf..cd69d0e 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceActionImpl.java
@@ -64,6 +64,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SliceActionImpl implements SliceAction {
 
     // Either mAction or mActionItem must be non-null.
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceHints.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceHints.java
index e08441b..29e0353 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceHints.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceHints.java
@@ -31,6 +31,7 @@
  */
 @RestrictTo(LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SliceHints {
 
     /**
diff --git a/slice/slice-core/src/main/java/androidx/slice/core/SliceQuery.java b/slice/slice-core/src/main/java/androidx/slice/core/SliceQuery.java
index 3111e1b..f4d3121 100644
--- a/slice/slice-core/src/main/java/androidx/slice/core/SliceQuery.java
+++ b/slice/slice-core/src/main/java/androidx/slice/core/SliceQuery.java
@@ -40,6 +40,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class SliceQuery {
 
     /**
diff --git a/slice/slice-view/api/current.txt b/slice/slice-view/api/current.txt
index 91918d9..0b172e8 100644
--- a/slice/slice-view/api/current.txt
+++ b/slice/slice-view/api/current.txt
@@ -1,235 +1,235 @@
 // Signature format: 4.0
 package androidx.slice {
 
-  @RequiresApi(19) public class SliceMetadata {
-    method public static androidx.slice.SliceMetadata from(android.content.Context?, androidx.slice.Slice);
-    method public long getExpiry();
-    method public int getHeaderType();
-    method public android.os.Bundle getHostExtras();
-    method public android.app.PendingIntent? getInputRangeAction();
-    method public long getLastUpdatedTime();
-    method public int getLoadingState();
-    method public androidx.slice.core.SliceAction? getPrimaryAction();
-    method public androidx.core.util.Pair<java.lang.Integer!,java.lang.Integer!>? getRange();
-    method public int getRangeValue();
-    method public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
-    method public java.util.List<java.lang.String!>? getSliceKeywords();
-    method public CharSequence? getSubtitle();
-    method public CharSequence? getSummary();
-    method public CharSequence? getTitle();
-    method public java.util.List<androidx.slice.core.SliceAction!>! getToggles();
-    method public boolean hasLargeMode();
-    method public boolean isCachedSlice();
-    method public boolean isErrorSlice();
-    method public boolean isPermissionSlice();
-    method public boolean isSelection();
-    method public boolean sendInputRangeAction(int) throws android.app.PendingIntent.CanceledException;
-    method public boolean sendToggleAction(androidx.slice.core.SliceAction!, boolean) throws android.app.PendingIntent.CanceledException;
-    field public static final int LOADED_ALL = 2; // 0x2
-    field public static final int LOADED_NONE = 0; // 0x0
-    field public static final int LOADED_PARTIAL = 1; // 0x1
+  @Deprecated @RequiresApi(19) public class SliceMetadata {
+    method @Deprecated public static androidx.slice.SliceMetadata from(android.content.Context?, androidx.slice.Slice);
+    method @Deprecated public long getExpiry();
+    method @Deprecated public int getHeaderType();
+    method @Deprecated public android.os.Bundle getHostExtras();
+    method @Deprecated public android.app.PendingIntent? getInputRangeAction();
+    method @Deprecated public long getLastUpdatedTime();
+    method @Deprecated public int getLoadingState();
+    method @Deprecated public androidx.slice.core.SliceAction? getPrimaryAction();
+    method @Deprecated public androidx.core.util.Pair<java.lang.Integer!,java.lang.Integer!>? getRange();
+    method @Deprecated public int getRangeValue();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
+    method @Deprecated public java.util.List<java.lang.String!>? getSliceKeywords();
+    method @Deprecated public CharSequence? getSubtitle();
+    method @Deprecated public CharSequence? getSummary();
+    method @Deprecated public CharSequence? getTitle();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>! getToggles();
+    method @Deprecated public boolean hasLargeMode();
+    method @Deprecated public boolean isCachedSlice();
+    method @Deprecated public boolean isErrorSlice();
+    method @Deprecated public boolean isPermissionSlice();
+    method @Deprecated public boolean isSelection();
+    method @Deprecated public boolean sendInputRangeAction(int) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated public boolean sendToggleAction(androidx.slice.core.SliceAction!, boolean) throws android.app.PendingIntent.CanceledException;
+    field @Deprecated public static final int LOADED_ALL = 2; // 0x2
+    field @Deprecated public static final int LOADED_NONE = 0; // 0x0
+    field @Deprecated public static final int LOADED_PARTIAL = 1; // 0x1
   }
 
-  @RequiresApi(19) public class SliceStructure {
-    ctor public SliceStructure(androidx.slice.Slice!);
+  @Deprecated @RequiresApi(19) public class SliceStructure {
+    ctor @Deprecated public SliceStructure(androidx.slice.Slice!);
   }
 
-  @RequiresApi(19) public class SliceUtils {
-    method public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
-    method public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, androidx.slice.SliceUtils.SerializeOptions) throws java.lang.IllegalArgumentException;
-    method public static androidx.slice.Slice stripSlice(androidx.slice.Slice, int, boolean);
+  @Deprecated @RequiresApi(19) public class SliceUtils {
+    method @Deprecated public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
+    method @Deprecated public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, androidx.slice.SliceUtils.SerializeOptions) throws java.lang.IllegalArgumentException;
+    method @Deprecated public static androidx.slice.Slice stripSlice(androidx.slice.Slice, int, boolean);
   }
 
-  public static class SliceUtils.SerializeOptions {
-    ctor public SliceUtils.SerializeOptions();
-    method public androidx.slice.SliceUtils.SerializeOptions! setActionMode(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setImageConversionFormat(android.graphics.Bitmap.CompressFormat!, int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setImageMode(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setMaxImageHeight(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setMaxImageWidth(int);
-    field public static final int MODE_CONVERT = 2; // 0x2
-    field public static final int MODE_REMOVE = 1; // 0x1
-    field public static final int MODE_THROW = 0; // 0x0
+  @Deprecated public static class SliceUtils.SerializeOptions {
+    ctor @Deprecated public SliceUtils.SerializeOptions();
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setActionMode(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setImageConversionFormat(android.graphics.Bitmap.CompressFormat!, int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setImageMode(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setMaxImageHeight(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setMaxImageWidth(int);
+    field @Deprecated public static final int MODE_CONVERT = 2; // 0x2
+    field @Deprecated public static final int MODE_REMOVE = 1; // 0x1
+    field @Deprecated public static final int MODE_THROW = 0; // 0x0
   }
 
-  public static interface SliceUtils.SliceActionListener {
-    method public void onSliceAction(android.net.Uri!, android.content.Context!, android.content.Intent!);
+  @Deprecated public static interface SliceUtils.SliceActionListener {
+    method @Deprecated public void onSliceAction(android.net.Uri!, android.content.Context!, android.content.Intent!);
   }
 
-  public static class SliceUtils.SliceParseException extends java.lang.Exception {
+  @Deprecated public static class SliceUtils.SliceParseException extends java.lang.Exception {
   }
 
-  @RequiresApi(19) public abstract class SliceViewManager {
-    method public abstract androidx.slice.Slice? bindSlice(android.content.Intent);
-    method public abstract androidx.slice.Slice? bindSlice(android.net.Uri);
-    method public static androidx.slice.SliceViewManager getInstance(android.content.Context);
-    method @WorkerThread public abstract java.util.Collection<android.net.Uri!> getSliceDescendants(android.net.Uri);
-    method public abstract android.net.Uri? mapIntentToUri(android.content.Intent);
-    method public abstract void pinSlice(android.net.Uri);
-    method public abstract void registerSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
-    method public abstract void registerSliceCallback(android.net.Uri, java.util.concurrent.Executor, androidx.slice.SliceViewManager.SliceCallback);
-    method public abstract void unpinSlice(android.net.Uri);
-    method public abstract void unregisterSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
+  @Deprecated @RequiresApi(19) public abstract class SliceViewManager {
+    method @Deprecated public abstract androidx.slice.Slice? bindSlice(android.content.Intent);
+    method @Deprecated public abstract androidx.slice.Slice? bindSlice(android.net.Uri);
+    method @Deprecated public static androidx.slice.SliceViewManager getInstance(android.content.Context);
+    method @Deprecated @WorkerThread public abstract java.util.Collection<android.net.Uri!> getSliceDescendants(android.net.Uri);
+    method @Deprecated public abstract android.net.Uri? mapIntentToUri(android.content.Intent);
+    method @Deprecated public abstract void pinSlice(android.net.Uri);
+    method @Deprecated public abstract void registerSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
+    method @Deprecated public abstract void registerSliceCallback(android.net.Uri, java.util.concurrent.Executor, androidx.slice.SliceViewManager.SliceCallback);
+    method @Deprecated public abstract void unpinSlice(android.net.Uri);
+    method @Deprecated public abstract void unregisterSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
   }
 
-  public static interface SliceViewManager.SliceCallback {
-    method public void onSliceUpdated(androidx.slice.Slice?);
+  @Deprecated public static interface SliceViewManager.SliceCallback {
+    method @Deprecated public void onSliceUpdated(androidx.slice.Slice?);
   }
 
 }
 
 package androidx.slice.widget {
 
-  @RequiresApi(19) public class EventInfo {
-    ctor public EventInfo(int, int, int, int);
-    method public void setPosition(int, int, int);
-    field public static final int ACTION_TYPE_BUTTON = 1; // 0x1
-    field public static final int ACTION_TYPE_CONTENT = 3; // 0x3
-    field public static final int ACTION_TYPE_SEE_MORE = 4; // 0x4
-    field public static final int ACTION_TYPE_SELECTION = 5; // 0x5
-    field public static final int ACTION_TYPE_SLIDER = 2; // 0x2
-    field public static final int ACTION_TYPE_TOGGLE = 0; // 0x0
-    field public static final int POSITION_CELL = 2; // 0x2
-    field public static final int POSITION_END = 1; // 0x1
-    field public static final int POSITION_START = 0; // 0x0
-    field public static final int ROW_TYPE_GRID = 1; // 0x1
-    field public static final int ROW_TYPE_LIST = 0; // 0x0
-    field public static final int ROW_TYPE_MESSAGING = 2; // 0x2
-    field public static final int ROW_TYPE_PROGRESS = 5; // 0x5
-    field public static final int ROW_TYPE_SELECTION = 6; // 0x6
-    field public static final int ROW_TYPE_SHORTCUT = -1; // 0xffffffff
-    field public static final int ROW_TYPE_SLIDER = 4; // 0x4
-    field public static final int ROW_TYPE_TOGGLE = 3; // 0x3
-    field public static final int STATE_OFF = 0; // 0x0
-    field public static final int STATE_ON = 1; // 0x1
-    field public int actionCount;
-    field public int actionIndex;
-    field public int actionPosition;
-    field public int actionType;
-    field public int rowIndex;
-    field public int rowTemplateType;
-    field public int sliceMode;
-    field public int state;
+  @Deprecated @RequiresApi(19) public class EventInfo {
+    ctor @Deprecated public EventInfo(int, int, int, int);
+    method @Deprecated public void setPosition(int, int, int);
+    field @Deprecated public static final int ACTION_TYPE_BUTTON = 1; // 0x1
+    field @Deprecated public static final int ACTION_TYPE_CONTENT = 3; // 0x3
+    field @Deprecated public static final int ACTION_TYPE_SEE_MORE = 4; // 0x4
+    field @Deprecated public static final int ACTION_TYPE_SELECTION = 5; // 0x5
+    field @Deprecated public static final int ACTION_TYPE_SLIDER = 2; // 0x2
+    field @Deprecated public static final int ACTION_TYPE_TOGGLE = 0; // 0x0
+    field @Deprecated public static final int POSITION_CELL = 2; // 0x2
+    field @Deprecated public static final int POSITION_END = 1; // 0x1
+    field @Deprecated public static final int POSITION_START = 0; // 0x0
+    field @Deprecated public static final int ROW_TYPE_GRID = 1; // 0x1
+    field @Deprecated public static final int ROW_TYPE_LIST = 0; // 0x0
+    field @Deprecated public static final int ROW_TYPE_MESSAGING = 2; // 0x2
+    field @Deprecated public static final int ROW_TYPE_PROGRESS = 5; // 0x5
+    field @Deprecated public static final int ROW_TYPE_SELECTION = 6; // 0x6
+    field @Deprecated public static final int ROW_TYPE_SHORTCUT = -1; // 0xffffffff
+    field @Deprecated public static final int ROW_TYPE_SLIDER = 4; // 0x4
+    field @Deprecated public static final int ROW_TYPE_TOGGLE = 3; // 0x3
+    field @Deprecated public static final int STATE_OFF = 0; // 0x0
+    field @Deprecated public static final int STATE_ON = 1; // 0x1
+    field @Deprecated public int actionCount;
+    field @Deprecated public int actionIndex;
+    field @Deprecated public int actionPosition;
+    field @Deprecated public int actionType;
+    field @Deprecated public int rowIndex;
+    field @Deprecated public int rowTemplateType;
+    field @Deprecated public int sliceMode;
+    field @Deprecated public int state;
   }
 
-  @RequiresApi(19) public class GridContent extends androidx.slice.widget.SliceContent {
-    method public android.graphics.Point getFirstImageSize(android.content.Context);
-    method public boolean isValid();
+  @Deprecated @RequiresApi(19) public class GridContent extends androidx.slice.widget.SliceContent {
+    method @Deprecated public android.graphics.Point getFirstImageSize(android.content.Context);
+    method @Deprecated public boolean isValid();
   }
 
-  @RequiresApi(19) public class GridRowView extends androidx.slice.widget.SliceChildView implements android.view.View.OnClickListener android.view.View.OnTouchListener {
-    ctor public GridRowView(android.content.Context);
-    ctor public GridRowView(android.content.Context, android.util.AttributeSet?);
-    method protected boolean addImageItem(androidx.slice.SliceItem, androidx.slice.SliceItem?, int, android.view.ViewGroup, boolean);
-    method protected int getExtraBottomPadding();
-    method protected int getExtraTopPadding();
-    method protected int getMaxCells();
-    method protected int getTitleTextLayout();
+  @Deprecated @RequiresApi(19) public class GridRowView extends androidx.slice.widget.SliceChildView implements android.view.View.OnClickListener android.view.View.OnTouchListener {
+    ctor @Deprecated public GridRowView(android.content.Context);
+    ctor @Deprecated public GridRowView(android.content.Context, android.util.AttributeSet?);
+    method @Deprecated protected boolean addImageItem(androidx.slice.SliceItem, androidx.slice.SliceItem?, int, android.view.ViewGroup, boolean);
+    method @Deprecated protected int getExtraBottomPadding();
+    method @Deprecated protected int getExtraTopPadding();
+    method @Deprecated protected int getMaxCells();
+    method @Deprecated protected int getTitleTextLayout();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void onClick(android.view.View);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public boolean onTouch(android.view.View, android.view.MotionEvent);
-    method protected void populateViews();
-    method public void resetView();
-    method protected boolean scheduleMaxCellsUpdate();
+    method @Deprecated protected void populateViews();
+    method @Deprecated public void resetView();
+    method @Deprecated protected boolean scheduleMaxCellsUpdate();
   }
 
-  public interface RowStyleFactory {
-    method @StyleRes public int getRowStyleRes(androidx.slice.SliceItem);
+  @Deprecated public interface RowStyleFactory {
+    method @Deprecated @StyleRes public int getRowStyleRes(androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class RowView extends androidx.slice.widget.SliceChildView implements android.widget.AdapterView.OnItemSelectedListener android.view.View.OnClickListener {
-    ctor public RowView(android.content.Context);
-    method protected java.util.List<java.lang.String!> getEndItemKeys();
-    method protected androidx.slice.SliceItem? getPrimaryActionItem();
-    method protected String? getPrimaryActionKey();
-    method public void onClick(android.view.View);
-    method public void onItemSelected(android.widget.AdapterView<?>, android.view.View, int, long);
-    method public void onNothingSelected(android.widget.AdapterView<?>);
+  @Deprecated @RequiresApi(19) public class RowView extends androidx.slice.widget.SliceChildView implements android.widget.AdapterView.OnItemSelectedListener android.view.View.OnClickListener {
+    ctor @Deprecated public RowView(android.content.Context);
+    method @Deprecated protected java.util.List<java.lang.String!> getEndItemKeys();
+    method @Deprecated protected androidx.slice.SliceItem? getPrimaryActionItem();
+    method @Deprecated protected String? getPrimaryActionKey();
+    method @Deprecated public void onClick(android.view.View);
+    method @Deprecated public void onItemSelected(android.widget.AdapterView<?>, android.view.View, int, long);
+    method @Deprecated public void onNothingSelected(android.widget.AdapterView<?>);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
   }
 
-  @RequiresApi(19) public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
-    ctor public SliceAdapter(android.content.Context);
-    method public androidx.slice.widget.GridRowView getGridRowView();
-    method public int getItemCount();
-    method public androidx.slice.widget.RowView getRowView();
+  @Deprecated @RequiresApi(19) public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
+    ctor @Deprecated public SliceAdapter(android.content.Context);
+    method @Deprecated public androidx.slice.widget.GridRowView getGridRowView();
+    method @Deprecated public int getItemCount();
+    method @Deprecated public androidx.slice.widget.RowView getRowView();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void onBindViewHolder(androidx.slice.widget.SliceAdapter.SliceViewHolder, int);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public androidx.slice.widget.SliceAdapter.SliceViewHolder onCreateViewHolder(android.view.ViewGroup, int);
   }
 
-  @RequiresApi(19) public abstract class SliceChildView extends android.widget.FrameLayout {
-    ctor public SliceChildView(android.content.Context);
-    ctor public SliceChildView(android.content.Context, android.util.AttributeSet?);
-    method public abstract void resetView();
-    method public void setSliceItem(androidx.slice.widget.SliceContent?, boolean, int, int, androidx.slice.widget.SliceView.OnSliceActionListener?);
+  @Deprecated @RequiresApi(19) public abstract class SliceChildView extends android.widget.FrameLayout {
+    ctor @Deprecated public SliceChildView(android.content.Context);
+    ctor @Deprecated public SliceChildView(android.content.Context, android.util.AttributeSet?);
+    method @Deprecated public abstract void resetView();
+    method @Deprecated public void setSliceItem(androidx.slice.widget.SliceContent?, boolean, int, int, androidx.slice.widget.SliceView.OnSliceActionListener?);
   }
 
-  @RequiresApi(19) public class SliceContent {
-    ctor public SliceContent(androidx.slice.Slice?);
+  @Deprecated @RequiresApi(19) public class SliceContent {
+    ctor @Deprecated public SliceContent(androidx.slice.Slice?);
   }
 
-  @RequiresApi(19) public final class SliceLiveData {
-    method public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromCachedSlice(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent, androidx.slice.widget.SliceLiveData.OnErrorListener?);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromStream(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
+  @Deprecated @RequiresApi(19) public final class SliceLiveData {
+    method @Deprecated public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromCachedSlice(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent, androidx.slice.widget.SliceLiveData.OnErrorListener?);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromStream(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
   }
 
-  public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
-    method public void goLive();
-    method public void parseStream();
+  @Deprecated public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
+    method @Deprecated public void goLive();
+    method @Deprecated public void parseStream();
   }
 
-  public static interface SliceLiveData.OnErrorListener {
-    method public void onSliceError(@androidx.slice.widget.SliceLiveData.OnErrorListener.ErrorType int, Throwable?);
-    field public static final int ERROR_INVALID_INPUT = 3; // 0x3
-    field public static final int ERROR_SLICE_NO_LONGER_PRESENT = 2; // 0x2
-    field public static final int ERROR_STRUCTURE_CHANGED = 1; // 0x1
-    field public static final int ERROR_UNKNOWN = 0; // 0x0
+  @Deprecated public static interface SliceLiveData.OnErrorListener {
+    method @Deprecated public void onSliceError(@androidx.slice.widget.SliceLiveData.OnErrorListener.ErrorType int, Throwable?);
+    field @Deprecated public static final int ERROR_INVALID_INPUT = 3; // 0x3
+    field @Deprecated public static final int ERROR_SLICE_NO_LONGER_PRESENT = 2; // 0x2
+    field @Deprecated public static final int ERROR_STRUCTURE_CHANGED = 1; // 0x1
+    field @Deprecated public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
-  @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
+  @Deprecated @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
   }
 
-  @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
-    ctor public SliceView(android.content.Context!);
-    ctor public SliceView(android.content.Context!, android.util.AttributeSet?);
-    ctor public SliceView(android.content.Context!, android.util.AttributeSet?, int);
-    ctor @RequiresApi(21) public SliceView(android.content.Context!, android.util.AttributeSet!, int, int);
-    method protected void configureViewPolicy(int);
-    method public int getHiddenItemCount();
-    method public int getMode();
-    method public androidx.slice.Slice? getSlice();
-    method public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
-    method public boolean isScrollable();
-    method public void onChanged(androidx.slice.Slice?);
-    method public void onClick(android.view.View!);
-    method public void setAccentColor(@ColorInt int);
-    method public void setCurrentView(androidx.slice.widget.SliceChildView);
-    method public void setMode(int);
-    method public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener?);
-    method public void setRowStyleFactory(androidx.slice.widget.RowStyleFactory?);
-    method public void setScrollable(boolean);
-    method public void setShowActionDividers(boolean);
-    method public void setShowHeaderDivider(boolean);
-    method public void setShowTitleItems(boolean);
-    method public void setSlice(androidx.slice.Slice?);
-    method public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
-    field public static final int MODE_LARGE = 2; // 0x2
-    field public static final int MODE_SHORTCUT = 3; // 0x3
-    field public static final int MODE_SMALL = 1; // 0x1
+  @Deprecated @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
+    ctor @Deprecated public SliceView(android.content.Context!);
+    ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?);
+    ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?, int);
+    ctor @Deprecated @RequiresApi(21) public SliceView(android.content.Context!, android.util.AttributeSet!, int, int);
+    method @Deprecated protected void configureViewPolicy(int);
+    method @Deprecated public int getHiddenItemCount();
+    method @Deprecated public int getMode();
+    method @Deprecated public androidx.slice.Slice? getSlice();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
+    method @Deprecated public boolean isScrollable();
+    method @Deprecated public void onChanged(androidx.slice.Slice?);
+    method @Deprecated public void onClick(android.view.View!);
+    method @Deprecated public void setAccentColor(@ColorInt int);
+    method @Deprecated public void setCurrentView(androidx.slice.widget.SliceChildView);
+    method @Deprecated public void setMode(int);
+    method @Deprecated public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener?);
+    method @Deprecated public void setRowStyleFactory(androidx.slice.widget.RowStyleFactory?);
+    method @Deprecated public void setScrollable(boolean);
+    method @Deprecated public void setShowActionDividers(boolean);
+    method @Deprecated public void setShowHeaderDivider(boolean);
+    method @Deprecated public void setShowTitleItems(boolean);
+    method @Deprecated public void setSlice(androidx.slice.Slice?);
+    method @Deprecated public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
+    field @Deprecated public static final int MODE_LARGE = 2; // 0x2
+    field @Deprecated public static final int MODE_SHORTCUT = 3; // 0x3
+    field @Deprecated public static final int MODE_SMALL = 1; // 0x1
   }
 
-  public static interface SliceView.OnSliceActionListener {
-    method public void onSliceAction(androidx.slice.widget.EventInfo, androidx.slice.SliceItem);
+  @Deprecated public static interface SliceView.OnSliceActionListener {
+    method @Deprecated public void onSliceAction(androidx.slice.widget.EventInfo, androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class TemplateView extends androidx.slice.widget.SliceChildView {
-    ctor public TemplateView(android.content.Context);
-    method public void onAttachedToWindow();
+  @Deprecated @RequiresApi(19) public class TemplateView extends androidx.slice.widget.SliceChildView {
+    ctor @Deprecated public TemplateView(android.content.Context);
+    method @Deprecated public void onAttachedToWindow();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
-    method public void setAdapter(androidx.slice.widget.SliceAdapter);
+    method @Deprecated public void setAdapter(androidx.slice.widget.SliceAdapter);
   }
 
 }
diff --git a/slice/slice-view/api/removed_current.txt b/slice/slice-view/api/removed_current.txt
index 83b9996..c72daba 100644
--- a/slice/slice-view/api/removed_current.txt
+++ b/slice/slice-view/api/removed_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.slice.widget {
 
-  @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
+  @Deprecated @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
     method @Deprecated public void showActionDividers(boolean);
     method @Deprecated public void showHeaderDivider(boolean);
     method @Deprecated public void showTitleItems(boolean);
diff --git a/slice/slice-view/api/restricted_current.txt b/slice/slice-view/api/restricted_current.txt
index 9f3c43a..068889b 100644
--- a/slice/slice-view/api/restricted_current.txt
+++ b/slice/slice-view/api/restricted_current.txt
@@ -1,294 +1,294 @@
 // Signature format: 4.0
 package androidx.slice {
 
-  @RequiresApi(19) public class SliceMetadata {
-    method public static androidx.slice.SliceMetadata from(android.content.Context?, androidx.slice.Slice);
-    method public long getExpiry();
-    method public int getHeaderType();
-    method public android.os.Bundle getHostExtras();
-    method public android.app.PendingIntent? getInputRangeAction();
-    method public long getLastUpdatedTime();
-    method public int getLoadingState();
-    method public androidx.slice.core.SliceAction? getPrimaryAction();
-    method public androidx.core.util.Pair<java.lang.Integer!,java.lang.Integer!>? getRange();
-    method public int getRangeValue();
-    method public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
-    method public java.util.List<java.lang.String!>? getSliceKeywords();
-    method public CharSequence? getSubtitle();
-    method public CharSequence? getSummary();
-    method public CharSequence? getTitle();
-    method public java.util.List<androidx.slice.core.SliceAction!>! getToggles();
-    method public boolean hasLargeMode();
-    method public boolean isCachedSlice();
-    method public boolean isErrorSlice();
-    method public boolean isPermissionSlice();
-    method public boolean isSelection();
-    method public boolean sendInputRangeAction(int) throws android.app.PendingIntent.CanceledException;
-    method public boolean sendToggleAction(androidx.slice.core.SliceAction!, boolean) throws android.app.PendingIntent.CanceledException;
-    field public static final int LOADED_ALL = 2; // 0x2
-    field public static final int LOADED_NONE = 0; // 0x0
-    field public static final int LOADED_PARTIAL = 1; // 0x1
+  @Deprecated @RequiresApi(19) public class SliceMetadata {
+    method @Deprecated public static androidx.slice.SliceMetadata from(android.content.Context?, androidx.slice.Slice);
+    method @Deprecated public long getExpiry();
+    method @Deprecated public int getHeaderType();
+    method @Deprecated public android.os.Bundle getHostExtras();
+    method @Deprecated public android.app.PendingIntent? getInputRangeAction();
+    method @Deprecated public long getLastUpdatedTime();
+    method @Deprecated public int getLoadingState();
+    method @Deprecated public androidx.slice.core.SliceAction? getPrimaryAction();
+    method @Deprecated public androidx.core.util.Pair<java.lang.Integer!,java.lang.Integer!>? getRange();
+    method @Deprecated public int getRangeValue();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
+    method @Deprecated public java.util.List<java.lang.String!>? getSliceKeywords();
+    method @Deprecated public CharSequence? getSubtitle();
+    method @Deprecated public CharSequence? getSummary();
+    method @Deprecated public CharSequence? getTitle();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>! getToggles();
+    method @Deprecated public boolean hasLargeMode();
+    method @Deprecated public boolean isCachedSlice();
+    method @Deprecated public boolean isErrorSlice();
+    method @Deprecated public boolean isPermissionSlice();
+    method @Deprecated public boolean isSelection();
+    method @Deprecated public boolean sendInputRangeAction(int) throws android.app.PendingIntent.CanceledException;
+    method @Deprecated public boolean sendToggleAction(androidx.slice.core.SliceAction!, boolean) throws android.app.PendingIntent.CanceledException;
+    field @Deprecated public static final int LOADED_ALL = 2; // 0x2
+    field @Deprecated public static final int LOADED_NONE = 0; // 0x0
+    field @Deprecated public static final int LOADED_PARTIAL = 1; // 0x1
   }
 
-  @RequiresApi(19) public class SliceStructure {
-    ctor public SliceStructure(androidx.slice.Slice!);
+  @Deprecated @RequiresApi(19) public class SliceStructure {
+    ctor @Deprecated public SliceStructure(androidx.slice.Slice!);
   }
 
-  @RequiresApi(19) public class SliceUtils {
-    method public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
-    method public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, androidx.slice.SliceUtils.SerializeOptions) throws java.lang.IllegalArgumentException;
-    method public static androidx.slice.Slice stripSlice(androidx.slice.Slice, int, boolean);
+  @Deprecated @RequiresApi(19) public class SliceUtils {
+    method @Deprecated public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
+    method @Deprecated public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, androidx.slice.SliceUtils.SerializeOptions) throws java.lang.IllegalArgumentException;
+    method @Deprecated public static androidx.slice.Slice stripSlice(androidx.slice.Slice, int, boolean);
   }
 
-  public static class SliceUtils.SerializeOptions {
-    ctor public SliceUtils.SerializeOptions();
-    method public androidx.slice.SliceUtils.SerializeOptions! setActionMode(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setImageConversionFormat(android.graphics.Bitmap.CompressFormat!, int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setImageMode(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setMaxImageHeight(int);
-    method public androidx.slice.SliceUtils.SerializeOptions! setMaxImageWidth(int);
-    field public static final int MODE_CONVERT = 2; // 0x2
-    field public static final int MODE_REMOVE = 1; // 0x1
-    field public static final int MODE_THROW = 0; // 0x0
+  @Deprecated public static class SliceUtils.SerializeOptions {
+    ctor @Deprecated public SliceUtils.SerializeOptions();
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setActionMode(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setImageConversionFormat(android.graphics.Bitmap.CompressFormat!, int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setImageMode(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setMaxImageHeight(int);
+    method @Deprecated public androidx.slice.SliceUtils.SerializeOptions! setMaxImageWidth(int);
+    field @Deprecated public static final int MODE_CONVERT = 2; // 0x2
+    field @Deprecated public static final int MODE_REMOVE = 1; // 0x1
+    field @Deprecated public static final int MODE_THROW = 0; // 0x0
   }
 
-  public static interface SliceUtils.SliceActionListener {
-    method public void onSliceAction(android.net.Uri!, android.content.Context!, android.content.Intent!);
+  @Deprecated public static interface SliceUtils.SliceActionListener {
+    method @Deprecated public void onSliceAction(android.net.Uri!, android.content.Context!, android.content.Intent!);
   }
 
-  public static class SliceUtils.SliceParseException extends java.lang.Exception {
+  @Deprecated public static class SliceUtils.SliceParseException extends java.lang.Exception {
   }
 
-  @RequiresApi(19) public abstract class SliceViewManager {
-    method public abstract androidx.slice.Slice? bindSlice(android.content.Intent);
-    method public abstract androidx.slice.Slice? bindSlice(android.net.Uri);
-    method public static androidx.slice.SliceViewManager getInstance(android.content.Context);
-    method @WorkerThread public abstract java.util.Collection<android.net.Uri!> getSliceDescendants(android.net.Uri);
-    method public abstract android.net.Uri? mapIntentToUri(android.content.Intent);
-    method public abstract void pinSlice(android.net.Uri);
-    method public abstract void registerSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
-    method public abstract void registerSliceCallback(android.net.Uri, java.util.concurrent.Executor, androidx.slice.SliceViewManager.SliceCallback);
-    method public abstract void unpinSlice(android.net.Uri);
-    method public abstract void unregisterSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
+  @Deprecated @RequiresApi(19) public abstract class SliceViewManager {
+    method @Deprecated public abstract androidx.slice.Slice? bindSlice(android.content.Intent);
+    method @Deprecated public abstract androidx.slice.Slice? bindSlice(android.net.Uri);
+    method @Deprecated public static androidx.slice.SliceViewManager getInstance(android.content.Context);
+    method @Deprecated @WorkerThread public abstract java.util.Collection<android.net.Uri!> getSliceDescendants(android.net.Uri);
+    method @Deprecated public abstract android.net.Uri? mapIntentToUri(android.content.Intent);
+    method @Deprecated public abstract void pinSlice(android.net.Uri);
+    method @Deprecated public abstract void registerSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
+    method @Deprecated public abstract void registerSliceCallback(android.net.Uri, java.util.concurrent.Executor, androidx.slice.SliceViewManager.SliceCallback);
+    method @Deprecated public abstract void unpinSlice(android.net.Uri);
+    method @Deprecated public abstract void unregisterSliceCallback(android.net.Uri, androidx.slice.SliceViewManager.SliceCallback);
   }
 
-  public static interface SliceViewManager.SliceCallback {
-    method public void onSliceUpdated(androidx.slice.Slice?);
+  @Deprecated public static interface SliceViewManager.SliceCallback {
+    method @Deprecated public void onSliceUpdated(androidx.slice.Slice?);
   }
 
 }
 
 package androidx.slice.widget {
 
-  @RequiresApi(19) public class EventInfo {
-    ctor public EventInfo(int, int, int, int);
-    method public void setPosition(int, int, int);
-    field public static final int ACTION_TYPE_BUTTON = 1; // 0x1
-    field public static final int ACTION_TYPE_CONTENT = 3; // 0x3
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ACTION_TYPE_DATE_PICK = 6; // 0x6
-    field public static final int ACTION_TYPE_SEE_MORE = 4; // 0x4
-    field public static final int ACTION_TYPE_SELECTION = 5; // 0x5
-    field public static final int ACTION_TYPE_SLIDER = 2; // 0x2
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ACTION_TYPE_TIME_PICK = 7; // 0x7
-    field public static final int ACTION_TYPE_TOGGLE = 0; // 0x0
-    field public static final int POSITION_CELL = 2; // 0x2
-    field public static final int POSITION_END = 1; // 0x1
-    field public static final int POSITION_START = 0; // 0x0
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ROW_TYPE_DATE_PICK = 7; // 0x7
-    field public static final int ROW_TYPE_GRID = 1; // 0x1
-    field public static final int ROW_TYPE_LIST = 0; // 0x0
-    field public static final int ROW_TYPE_MESSAGING = 2; // 0x2
-    field public static final int ROW_TYPE_PROGRESS = 5; // 0x5
-    field public static final int ROW_TYPE_SELECTION = 6; // 0x6
-    field public static final int ROW_TYPE_SHORTCUT = -1; // 0xffffffff
-    field public static final int ROW_TYPE_SLIDER = 4; // 0x4
-    field @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ROW_TYPE_TIME_PICK = 8; // 0x8
-    field public static final int ROW_TYPE_TOGGLE = 3; // 0x3
-    field public static final int STATE_OFF = 0; // 0x0
-    field public static final int STATE_ON = 1; // 0x1
-    field public int actionCount;
-    field public int actionIndex;
-    field public int actionPosition;
-    field public int actionType;
-    field public int rowIndex;
-    field public int rowTemplateType;
-    field public int sliceMode;
-    field public int state;
+  @Deprecated @RequiresApi(19) public class EventInfo {
+    ctor @Deprecated public EventInfo(int, int, int, int);
+    method @Deprecated public void setPosition(int, int, int);
+    field @Deprecated public static final int ACTION_TYPE_BUTTON = 1; // 0x1
+    field @Deprecated public static final int ACTION_TYPE_CONTENT = 3; // 0x3
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ACTION_TYPE_DATE_PICK = 6; // 0x6
+    field @Deprecated public static final int ACTION_TYPE_SEE_MORE = 4; // 0x4
+    field @Deprecated public static final int ACTION_TYPE_SELECTION = 5; // 0x5
+    field @Deprecated public static final int ACTION_TYPE_SLIDER = 2; // 0x2
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ACTION_TYPE_TIME_PICK = 7; // 0x7
+    field @Deprecated public static final int ACTION_TYPE_TOGGLE = 0; // 0x0
+    field @Deprecated public static final int POSITION_CELL = 2; // 0x2
+    field @Deprecated public static final int POSITION_END = 1; // 0x1
+    field @Deprecated public static final int POSITION_START = 0; // 0x0
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ROW_TYPE_DATE_PICK = 7; // 0x7
+    field @Deprecated public static final int ROW_TYPE_GRID = 1; // 0x1
+    field @Deprecated public static final int ROW_TYPE_LIST = 0; // 0x0
+    field @Deprecated public static final int ROW_TYPE_MESSAGING = 2; // 0x2
+    field @Deprecated public static final int ROW_TYPE_PROGRESS = 5; // 0x5
+    field @Deprecated public static final int ROW_TYPE_SELECTION = 6; // 0x6
+    field @Deprecated public static final int ROW_TYPE_SHORTCUT = -1; // 0xffffffff
+    field @Deprecated public static final int ROW_TYPE_SLIDER = 4; // 0x4
+    field @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static final int ROW_TYPE_TIME_PICK = 8; // 0x8
+    field @Deprecated public static final int ROW_TYPE_TOGGLE = 3; // 0x3
+    field @Deprecated public static final int STATE_OFF = 0; // 0x0
+    field @Deprecated public static final int STATE_ON = 1; // 0x1
+    field @Deprecated public int actionCount;
+    field @Deprecated public int actionIndex;
+    field @Deprecated public int actionPosition;
+    field @Deprecated public int actionType;
+    field @Deprecated public int rowIndex;
+    field @Deprecated public int rowTemplateType;
+    field @Deprecated public int sliceMode;
+    field @Deprecated public int state;
   }
 
-  @RequiresApi(19) public class GridContent extends androidx.slice.widget.SliceContent {
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public GridContent(androidx.slice.SliceItem, int);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getContentIntent();
-    method public android.graphics.Point getFirstImageSize(android.content.Context);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public java.util.ArrayList<androidx.slice.widget.GridContent.CellContent!> getGridContent();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean getIsLastIndex();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getLargestImageMode();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getMaxCellLineCount();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getSeeMoreItem();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public CharSequence? getTitle();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean hasImage();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean isAllImages();
-    method public boolean isValid();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setIsLastIndex(boolean);
+  @Deprecated @RequiresApi(19) public class GridContent extends androidx.slice.widget.SliceContent {
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public GridContent(androidx.slice.SliceItem, int);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getContentIntent();
+    method @Deprecated public android.graphics.Point getFirstImageSize(android.content.Context);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public java.util.ArrayList<androidx.slice.widget.GridContent.CellContent!> getGridContent();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean getIsLastIndex();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getLargestImageMode();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public int getMaxCellLineCount();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.slice.SliceItem? getSeeMoreItem();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public CharSequence? getTitle();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean hasImage();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public boolean isAllImages();
+    method @Deprecated public boolean isValid();
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setIsLastIndex(boolean);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class GridContent.CellContent {
-    ctor public GridContent.CellContent(androidx.slice.SliceItem);
-    method public java.util.ArrayList<androidx.slice.SliceItem!> getCellItems();
-    method public CharSequence? getContentDescription();
-    method public androidx.slice.SliceItem? getContentIntent();
-    method public androidx.core.graphics.drawable.IconCompat? getImageIcon();
-    method public int getImageMode();
-    method public androidx.slice.SliceItem? getOverlayItem();
-    method public androidx.slice.SliceItem? getPicker();
-    method public int getTextCount();
-    method public androidx.slice.SliceItem? getTitleItem();
-    method public androidx.slice.SliceItem? getToggleItem();
-    method public boolean hasImage();
-    method public boolean isImageOnly();
-    method public boolean isValid();
-    method public boolean populate(androidx.slice.SliceItem);
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static class GridContent.CellContent {
+    ctor @Deprecated public GridContent.CellContent(androidx.slice.SliceItem);
+    method @Deprecated public java.util.ArrayList<androidx.slice.SliceItem!> getCellItems();
+    method @Deprecated public CharSequence? getContentDescription();
+    method @Deprecated public androidx.slice.SliceItem? getContentIntent();
+    method @Deprecated public androidx.core.graphics.drawable.IconCompat? getImageIcon();
+    method @Deprecated public int getImageMode();
+    method @Deprecated public androidx.slice.SliceItem? getOverlayItem();
+    method @Deprecated public androidx.slice.SliceItem? getPicker();
+    method @Deprecated public int getTextCount();
+    method @Deprecated public androidx.slice.SliceItem? getTitleItem();
+    method @Deprecated public androidx.slice.SliceItem? getToggleItem();
+    method @Deprecated public boolean hasImage();
+    method @Deprecated public boolean isImageOnly();
+    method @Deprecated public boolean isValid();
+    method @Deprecated public boolean populate(androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class GridRowView extends androidx.slice.widget.SliceChildView implements android.view.View.OnClickListener android.view.View.OnTouchListener {
-    ctor public GridRowView(android.content.Context);
-    ctor public GridRowView(android.content.Context, android.util.AttributeSet?);
-    method protected boolean addImageItem(androidx.slice.SliceItem, androidx.slice.SliceItem?, int, android.view.ViewGroup, boolean);
-    method protected int getExtraBottomPadding();
-    method protected int getExtraTopPadding();
-    method protected int getMaxCells();
-    method protected int getTitleTextLayout();
+  @Deprecated @RequiresApi(19) public class GridRowView extends androidx.slice.widget.SliceChildView implements android.view.View.OnClickListener android.view.View.OnTouchListener {
+    ctor @Deprecated public GridRowView(android.content.Context);
+    ctor @Deprecated public GridRowView(android.content.Context, android.util.AttributeSet?);
+    method @Deprecated protected boolean addImageItem(androidx.slice.SliceItem, androidx.slice.SliceItem?, int, android.view.ViewGroup, boolean);
+    method @Deprecated protected int getExtraBottomPadding();
+    method @Deprecated protected int getExtraTopPadding();
+    method @Deprecated protected int getMaxCells();
+    method @Deprecated protected int getTitleTextLayout();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void onClick(android.view.View);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public boolean onTouch(android.view.View, android.view.MotionEvent);
-    method protected void populateViews();
-    method public void resetView();
-    method protected boolean scheduleMaxCellsUpdate();
+    method @Deprecated protected void populateViews();
+    method @Deprecated public void resetView();
+    method @Deprecated protected boolean scheduleMaxCellsUpdate();
   }
 
-  @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RowContent extends androidx.slice.widget.SliceContent {
-    ctor public RowContent(androidx.slice.SliceItem!, int);
-    method public java.util.List<androidx.slice.SliceItem!>! getEndItems();
-    method public androidx.slice.SliceItem? getInputRangeThumb();
-    method public boolean getIsHeader();
-    method public int getLineCount();
-    method public androidx.slice.SliceItem? getPrimaryAction();
-    method public androidx.slice.SliceItem? getRange();
-    method public androidx.slice.SliceItem? getSelection();
-    method public androidx.slice.SliceItem? getStartItem();
-    method public androidx.slice.SliceItem? getSubtitleItem();
-    method public androidx.slice.SliceItem? getSummaryItem();
-    method public androidx.slice.SliceItem? getTitleItem();
-    method public java.util.List<androidx.slice.core.SliceAction!>! getToggleItems();
-    method public boolean hasActionDivider();
-    method public boolean hasBottomDivider();
-    method public boolean hasTitleItems();
-    method public boolean isDefaultSeeMore();
-    method public boolean isValid();
-    method public void setIsHeader(boolean);
-    method public void showActionDivider(boolean);
-    method public void showBottomDivider(boolean);
-    method public void showTitleItems(boolean);
+  @Deprecated @RequiresApi(19) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RowContent extends androidx.slice.widget.SliceContent {
+    ctor @Deprecated public RowContent(androidx.slice.SliceItem!, int);
+    method @Deprecated public java.util.List<androidx.slice.SliceItem!>! getEndItems();
+    method @Deprecated public androidx.slice.SliceItem? getInputRangeThumb();
+    method @Deprecated public boolean getIsHeader();
+    method @Deprecated public int getLineCount();
+    method @Deprecated public androidx.slice.SliceItem? getPrimaryAction();
+    method @Deprecated public androidx.slice.SliceItem? getRange();
+    method @Deprecated public androidx.slice.SliceItem? getSelection();
+    method @Deprecated public androidx.slice.SliceItem? getStartItem();
+    method @Deprecated public androidx.slice.SliceItem? getSubtitleItem();
+    method @Deprecated public androidx.slice.SliceItem? getSummaryItem();
+    method @Deprecated public androidx.slice.SliceItem? getTitleItem();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>! getToggleItems();
+    method @Deprecated public boolean hasActionDivider();
+    method @Deprecated public boolean hasBottomDivider();
+    method @Deprecated public boolean hasTitleItems();
+    method @Deprecated public boolean isDefaultSeeMore();
+    method @Deprecated public boolean isValid();
+    method @Deprecated public void setIsHeader(boolean);
+    method @Deprecated public void showActionDivider(boolean);
+    method @Deprecated public void showBottomDivider(boolean);
+    method @Deprecated public void showTitleItems(boolean);
   }
 
-  public interface RowStyleFactory {
-    method @StyleRes public int getRowStyleRes(androidx.slice.SliceItem);
+  @Deprecated public interface RowStyleFactory {
+    method @Deprecated @StyleRes public int getRowStyleRes(androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class RowView extends androidx.slice.widget.SliceChildView implements android.widget.AdapterView.OnItemSelectedListener android.view.View.OnClickListener {
-    ctor public RowView(android.content.Context);
-    method protected java.util.List<java.lang.String!> getEndItemKeys();
-    method protected androidx.slice.SliceItem? getPrimaryActionItem();
-    method protected String? getPrimaryActionKey();
-    method public void onClick(android.view.View);
-    method public void onItemSelected(android.widget.AdapterView<?>, android.view.View, int, long);
-    method public void onNothingSelected(android.widget.AdapterView<?>);
+  @Deprecated @RequiresApi(19) public class RowView extends androidx.slice.widget.SliceChildView implements android.widget.AdapterView.OnItemSelectedListener android.view.View.OnClickListener {
+    ctor @Deprecated public RowView(android.content.Context);
+    method @Deprecated protected java.util.List<java.lang.String!> getEndItemKeys();
+    method @Deprecated protected androidx.slice.SliceItem? getPrimaryActionItem();
+    method @Deprecated protected String? getPrimaryActionKey();
+    method @Deprecated public void onClick(android.view.View);
+    method @Deprecated public void onItemSelected(android.widget.AdapterView<?>, android.view.View, int, long);
+    method @Deprecated public void onNothingSelected(android.widget.AdapterView<?>);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
   }
 
-  @RequiresApi(19) public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
-    ctor public SliceAdapter(android.content.Context);
-    method public androidx.slice.widget.GridRowView getGridRowView();
-    method public int getItemCount();
-    method public androidx.slice.widget.RowView getRowView();
+  @Deprecated @RequiresApi(19) public class SliceAdapter extends androidx.recyclerview.widget.RecyclerView.Adapter<androidx.slice.widget.SliceAdapter.SliceViewHolder> {
+    ctor @Deprecated public SliceAdapter(android.content.Context);
+    method @Deprecated public androidx.slice.widget.GridRowView getGridRowView();
+    method @Deprecated public int getItemCount();
+    method @Deprecated public androidx.slice.widget.RowView getRowView();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void onBindViewHolder(androidx.slice.widget.SliceAdapter.SliceViewHolder, int);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public androidx.slice.widget.SliceAdapter.SliceViewHolder onCreateViewHolder(android.view.ViewGroup, int);
   }
 
-  @RequiresApi(19) public abstract class SliceChildView extends android.widget.FrameLayout {
-    ctor public SliceChildView(android.content.Context);
-    ctor public SliceChildView(android.content.Context, android.util.AttributeSet?);
-    method public abstract void resetView();
-    method public void setSliceItem(androidx.slice.widget.SliceContent?, boolean, int, int, androidx.slice.widget.SliceView.OnSliceActionListener?);
+  @Deprecated @RequiresApi(19) public abstract class SliceChildView extends android.widget.FrameLayout {
+    ctor @Deprecated public SliceChildView(android.content.Context);
+    ctor @Deprecated public SliceChildView(android.content.Context, android.util.AttributeSet?);
+    method @Deprecated public abstract void resetView();
+    method @Deprecated public void setSliceItem(androidx.slice.widget.SliceContent?, boolean, int, int, androidx.slice.widget.SliceView.OnSliceActionListener?);
   }
 
-  @RequiresApi(19) public class SliceContent {
-    ctor public SliceContent(androidx.slice.Slice?);
+  @Deprecated @RequiresApi(19) public class SliceContent {
+    ctor @Deprecated public SliceContent(androidx.slice.Slice?);
   }
 
-  @RequiresApi(19) public final class SliceLiveData {
-    method public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromCachedSlice(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent, androidx.slice.widget.SliceLiveData.OnErrorListener?);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromStream(android.content.Context, androidx.slice.SliceViewManager!, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromStream(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri);
-    method public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
+  @Deprecated @RequiresApi(19) public final class SliceLiveData {
+    method @Deprecated public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromCachedSlice(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromIntent(android.content.Context, android.content.Intent, androidx.slice.widget.SliceLiveData.OnErrorListener?);
+    method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.slice.widget.SliceLiveData.CachedSliceLiveData fromStream(android.content.Context, androidx.slice.SliceViewManager!, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromStream(android.content.Context, java.io.InputStream, androidx.slice.widget.SliceLiveData.OnErrorListener!);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri);
+    method @Deprecated public static androidx.lifecycle.LiveData<androidx.slice.Slice!> fromUri(android.content.Context, android.net.Uri, androidx.slice.widget.SliceLiveData.OnErrorListener?);
   }
 
-  public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
-    method public void goLive();
-    method public void parseStream();
+  @Deprecated public static class SliceLiveData.CachedSliceLiveData extends androidx.lifecycle.LiveData<androidx.slice.Slice> {
+    method @Deprecated public void goLive();
+    method @Deprecated public void parseStream();
   }
 
-  public static interface SliceLiveData.OnErrorListener {
-    method public void onSliceError(@androidx.slice.widget.SliceLiveData.OnErrorListener.ErrorType int, Throwable?);
-    field public static final int ERROR_INVALID_INPUT = 3; // 0x3
-    field public static final int ERROR_SLICE_NO_LONGER_PRESENT = 2; // 0x2
-    field public static final int ERROR_STRUCTURE_CHANGED = 1; // 0x1
-    field public static final int ERROR_UNKNOWN = 0; // 0x0
+  @Deprecated public static interface SliceLiveData.OnErrorListener {
+    method @Deprecated public void onSliceError(@androidx.slice.widget.SliceLiveData.OnErrorListener.ErrorType int, Throwable?);
+    field @Deprecated public static final int ERROR_INVALID_INPUT = 3; // 0x3
+    field @Deprecated public static final int ERROR_SLICE_NO_LONGER_PRESENT = 2; // 0x2
+    field @Deprecated public static final int ERROR_STRUCTURE_CHANGED = 1; // 0x1
+    field @Deprecated public static final int ERROR_UNKNOWN = 0; // 0x0
   }
 
-  @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
+  @Deprecated @IntDef({androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_UNKNOWN, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_STRUCTURE_CHANGED, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, androidx.slice.widget.SliceLiveData.OnErrorListener.ERROR_INVALID_INPUT}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface SliceLiveData.OnErrorListener.ErrorType {
   }
 
-  @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
-    ctor public SliceView(android.content.Context!);
-    ctor public SliceView(android.content.Context!, android.util.AttributeSet?);
-    ctor public SliceView(android.content.Context!, android.util.AttributeSet?, int);
-    ctor @RequiresApi(21) public SliceView(android.content.Context!, android.util.AttributeSet!, int, int);
-    method protected void configureViewPolicy(int);
-    method public int getHiddenItemCount();
-    method public int getMode();
-    method public androidx.slice.Slice? getSlice();
-    method public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
-    method public boolean isScrollable();
-    method public void onChanged(androidx.slice.Slice?);
-    method public void onClick(android.view.View!);
-    method public void setAccentColor(@ColorInt int);
-    method public void setCurrentView(androidx.slice.widget.SliceChildView);
-    method public void setMode(int);
-    method public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener?);
-    method public void setRowStyleFactory(androidx.slice.widget.RowStyleFactory?);
-    method public void setScrollable(boolean);
-    method public void setShowActionDividers(boolean);
-    method public void setShowHeaderDivider(boolean);
-    method public void setShowTitleItems(boolean);
-    method public void setSlice(androidx.slice.Slice?);
-    method public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
-    field public static final int MODE_LARGE = 2; // 0x2
-    field public static final int MODE_SHORTCUT = 3; // 0x3
-    field public static final int MODE_SMALL = 1; // 0x1
+  @Deprecated @RequiresApi(19) public class SliceView extends android.view.ViewGroup implements androidx.lifecycle.Observer<androidx.slice.Slice> android.view.View.OnClickListener {
+    ctor @Deprecated public SliceView(android.content.Context!);
+    ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?);
+    ctor @Deprecated public SliceView(android.content.Context!, android.util.AttributeSet?, int);
+    ctor @Deprecated @RequiresApi(21) public SliceView(android.content.Context!, android.util.AttributeSet!, int, int);
+    method @Deprecated protected void configureViewPolicy(int);
+    method @Deprecated public int getHiddenItemCount();
+    method @Deprecated public int getMode();
+    method @Deprecated public androidx.slice.Slice? getSlice();
+    method @Deprecated public java.util.List<androidx.slice.core.SliceAction!>? getSliceActions();
+    method @Deprecated public boolean isScrollable();
+    method @Deprecated public void onChanged(androidx.slice.Slice?);
+    method @Deprecated public void onClick(android.view.View!);
+    method @Deprecated public void setAccentColor(@ColorInt int);
+    method @Deprecated public void setCurrentView(androidx.slice.widget.SliceChildView);
+    method @Deprecated public void setMode(int);
+    method @Deprecated public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener?);
+    method @Deprecated public void setRowStyleFactory(androidx.slice.widget.RowStyleFactory?);
+    method @Deprecated public void setScrollable(boolean);
+    method @Deprecated public void setShowActionDividers(boolean);
+    method @Deprecated public void setShowHeaderDivider(boolean);
+    method @Deprecated public void setShowTitleItems(boolean);
+    method @Deprecated public void setSlice(androidx.slice.Slice?);
+    method @Deprecated public void setSliceActions(java.util.List<androidx.slice.core.SliceAction!>?);
+    field @Deprecated public static final int MODE_LARGE = 2; // 0x2
+    field @Deprecated public static final int MODE_SHORTCUT = 3; // 0x3
+    field @Deprecated public static final int MODE_SMALL = 1; // 0x1
   }
 
-  public static interface SliceView.OnSliceActionListener {
-    method public void onSliceAction(androidx.slice.widget.EventInfo, androidx.slice.SliceItem);
+  @Deprecated public static interface SliceView.OnSliceActionListener {
+    method @Deprecated public void onSliceAction(androidx.slice.widget.EventInfo, androidx.slice.SliceItem);
   }
 
-  @RequiresApi(19) public class TemplateView extends androidx.slice.widget.SliceChildView {
-    ctor public TemplateView(android.content.Context);
-    method public void onAttachedToWindow();
+  @Deprecated @RequiresApi(19) public class TemplateView extends androidx.slice.widget.SliceChildView {
+    ctor @Deprecated public TemplateView(android.content.Context);
+    method @Deprecated public void onAttachedToWindow();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public void resetView();
-    method public void setAdapter(androidx.slice.widget.SliceAdapter);
+    method @Deprecated public void setAdapter(androidx.slice.widget.SliceAdapter);
   }
 
 }
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java b/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
index edbc303..45009c3 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceMetadata.java
@@ -71,8 +71,13 @@
 
 /**
  * Utility class to parse a {@link Slice} and provide access to information around its contents.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceMetadata {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceStructure.java b/slice/slice-view/src/main/java/androidx/slice/SliceStructure.java
index f4f1f27..4b9f7cd 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceStructure.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceStructure.java
@@ -37,8 +37,13 @@
  * specific content such as text or icons.
  *
  * Two structures can be compared using {@link #equals(Object)}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceStructure {
 
     private final String mStructure;
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceUtils.java b/slice/slice-view/src/main/java/androidx/slice/SliceUtils.java
index 46f1d35..d6209ce 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceUtils.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceUtils.java
@@ -63,8 +63,13 @@
 
 /**
  * Utilities for dealing with slices.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceUtils {
 
     private SliceUtils() {
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManager.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManager.java
index 95c0d6d..3ebb7de 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManager.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManager.java
@@ -34,8 +34,13 @@
  * Class to handle interactions with {@link Slice}s.
  * <p>
  * The SliceViewManager manages permissions and pinned state for slices.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public abstract class SliceViewManager {
 
     /**
@@ -183,7 +188,12 @@
 
     /**
      * Class that listens to changes in {@link Slice}s.
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public interface SliceCallback {
 
         /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerBase.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerBase.java
index 217d0f17d..b7a5750 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerBase.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerBase.java
@@ -37,6 +37,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public abstract class SliceViewManagerBase extends SliceViewManager {
     private final ArrayMap<Pair<Uri, SliceCallback>, SliceListenerImpl> mListenerLookup =
             new ArrayMap<>();
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerCompat.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerCompat.java
index 1c1d436..02bfec0 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerCompat.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerCompat.java
@@ -36,6 +36,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 class SliceViewManagerCompat extends SliceViewManagerBase {
 
     SliceViewManagerCompat(Context context) {
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
index c4364d9..58a354a 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceViewManagerWrapper.java
@@ -43,6 +43,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(api = 28)
+@Deprecated
 class SliceViewManagerWrapper extends SliceViewManagerBase {
     private static final String TAG = "SliceViewManagerWrapper"; // exactly 23
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/SliceXml.java b/slice/slice-view/src/main/java/androidx/slice/SliceXml.java
index 1251afb..b09499e 100644
--- a/slice/slice-view/src/main/java/androidx/slice/SliceXml.java
+++ b/slice/slice-view/src/main/java/androidx/slice/SliceXml.java
@@ -59,6 +59,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 class SliceXml {
 
     private static final String NAMESPACE = null;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/ActionRow.java b/slice/slice-view/src/main/java/androidx/slice/widget/ActionRow.java
index 15f0ac0..c32f815 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/ActionRow.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/ActionRow.java
@@ -54,6 +54,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ActionRow extends FrameLayout {
 
     private static final int MAX_ACTIONS = 5;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/DisplayedListItems.java b/slice/slice-view/src/main/java/androidx/slice/widget/DisplayedListItems.java
index 4b5ee95..154d842 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/DisplayedListItems.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/DisplayedListItems.java
@@ -25,6 +25,7 @@
  *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@Deprecated
 class DisplayedListItems {
     private final List<SliceContent> mDisplayedItems;
     private final int mHiddenItemCount;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/EventInfo.java b/slice/slice-view/src/main/java/androidx/slice/widget/EventInfo.java
index db5231f..5e315a2 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/EventInfo.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/EventInfo.java
@@ -25,8 +25,13 @@
 
 /**
  * Represents information associated with a logged event on {@link SliceView}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class EventInfo {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/GridContent.java b/slice/slice-view/src/main/java/androidx/slice/widget/GridContent.java
index ee38115..1c8036e 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/GridContent.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/GridContent.java
@@ -55,8 +55,13 @@
 
 /**
  * Extracts information required to present content in a grid format from a slice.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class GridContent extends SliceContent {
 
     private boolean mAllImages;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/GridRowView.java b/slice/slice-view/src/main/java/androidx/slice/widget/GridRowView.java
index 9bbd5de..b48c559 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/GridRowView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/GridRowView.java
@@ -86,6 +86,12 @@
 import java.util.Iterator;
 import java.util.List;
 
+/**
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
+ */
+@Deprecated
 @RequiresApi(19)
 public class GridRowView extends SliceChildView implements View.OnClickListener,
         View.OnTouchListener {
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/ListContent.java b/slice/slice-view/src/main/java/androidx/slice/widget/ListContent.java
index 8151152..8f28a05 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/ListContent.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/ListContent.java
@@ -54,6 +54,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ListContent extends SliceContent {
 
     private SliceAction mPrimaryAction;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/LocationBasedViewTracker.java b/slice/slice-view/src/main/java/androidx/slice/widget/LocationBasedViewTracker.java
index 0404633..b7c5639 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/LocationBasedViewTracker.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/LocationBasedViewTracker.java
@@ -34,6 +34,7 @@
  * Utility class to track view based on relative location to the parent.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
+@Deprecated
 public class LocationBasedViewTracker implements Runnable, View.OnLayoutChangeListener {
 
     private static final SelectionLogic INPUT_FOCUS = new SelectionLogic() {
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/MessageView.java b/slice/slice-view/src/main/java/androidx/slice/widget/MessageView.java
index 8e44e18..d14fbdb 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/MessageView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/MessageView.java
@@ -40,6 +40,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class MessageView extends SliceChildView {
 
     private TextView mDetails;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RemoteInputView.java b/slice/slice-view/src/main/java/androidx/slice/widget/RemoteInputView.java
index 09392c9..5e89e45 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RemoteInputView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RemoteInputView.java
@@ -63,6 +63,7 @@
 @SuppressWarnings("AppCompatCustomView")
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(21)
+@Deprecated
 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
 
     private static final String TAG = "RemoteInput";
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RowContent.java b/slice/slice-view/src/main/java/androidx/slice/widget/RowContent.java
index eed88be..dcd403c 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RowContent.java
@@ -60,6 +60,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @RequiresApi(19)
+@Deprecated
 public class RowContent extends SliceContent {
     private static final String TAG = "RowContent";
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RowStyle.java b/slice/slice-view/src/main/java/androidx/slice/widget/RowStyle.java
index 02416ac..960bfcc 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RowStyle.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RowStyle.java
@@ -31,6 +31,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class RowStyle {
     public static final int UNBOUNDED = -1;
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RowStyleFactory.java b/slice/slice-view/src/main/java/androidx/slice/widget/RowStyleFactory.java
index 39e9b74..4ada731 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RowStyleFactory.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RowStyleFactory.java
@@ -22,7 +22,12 @@
 
 /**
  * Factory to return different styles for child views of a slice.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public interface RowStyleFactory {
     /**
      * Returns the style resource to use for this child.
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/RowView.java b/slice/slice-view/src/main/java/androidx/slice/widget/RowView.java
index 34d23e5..84f7712 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/RowView.java
@@ -119,8 +119,13 @@
 /**
  * Row item is in small template format and can be used to construct list items for use
  * with {@link TemplateView}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class RowView extends SliceChildView implements View.OnClickListener,
         AdapterView.OnItemSelectedListener {
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/ShortcutView.java b/slice/slice-view/src/main/java/androidx/slice/widget/ShortcutView.java
index 89ae3d0..5fa1625 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/ShortcutView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/ShortcutView.java
@@ -42,6 +42,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class ShortcutView extends SliceChildView {
 
     private static final String TAG = "ShortcutView";
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceActionView.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceActionView.java
index e5b20d0..0ffdf27 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceActionView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceActionView.java
@@ -56,6 +56,7 @@
 @SuppressWarnings("AppCompatCustomView")
 @RestrictTo(LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SliceActionView extends FrameLayout implements View.OnClickListener,
         CompoundButton.OnCheckedChangeListener {
     private static final String TAG = "SliceActionView";
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceAdapter.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceAdapter.java
index f8ae73a..4fc45ab 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceAdapter.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceAdapter.java
@@ -47,9 +47,14 @@
 
 /**
  * RecyclerView.Adapter for the Slice components.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @SuppressWarnings("HiddenSuperclass")
 @RequiresApi(19)
+@Deprecated
 public class SliceAdapter extends RecyclerView.Adapter<SliceAdapter.SliceViewHolder>
         implements SliceActionView.SliceActionLoadingListener {
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceChildView.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceChildView.java
index b0795bf..9b1b11b 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceChildView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceChildView.java
@@ -35,8 +35,13 @@
 
 /**
  * Base class for children views of {@link SliceView}.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public abstract class SliceChildView extends FrameLayout {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceContent.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceContent.java
index a56e1e9..0d03a82 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceContent.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceContent.java
@@ -53,8 +53,13 @@
 
 /**
  * Base class representing content that can be displayed.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceContent {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceLiveData.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceLiveData.java
index 8fc3a85..ee2ca9e 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceLiveData.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceLiveData.java
@@ -55,8 +55,13 @@
  *
  * @see #fromUri(Context, Uri)
  * @see LiveData
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public final class SliceLiveData {
     private static final String TAG = "SliceLiveData";
 
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetrics.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetrics.java
index faa5e10..30178d3 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetrics.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetrics.java
@@ -29,6 +29,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 class SliceMetrics {
 
     public static @Nullable SliceMetrics getInstance(@NonNull Context context, @NonNull Uri uri) {
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetricsWrapper.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetricsWrapper.java
index adb43d8..fe4abcca 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetricsWrapper.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceMetricsWrapper.java
@@ -27,6 +27,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(api = 28)
+@Deprecated
 class SliceMetricsWrapper extends SliceMetrics {
 
     private final android.app.slice.SliceMetrics mSliceMetrics;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceStyle.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceStyle.java
index 2440e67..7d86b57 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceStyle.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceStyle.java
@@ -44,6 +44,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SliceStyle {
     private int mTintColor = -1;
     private final int mTitleColor;
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
index 8c6fd44..43cbaf8 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceView.java
@@ -94,8 +94,13 @@
  *
  * @see Slice
  * @see SliceLiveData
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @RequiresApi(19)
+@Deprecated
 public class SliceView extends ViewGroup implements Observer<Slice>, View.OnClickListener {
 
     private static final String TAG = "SliceView";
@@ -104,7 +109,12 @@
      * Implement this interface to be notified of interactions with the slice displayed
      * in this view.
      * @see EventInfo
+     *
+     * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+     * forward. If you are looking for a framework that handles communication across apps,
+     * consider using {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public interface OnSliceActionListener {
         /**
          * Called when an interaction has occurred with an element in this view.
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewPolicy.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewPolicy.java
index 57dcf89..3c649ac 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewPolicy.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewPolicy.java
@@ -27,6 +27,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SliceViewPolicy {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewUtil.java b/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewUtil.java
index f4f0499..bb0eef3 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewUtil.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/SliceViewUtil.java
@@ -54,6 +54,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 @RequiresApi(19)
+@Deprecated
 public class SliceViewUtil {
 
     /**
diff --git a/slice/slice-view/src/main/java/androidx/slice/widget/TemplateView.java b/slice/slice-view/src/main/java/androidx/slice/widget/TemplateView.java
index b26282b..fca31f0 100644
--- a/slice/slice-view/src/main/java/androidx/slice/widget/TemplateView.java
+++ b/slice/slice-view/src/main/java/androidx/slice/widget/TemplateView.java
@@ -36,9 +36,14 @@
 
 /**
  * Slice template containing all view components.
+ *
+ * @deprecated Slice framework has been deprecated, it will not receive any updates moving
+ * forward. If you are looking for a framework that handles communication across apps,
+ * consider using {@link android.app.appsearch.AppSearchManager}.
  */
 @SuppressWarnings("HiddenSuperclass")
 @RequiresApi(19)
+@Deprecated
 public class TemplateView extends SliceChildView implements
         SliceViewPolicy.PolicyChangeListener {
 
diff --git a/transition/transition/build.gradle b/transition/transition/build.gradle
index 165c99b..693fbbd 100644
--- a/transition/transition/build.gradle
+++ b/transition/transition/build.gradle
@@ -10,7 +10,7 @@
     api("androidx.annotation:annotation:1.2.0")
     api("androidx.core:core:1.12.0")
     implementation("androidx.collection:collection:1.1.0")
-    compileOnly("androidx.fragment:fragment:1.2.5")
+    compileOnly(projectOrArtifact(":fragment:fragment"))
     compileOnly("androidx.appcompat:appcompat:1.0.1")
     implementation("androidx.dynamicanimation:dynamicanimation:1.0.0")
 
diff --git a/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
new file mode 100644
index 0000000..868a511
--- /dev/null
+++ b/transition/transition/src/androidTest/java/androidx/transition/FragmentTransitionSeekingTest.kt
@@ -0,0 +1,446 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.transition
+
+import android.os.Build
+import android.window.BackEvent
+import androidx.activity.BackEventCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.waitForExecution
+import androidx.transition.test.R
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class FragmentTransitionSeekingTest {
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val activityRule = androidx.test.rule.ActivityTestRule(
+        FragmentTransitionTestActivity::class.java
+    )
+
+    @Test
+    fun replaceOperationWithTransitionsThenGestureBack() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        var startedEnter = false
+        val fragment1 = TransitionFragment(R.layout.scene1)
+        fragment1.setReenterTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedEnter = true
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val startedExitCountDownLatch = CountDownLatch(1)
+        val fragment2 = TransitionFragment()
+        fragment2.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        fragment1.waitForTransition()
+        fragment2.waitForTransition()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(startedEnter).isTrue()
+        assertThat(startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment1.waitForTransition()
+
+        assertThat(fragment2.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("2"))
+            .isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment1.requireView().parent).isNotNull()
+    }
+
+    @Test
+    fun replaceOperationWithTransitionsThenBackCancelled() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        var startedEnter = false
+        val fragment1 = TransitionFragment(R.layout.scene1)
+        fragment1.setReenterTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedEnter = true
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val startedExitCountDownLatch = CountDownLatch(1)
+        val fragment2 = TransitionFragment()
+        fragment2.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        fragment1.waitForTransition()
+        fragment2.waitForTransition()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(startedEnter).isTrue()
+        assertThat(startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackCancelled()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment1.waitForTransition()
+
+        assertThat(fragment2.isAdded).isTrue()
+        assertThat(fm1.findFragmentByTag("2")).isEqualTo(fragment2)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment2.requireView()).isNotNull()
+    }
+
+    @Test
+    fun replaceOperationWithTransitionsThenGestureBackTwice() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        var startedEnter = false
+        val fragment1 = TransitionFragment(R.layout.scene1)
+        fragment1.setReenterTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedEnter = true
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val fragment2startedExitCountDownLatch = CountDownLatch(1)
+        val fragment2 = TransitionFragment()
+        fragment2.setReenterTransition(Fade().apply { duration = 300 })
+        fragment2.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    fragment2startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        fragment1.waitForTransition()
+        fragment2.waitForTransition()
+
+        val fragment3startedExitCountDownLatch = CountDownLatch(1)
+        val fragment3 = TransitionFragment()
+        fragment3.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    fragment3startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment3, "3")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        assertThat(fragment3.startTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+            .isTrue()
+        // We need to wait for the exit animation to end
+        assertThat(fragment2.endTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+            .isTrue()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(fragment3startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment2.waitForTransition()
+        fragment3.waitForTransition()
+
+        assertThat(fragment3.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("3")).isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment2.requireView().parent).isNotNull()
+
+        val fragment2ResumedLatch = CountDownLatch(1)
+        activityRule.runOnUiThread {
+            fragment2.lifecycle.addObserver(
+                object : LifecycleEventObserver {
+                    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+                        if (event.targetState == Lifecycle.State.RESUMED) {
+                            fragment2ResumedLatch.countDown()
+                        }
+                    }
+                }
+            )
+        }
+
+        assertThat(fragment2ResumedLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackProgressed(
+                BackEventCompat(0.2F, 0.2F, 0.2F, BackEvent.EDGE_LEFT)
+            )
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(startedEnter).isTrue()
+        assertThat(fragment2startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        fragment1.waitForTransition()
+
+        assertThat(fragment2.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("2")).isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment1.requireView().parent).isNotNull()
+    }
+
+    @Test
+    fun replaceOperationWithTransitionsThenOnBackPressedTwice() {
+        val fm1 = activityRule.activity.supportFragmentManager
+
+        var startedEnter = false
+        val fragment1 = TransitionFragment(R.layout.scene1)
+        fragment1.setReenterTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    startedEnter = true
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment1, "1")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.waitForExecution()
+
+        val fragment2startedExitCountDownLatch = CountDownLatch(1)
+        val fragment2 = TransitionFragment()
+        fragment2.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    fragment2startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment2, "2")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        fragment1.waitForTransition()
+        fragment2.waitForTransition()
+
+        val fragment3startedExitCountDownLatch = CountDownLatch(1)
+        val fragment3 = TransitionFragment()
+        fragment3.setReturnTransition(Fade().apply {
+            duration = 300
+            addListener(object : TransitionListenerAdapter() {
+                override fun onTransitionStart(transition: Transition) {
+                    fragment3startedExitCountDownLatch.countDown()
+                }
+            })
+        })
+
+        fm1.beginTransaction()
+            .replace(R.id.fragmentContainer, fragment3, "3")
+            .setReorderingAllowed(true)
+            .addToBackStack(null)
+            .commit()
+        activityRule.executePendingTransactions()
+
+        assertThat(fragment3.startTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+            .isTrue()
+        // We need to wait for the exit animation to end
+        assertThat(fragment2.endTransitionCountDownLatch.await(1000, TimeUnit.MILLISECONDS))
+            .isTrue()
+
+        val dispatcher = activityRule.activity.onBackPressedDispatcher
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(fragment3startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        fragment2.waitForTransition()
+        fragment3.waitForTransition()
+
+        assertThat(fragment3.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("3")).isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment2.requireView().parent).isNotNull()
+
+        activityRule.runOnUiThread {
+            dispatcher.dispatchOnBackStarted(BackEventCompat(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        activityRule.runOnUiThread {
+            dispatcher.onBackPressed()
+        }
+        activityRule.executePendingTransactions(fm1)
+
+        assertThat(startedEnter).isTrue()
+        assertThat(fragment2startedExitCountDownLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+        fragment1.waitForTransition()
+
+        assertThat(fragment2.isAdded).isFalse()
+        assertThat(fm1.findFragmentByTag("2")).isEqualTo(null)
+
+        // Make sure the original fragment was correctly readded to the container
+        assertThat(fragment1.requireView().parent).isNotNull()
+    }
+}
diff --git a/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java b/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
index 217c891..6a0d6c9 100644
--- a/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
+++ b/transition/transition/src/main/java/androidx/transition/FragmentTransitionSupport.java
@@ -20,6 +20,7 @@
 
 import android.annotation.SuppressLint;
 import android.graphics.Rect;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -228,6 +229,54 @@
     }
 
     @Override
+    public boolean isSeekingSupported(@NonNull Object transition) {
+        boolean supported = ((Transition) transition).isSeekingSupported();
+        if (!supported) {
+            Log.v("FragmentManager",
+                    "Predictive back not available for AndroidX Transition "
+                            + transition + ". Please enable seeking support for the designated "
+                            + "transition by overriding isSeekingSupported().");
+        }
+        return supported;
+    }
+
+    @Override
+    @Nullable
+    public Object controlDelayedTransition(@NonNull ViewGroup sceneRoot,
+            @NonNull Object transition) {
+        return TransitionManager.controlDelayedTransition(sceneRoot, (Transition) transition);
+    }
+
+    @Override
+    public void setCurrentPlayTime(@NonNull Object transitionController, float progress) {
+        TransitionSeekController controller = (TransitionSeekController) transitionController;
+        if (controller.isReady()) {
+            long time = (long) (progress * controller.getDurationMillis());
+            // We cannot let the time get to 0 or the totalDuration to avoid
+            // completing the operation accidentally.
+            if (time == 0L) {
+                time = 1L;
+            }
+            if (time == controller.getDurationMillis()) {
+                time = controller.getDurationMillis() - 1;
+            }
+            controller.setCurrentPlayTimeMillis(time);
+        }
+    }
+
+    @Override
+    public void animateToEnd(@NonNull Object transitionController) {
+        TransitionSeekController controller = (TransitionSeekController) transitionController;
+        controller.animateToEnd();
+    }
+
+    @Override
+    public void animateToStart(@NonNull Object transitionController) {
+        TransitionSeekController controller = (TransitionSeekController) transitionController;
+        controller.animateToStart();
+    }
+
+    @Override
     public void scheduleRemoveTargets(final @NonNull Object overallTransitionObj,
             final @Nullable Object enterTransition, final @Nullable ArrayList<View> enteringViews,
             final @Nullable Object exitTransition, final @Nullable ArrayList<View> exitingViews,
@@ -269,11 +318,23 @@
     public void setListenerForTransitionEnd(@NonNull final Fragment outFragment,
             @NonNull final Object transition, @NonNull final CancellationSignal signal,
             @NonNull final Runnable transitionCompleteRunnable) {
+        setListenerForTransitionEnd(outFragment, transition, signal,
+                null, transitionCompleteRunnable);
+    }
+
+    @Override
+    public void setListenerForTransitionEnd(@NonNull Fragment outFragment,
+            @NonNull Object transition, @NonNull CancellationSignal signal,
+            @Nullable Runnable cancelRunnable, @NonNull Runnable transitionCompleteRunnable) {
         final Transition realTransition = ((Transition) transition);
         signal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
             @Override
             public void onCancel() {
-                realTransition.cancel();
+                if (cancelRunnable == null) {
+                    realTransition.cancel();
+                } else {
+                    cancelRunnable.run();
+                }
             }
         });
         realTransition.addListener(new Transition.TransitionListener() {
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
index 1557ac8..c9d1f31 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
@@ -16,9 +16,7 @@
 
 package androidx.tv.integration.playground
 
-import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -26,38 +24,30 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.selection.selectableGroup
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
 import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material.icons.filled.Settings
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.semantics.selected
-import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.zIndex
-import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Icon
 import androidx.tv.material3.NavigationDrawer
+import androidx.tv.material3.NavigationDrawerItem
+import androidx.tv.material3.NavigationDrawerItemDefaults
+import androidx.tv.material3.NavigationDrawerScope
 import androidx.tv.material3.Text
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -68,14 +58,7 @@
     CompositionLocalProvider(LocalLayoutDirection provides direction.value) {
         Row(Modifier.fillMaxSize()) {
             Box(modifier = Modifier.height(400.dp)) {
-                NavigationDrawer(
-                    drawerContent = { drawerValue ->
-                        Sidebar(
-                            drawerValue = drawerValue,
-                            direction = direction,
-                        )
-                    }
-                ) {
+                NavigationDrawer(drawerContent = { Sidebar(direction = direction) }) {
                     CommonBackground()
                 }
             }
@@ -92,12 +75,7 @@
         Row(Modifier.fillMaxSize()) {
             Box(modifier = Modifier.height(400.dp)) {
                 androidx.tv.material3.ModalNavigationDrawer(
-                    drawerContent = { drawerValue ->
-                        Sidebar(
-                            drawerValue = drawerValue,
-                            direction = direction,
-                        )
-                    }
+                    drawerContent = { Sidebar(direction = direction) }
                 ) {
                     CommonBackground()
                 }
@@ -115,10 +93,7 @@
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
-private fun Sidebar(
-    drawerValue: DrawerValue,
-    direction: MutableState<LayoutDirection>,
-) {
+private fun NavigationDrawerScope.Sidebar(direction: MutableState<LayoutDirection>) {
     val selectedIndex = remember { mutableStateOf(0) }
 
     LaunchedEffect(selectedIndex.value) {
@@ -132,81 +107,52 @@
         modifier = Modifier
             .fillMaxHeight()
             .background(pageColor)
+            .padding(12.dp)
             .selectableGroup(),
-        horizontalAlignment = Alignment.CenterHorizontally,
+        horizontalAlignment = Alignment.Start,
         verticalArrangement = Arrangement.spacedBy(10.dp)
     ) {
-        NavigationItem(
-            imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
-            text = "LTR",
-            drawerValue = drawerValue,
-            selectedIndex = selectedIndex,
-            index = 0
-        )
-        NavigationItem(
-            imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft,
-            text = "RTL",
-            drawerValue = drawerValue,
-            selectedIndex = selectedIndex,
-            index = 1
-        )
-    }
-}
-
-@OptIn(ExperimentalTvMaterial3Api::class)
-@Composable
-private fun NavigationItem(
-    imageVector: ImageVector,
-    text: String,
-    drawerValue: DrawerValue,
-    selectedIndex: MutableState<Int>,
-    index: Int,
-    modifier: Modifier = Modifier,
-) {
-    var isFocused by remember { mutableStateOf(false) }
-
-    Box(
-        modifier = modifier
-            .clip(RoundedCornerShape(10.dp))
-            .onFocusChanged { isFocused = it.isFocused }
-            .background(if (isFocused) Color.White else Color.Transparent)
-            .semantics(mergeDescendants = true) {
-                selected = selectedIndex.value == index
-            }
-            .clickable {
-                selectedIndex.value = index
-            }
-    ) {
-        Box(modifier = Modifier.padding(10.dp)) {
-            Row(
-                verticalAlignment = Alignment.CenterVertically,
-                horizontalArrangement = Arrangement.spacedBy(5.dp),
-            ) {
+        NavigationDrawerItem(
+            selected = true,
+            onClick = { },
+            leadingContent = {
                 Icon(
-                    imageVector = imageVector,
-                    tint = if (isFocused) pageColor else Color.White,
+                    imageVector = Icons.Default.Settings,
                     contentDescription = null,
                 )
-                AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
-                    Text(
-                        text = text,
-                        modifier = Modifier,
-                        softWrap = false,
-                        color = if (isFocused) pageColor else Color.White,
-                    )
-                }
+            },
+            supportingContent = {
+                Text("Switch account")
+            },
+            trailingContent = {
+                NavigationDrawerItemDefaults.TrailingBadge("NEW")
             }
-            if (selectedIndex.value == index) {
-                Box(
-                    modifier = Modifier
-                        .width(10.dp)
-                        .height(3.dp)
-                        .offset(y = 5.dp)
-                        .align(Alignment.BottomCenter)
-                        .background(Color.Red)
-                        .zIndex(10f)
+        ) {
+            Text(text = "Hi there")
+        }
+        NavigationDrawerItem(
+            selected = selectedIndex.value == 0,
+            onClick = { selectedIndex.value = 0 },
+            leadingContent = {
+                Icon(
+                    imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
+                    contentDescription = null,
                 )
-            }
+            },
+        ) {
+            Text(text = "Left to right")
+        }
+        NavigationDrawerItem(
+            selected = selectedIndex.value == 1,
+            onClick = { selectedIndex.value = 1 },
+            leadingContent = {
+                Icon(
+                    imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft,
+                    contentDescription = null,
+                )
+            },
+        ) {
+            Text(text = "Right to left")
         }
     }
 }
diff --git a/tv/samples/src/main/java/androidx/tv/samples/NavigationDrawerSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/NavigationDrawerSamples.kt
index dc061ec..e96c9d9 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/NavigationDrawerSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/NavigationDrawerSamples.kt
@@ -17,62 +17,79 @@
 package androidx.tv.samples
 
 import androidx.annotation.Sampled
-import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.background
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material3.Button
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
-import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Icon
 import androidx.tv.material3.ModalNavigationDrawer
 import androidx.tv.material3.NavigationDrawer
+import androidx.tv.material3.NavigationDrawerItem
 import androidx.tv.material3.Text
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Sampled
 @Composable
 fun SampleNavigationDrawer() {
-    val navigationRow: @Composable (drawerValue: DrawerValue, color: Color, text: String) -> Unit =
-        { drawerValue, color, text ->
-            Row(Modifier.padding(10.dp).focusable()) {
-                Box(Modifier.size(50.dp).background(color).padding(end = 20.dp))
-                AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
-                    Text(
-                        text = text,
-                        softWrap = false,
-                        modifier = Modifier.padding(15.dp).width(50.dp),
-                        textAlign = TextAlign.Center
-                    )
-                }
-            }
-        }
+    var selectedIndex by remember { mutableIntStateOf(0) }
+
+    val items = listOf(
+        "Home" to Icons.Default.Home,
+        "Settings" to Icons.Default.Settings,
+        "Favourites" to Icons.Default.Favorite,
+    )
 
     NavigationDrawer(
         drawerContent = {
-            Column(Modifier.background(Color.Gray).fillMaxHeight()) {
-                navigationRow(it, Color.Red, "Red")
-                navigationRow(it, Color.Blue, "Blue")
-                navigationRow(it, Color.Yellow, "Yellow")
+            Column(
+                Modifier
+                    .background(Color.Gray)
+                    .fillMaxHeight()
+                    .padding(12.dp)
+                    .selectableGroup(),
+                horizontalAlignment = Alignment.Start,
+                verticalArrangement = Arrangement.spacedBy(10.dp)
+            ) {
+                items.forEachIndexed { index, item ->
+                    val (text, icon) = item
+
+                    NavigationDrawerItem(
+                        selected = selectedIndex == index,
+                        onClick = { selectedIndex = index },
+                        leadingContent = {
+                            Icon(
+                                imageVector = icon,
+                                contentDescription = null,
+                            )
+                        }
+                    ) {
+                        Text(text)
+                    }
+                }
             }
         }
     ) {
-        Button(modifier = Modifier
-            .height(100.dp)
-            .fillMaxWidth(), onClick = {}) {
+        Button(modifier = Modifier.height(100.dp).fillMaxWidth(), onClick = {}) {
             Text("BUTTON")
         }
     }
@@ -82,33 +99,45 @@
 @Sampled
 @Composable
 fun SampleModalNavigationDrawerWithSolidScrim() {
-    val navigationRow: @Composable (drawerValue: DrawerValue, color: Color, text: String) -> Unit =
-        { drawerValue, color, text ->
-            Row(Modifier.padding(10.dp).focusable()) {
-                Box(Modifier.size(50.dp).background(color).padding(end = 20.dp))
-                AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
-                    Text(
-                        text = text,
-                        softWrap = false,
-                        modifier = Modifier.padding(15.dp).width(50.dp),
-                        textAlign = TextAlign.Center
-                    )
-                }
-            }
-        }
+    var selectedIndex by remember { mutableIntStateOf(0) }
+
+    val items = listOf(
+        "Home" to Icons.Default.Home,
+        "Settings" to Icons.Default.Settings,
+        "Favourites" to Icons.Default.Favorite,
+    )
 
     ModalNavigationDrawer(
         drawerContent = {
-            Column(Modifier.background(Color.Gray).fillMaxHeight()) {
-                navigationRow(it, Color.Red, "Red")
-                navigationRow(it, Color.Blue, "Blue")
-                navigationRow(it, Color.Yellow, "Yellow")
+            Column(
+                Modifier
+                    .background(Color.Gray)
+                    .fillMaxHeight()
+                    .padding(12.dp)
+                    .selectableGroup(),
+                horizontalAlignment = Alignment.Start,
+                verticalArrangement = Arrangement.spacedBy(10.dp)
+            ) {
+                items.forEachIndexed { index, item ->
+                    val (text, icon) = item
+
+                    NavigationDrawerItem(
+                        selected = selectedIndex == index,
+                        onClick = { selectedIndex = index },
+                        leadingContent = {
+                            Icon(
+                                imageVector = icon,
+                                contentDescription = null,
+                            )
+                        }
+                    ) {
+                        Text(text)
+                    }
+                }
             }
         }
     ) {
-        Button(modifier = Modifier
-            .height(100.dp)
-            .fillMaxWidth(), onClick = {}) {
+        Button(modifier = Modifier.height(100.dp).fillMaxWidth(), onClick = {}) {
             Text("BUTTON")
         }
     }
@@ -118,34 +147,46 @@
 @Sampled
 @Composable
 fun SampleModalNavigationDrawerWithGradientScrim() {
-    val navigationRow: @Composable (drawerValue: DrawerValue, color: Color, text: String) -> Unit =
-        { drawerValue, color, text ->
-            Row(Modifier.padding(10.dp).focusable()) {
-                Box(Modifier.size(50.dp).background(color).padding(end = 20.dp))
-                AnimatedVisibility(visible = drawerValue == DrawerValue.Open) {
-                    Text(
-                        text = text,
-                        softWrap = false,
-                        modifier = Modifier.padding(15.dp).width(50.dp),
-                        textAlign = TextAlign.Center
-                    )
-                }
-            }
-        }
+    var selectedIndex by remember { mutableIntStateOf(0) }
 
-    androidx.tv.material3.ModalNavigationDrawer(
+    val items = listOf(
+        "Home" to Icons.Default.Home,
+        "Settings" to Icons.Default.Settings,
+        "Favourites" to Icons.Default.Favorite,
+    )
+
+    ModalNavigationDrawer(
         drawerContent = {
-            Column(Modifier.fillMaxHeight()) {
-                navigationRow(it, Color.Red, "Red")
-                navigationRow(it, Color.Blue, "Blue")
-                navigationRow(it, Color.Yellow, "Yellow")
+            Column(
+                Modifier
+                    .background(Color.Gray)
+                    .fillMaxHeight()
+                    .padding(12.dp)
+                    .selectableGroup(),
+                horizontalAlignment = Alignment.Start,
+                verticalArrangement = Arrangement.spacedBy(10.dp)
+            ) {
+                items.forEachIndexed { index, item ->
+                    val (text, icon) = item
+
+                    NavigationDrawerItem(
+                        selected = selectedIndex == index,
+                        onClick = { selectedIndex = index },
+                        leadingContent = {
+                            Icon(
+                                imageVector = icon,
+                                contentDescription = null,
+                            )
+                        }
+                    ) {
+                        Text(text)
+                    }
+                }
             }
         },
         scrimBrush = Brush.horizontalGradient(listOf(Color.DarkGray, Color.Transparent))
     ) {
-        Button(modifier = Modifier
-            .height(100.dp)
-            .fillMaxWidth(), onClick = {}) {
+        Button(modifier = Modifier.height(100.dp).fillMaxWidth(), onClick = {}) {
             Text("BUTTON")
         }
     }
diff --git a/tv/tv-material/api/api_lint.ignore b/tv/tv-material/api/api_lint.ignore
new file mode 100644
index 0000000..5954166
--- /dev/null
+++ b/tv/tv-material/api/api_lint.ignore
@@ -0,0 +1,4 @@
+// Baseline format: 1.0
+
+GetterSetterNames: field NavigationDrawerScope.doesNavigationDrawerHaveFocus:
+    Invalid name for boolean property `doesNavigationDrawerHaveFocus`. Should start with one of `has`, `can`, `should`, `is`.
\ No newline at end of file
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 1ea43bf..dcdfda26 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -690,6 +690,10 @@
     property public final androidx.tv.material3.Glow selectedGlow;
   }
 
+  public final class NavigationDrawerItemKt {
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemScale {
     ctor public NavigationDrawerItemScale(@FloatRange(from=0.0) float scale, @FloatRange(from=0.0) float focusedScale, @FloatRange(from=0.0) float pressedScale, @FloatRange(from=0.0) float selectedScale, @FloatRange(from=0.0) float disabledScale, @FloatRange(from=0.0) float focusedSelectedScale, @FloatRange(from=0.0) float focusedDisabledScale, @FloatRange(from=0.0) float pressedSelectedScale);
     method public float getDisabledScale();
@@ -737,14 +741,14 @@
   }
 
   public final class NavigationDrawerKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.DrawerState rememberDrawerState(androidx.tv.material3.DrawerValue initialValue);
   }
 
   @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface NavigationDrawerScope {
-    method public boolean isActivated();
-    property public abstract boolean isActivated;
+    method public boolean getDoesNavigationDrawerHaveFocus();
+    property public abstract boolean doesNavigationDrawerHaveFocus;
   }
 
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NonInteractiveSurfaceColors {
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 1ea43bf..dcdfda26 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -690,6 +690,10 @@
     property public final androidx.tv.material3.Glow selectedGlow;
   }
 
+  public final class NavigationDrawerItemKt {
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawerItem(androidx.tv.material3.NavigationDrawerScope, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> leadingContent, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingContent, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingContent, optional float tonalElevation, optional androidx.tv.material3.NavigationDrawerItemShape shape, optional androidx.tv.material3.NavigationDrawerItemColors colors, optional androidx.tv.material3.NavigationDrawerItemScale scale, optional androidx.tv.material3.NavigationDrawerItemBorder border, optional androidx.tv.material3.NavigationDrawerItemGlow glow, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NavigationDrawerItemScale {
     ctor public NavigationDrawerItemScale(@FloatRange(from=0.0) float scale, @FloatRange(from=0.0) float focusedScale, @FloatRange(from=0.0) float pressedScale, @FloatRange(from=0.0) float selectedScale, @FloatRange(from=0.0) float disabledScale, @FloatRange(from=0.0) float focusedSelectedScale, @FloatRange(from=0.0) float focusedDisabledScale, @FloatRange(from=0.0) float pressedSelectedScale);
     method public float getDisabledScale();
@@ -737,14 +741,14 @@
   }
 
   public final class NavigationDrawerKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function1<? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void ModalNavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, optional androidx.compose.ui.graphics.Brush scrimBrush, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void NavigationDrawer(kotlin.jvm.functions.Function2<? super androidx.tv.material3.NavigationDrawerScope,? super androidx.tv.material3.DrawerValue,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.DrawerState drawerState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static androidx.tv.material3.DrawerState rememberDrawerState(androidx.tv.material3.DrawerValue initialValue);
   }
 
   @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public interface NavigationDrawerScope {
-    method public boolean isActivated();
-    property public abstract boolean isActivated;
+    method public boolean getDoesNavigationDrawerHaveFocus();
+    property public abstract boolean doesNavigationDrawerHaveFocus;
   }
 
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class NonInteractiveSurfaceColors {
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt
new file mode 100644
index 0000000..e98dd07
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemScreenshotTest.kt
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalTvMaterial3Api::class)
+class NavigationDrawerItemScreenshotTest(private val scheme: ColorSchemeWrapper) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
+
+    val wrapperModifier = Modifier
+        .testTag(NavigationDrawerItemWrapperTag)
+        .background(scheme.colorScheme.surface)
+        .padding(20.dp)
+
+    @Test
+    fun navigationDrawerItem_customColor() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    colors = NavigationDrawerItemDefaults.colors(containerColor = Color.Red)
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_customColor")
+    }
+
+    @Test
+    fun navigationDrawerItem_oneLine() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_oneLine")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_focused() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+            .onChild()
+            .requestFocus()
+        rule.waitForIdle()
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focused")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_disabled() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    enabled = false,
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_disabled")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_focusedDisabled() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    enabled = false,
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+            .onChild()
+            .requestFocus()
+        rule.waitForIdle()
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focusedDisabled")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_selected() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = true,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_selected")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_focusedSelected() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = true,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+            .onChild()
+            .requestFocus()
+        rule.waitForIdle()
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_focusedSelected")
+    }
+
+    @Test
+    fun navigationDrawerItem_inactive() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope(false) {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_inactive")
+    }
+
+    @Test
+    fun navigationDrawerItem_inactive_selected() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope(false) {
+                NavigationDrawerItem(
+                    selected = true,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_inactive_selected")
+    }
+
+    @Test
+    fun navigationDrawerItem_twoLine_withTrailingContent() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = {
+                        Icon(
+                            imageVector = Icons.Filled.Favorite,
+                            contentDescription = null,
+                            modifier = Modifier.size(NavigationDrawerItemDefaults.IconSize)
+                        )
+                    },
+                    supportingContent = { Text("You like this") },
+                    trailingContent = {
+                        NavigationDrawerItemDefaults.TrailingBadge("NEW")
+                    }
+                ) {
+                    Text("Favourite")
+                }
+            }
+        }
+
+        assertAgainstGolden("navigationDrawerItem_${scheme.name}_twoLine_withTrailingContent")
+    }
+
+    private fun assertAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(NavigationDrawerItemWrapperTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+
+    // Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
+    // This makes sure that the default method name and the initial Scuba image generated
+    // name is as expected.
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            ColorSchemeWrapper("lightTheme", lightColorScheme()),
+            ColorSchemeWrapper("darkTheme", darkColorScheme()),
+        )
+    }
+
+    class ColorSchemeWrapper constructor(val name: String, val colorScheme: ColorScheme) {
+        override fun toString(): String {
+            return name
+        }
+    }
+
+    @Composable
+    private fun DrawerScope(
+        doesNavigationDrawerHaveFocus: Boolean = true,
+        content: @Composable NavigationDrawerScope.() -> Unit
+    ) {
+        Box(wrapperModifier) {
+            NavigationDrawerScopeImpl(doesNavigationDrawerHaveFocus).apply {
+                content()
+            }
+        }
+    }
+}
+
+private const val NavigationDrawerItemWrapperTag = "navigationDrawerItem_wrapper"
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt
new file mode 100644
index 0000000..74d9ee4
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerItemTest.kt
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.width
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(
+    ExperimentalTestApi::class,
+    ExperimentalTvMaterial3Api::class
+)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class NavigationDrawerItemTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun navigationDrawerItem_findByTagAndClick() {
+        var counter = 0
+        val onClick: () -> Unit = { ++counter }
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = onClick,
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        rule.runOnIdle {
+            Truth.assertThat(counter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun navigationDrawerItem_clickIsIndependentBetweenItems() {
+        var openItemClickCounter = 0
+        val openItemOnClick: () -> Unit = { ++openItemClickCounter }
+        val openItemTag = "OpenItem"
+
+        var closeItemClickCounter = 0
+        val closeItemOnClick: () -> Unit = { ++closeItemClickCounter }
+        val closeItemTag = "CloseItem"
+
+        rule.setContent {
+            DrawerScope {
+                Column {
+                    NavigationDrawerItem(
+                        selected = false,
+                        onClick = openItemOnClick,
+                        leadingContent = { },
+                        modifier = Modifier.testTag(openItemTag),
+                    ) {
+                        Text(text = "Test Text")
+                    }
+                    NavigationDrawerItem(
+                        selected = false,
+                        onClick = closeItemOnClick,
+                        leadingContent = { },
+                        modifier = Modifier.testTag(closeItemTag),
+                    ) {
+                        Text(text = "Test Text")
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(openItemTag)
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+
+        rule.runOnIdle {
+            Truth.assertThat(openItemClickCounter).isEqualTo(1)
+            Truth.assertThat(closeItemClickCounter).isEqualTo(0)
+        }
+
+        rule.onNodeWithTag(closeItemTag)
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+
+        rule.runOnIdle {
+            Truth.assertThat(openItemClickCounter).isEqualTo(1)
+            Truth.assertThat(closeItemClickCounter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun navigationDrawerItem_longClickAction() {
+        var counter = 0
+        val onLongClick: () -> Unit = { ++counter }
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = { },
+                    onLongClick = onLongClick,
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .requestFocus()
+            .performLongKeyPress(rule, Key.DirectionCenter)
+        rule.runOnIdle {
+            Truth.assertThat(counter).isEqualTo(1)
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .requestFocus()
+            .performLongKeyPress(rule, Key.DirectionCenter, count = 2)
+        rule.runOnIdle {
+            Truth.assertThat(counter).isEqualTo(3)
+        }
+    }
+
+    @Test
+    fun navigationDrawerItem_findByTagAndStateChangeCheck() {
+        var checkedState by mutableStateOf(true)
+        val onClick: () -> Unit = { checkedState = !checkedState }
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = checkedState,
+                    onClick = onClick,
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        rule.runOnIdle {
+            Truth.assertThat(!checkedState)
+        }
+    }
+
+    @Test
+    fun navigationDrawerItem_trailingContentPadding() {
+        val testTrailingContentTag = "TrailingIconTag"
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    trailingContent = {
+                        Box(
+                            modifier = Modifier
+                                .size(NavigationDrawerItemDefaults.IconSize)
+                                .background(Color.Red)
+                                .testTag(testTrailingContentTag)
+                        )
+                    },
+                    leadingContent = { },
+                    modifier = Modifier
+                        .testTag(NavigationDrawerItemTag)
+                        .border(1.dp, Color.Blue),
+                ) {
+                    Text(
+                        text = "Test Text",
+                        modifier = Modifier
+                            .testTag(NavigationDrawerItemTextTag)
+                            .fillMaxWidth()
+                    )
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        val itemBounds = rule.onNodeWithTag(NavigationDrawerItemTag).getUnclippedBoundsInRoot()
+        val textBounds = rule.onNodeWithTag(
+            NavigationDrawerItemTextTag,
+            useUnmergedTree = true
+        ).getUnclippedBoundsInRoot()
+        val trailingContentBounds = rule
+            .onNodeWithTag(testTrailingContentTag, useUnmergedTree = true)
+            .getUnclippedBoundsInRoot()
+
+        (itemBounds.bottom - trailingContentBounds.bottom).assertIsEqualTo(
+            16.dp,
+            "padding between the bottom of the trailing content and the bottom of the nav " +
+                "drawer item"
+        )
+
+        (itemBounds.right - trailingContentBounds.right).assertIsEqualTo(
+            16.dp,
+            "padding between the end of the trailing content and the end of the nav drawer item"
+        )
+
+        (trailingContentBounds.left - textBounds.right).assertIsEqualTo(
+            8.dp,
+            "padding between the start of the trailing content and the end of the text."
+        )
+    }
+
+    @Test
+    fun navigationDrawerItem_semantics() {
+        var selected by mutableStateOf(false)
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = selected,
+                    onClick = { selected = !selected },
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Selected))
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, false))
+            .requestFocus()
+            .assertIsEnabled()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, true))
+        Truth.assertThat(selected).isEqualTo(true)
+    }
+
+    @Test
+    fun navigationDrawerItem_longClickSemantics() {
+        var selected by mutableStateOf(false)
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = selected,
+                    onClick = {},
+                    onLongClick = { selected = !selected },
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.OnLongClick))
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Selected))
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, false))
+            .requestFocus()
+            .assertIsEnabled()
+            .performLongKeyPress(rule, Key.DirectionCenter)
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Selected, true))
+        Truth.assertThat(selected).isEqualTo(true)
+    }
+
+    @Test
+    fun navigationDrawerItem_disabledSemantics() {
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    enabled = false,
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertIsNotEnabled()
+    }
+
+    @Test
+    fun navigationDrawerItem_canBeDisabled() {
+        rule.setContent {
+            var enabled by remember { mutableStateOf(true) }
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = { enabled = false },
+                    leadingContent = { },
+                    enabled = enabled,
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            // Confirm the button starts off enabled, with a click action
+            .assertHasClickAction()
+            .assertIsEnabled()
+            .requestFocus()
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            // Then confirm it's disabled with click action after clicking it
+            .assertHasClickAction()
+            .assertIsNotEnabled()
+    }
+
+    @Test
+    fun navigationDrawerItem_oneLineHeight() {
+        val expectedHeightNoIcon = 56.dp
+
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = {},
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(NavigationDrawerItemTag).assertHeightIsEqualTo(expectedHeightNoIcon)
+    }
+
+    @Test
+    fun navigationDrawerItem_width() {
+        rule.setContent {
+            DrawerScope {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = { },
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertWidthIsEqualTo(rule.onRoot().getUnclippedBoundsInRoot().width)
+    }
+
+    @Test
+    fun navigationDrawerItem_widthInInactiveState() {
+        rule.setContent {
+            DrawerScope(false) {
+                NavigationDrawerItem(
+                    selected = false,
+                    onClick = { },
+                    leadingContent = { },
+                    modifier = Modifier.testTag(NavigationDrawerItemTag),
+                ) {
+                    Text(text = "Test Text")
+                }
+            }
+        }
+        rule.onNodeWithTag(NavigationDrawerItemTag)
+            .assertWidthIsEqualTo(56.dp)
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun DrawerScope(
+    isActivated: Boolean = true,
+    content: @Composable NavigationDrawerScope.() -> Unit
+) {
+    Box {
+        NavigationDrawerScopeImpl(isActivated).apply {
+            content()
+        }
+    }
+}
+
+private const val NavigationDrawerItemTag = "NavigationDrawerItem"
+private const val NavigationDrawerItemTextTag = "NavigationDrawerItemText"
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
index 049ddf1..320fe9e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
@@ -79,7 +79,7 @@
 @ExperimentalTvMaterial3Api
 @Composable
 fun ModalNavigationDrawer(
-    drawerContent: @Composable (DrawerValue) -> Unit,
+    drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,
     modifier: Modifier = Modifier,
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
     scrimBrush: Brush = SolidColor(LocalColorScheme.current.scrim.copy(alpha = 0.5f)),
@@ -155,7 +155,7 @@
 @ExperimentalTvMaterial3Api
 @Composable
 fun NavigationDrawer(
-    drawerContent: @Composable (DrawerValue) -> Unit,
+    drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,
     modifier: Modifier = Modifier,
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
     content: @Composable () -> Unit
@@ -237,7 +237,7 @@
     modifier: Modifier = Modifier,
     drawerState: DrawerState = remember { DrawerState() },
     sizeAnimationFinishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null,
-    content: @Composable (DrawerValue) -> Unit
+    content: @Composable NavigationDrawerScope.(DrawerValue) -> Unit
 ) {
     // indicates that the drawer has been set to its initial state and has grabbed focus if
     // necessary. Controls whether focus is used to decide the state of the drawer going forward.
@@ -269,7 +269,11 @@
             }
             .focusGroup()
 
-    Box(modifier = internalModifier) { content.invoke(drawerState.currentValue) }
+    Box(modifier = internalModifier) {
+        NavigationDrawerScopeImpl(drawerState.currentValue == DrawerValue.Open).apply {
+            content(drawerState.currentValue)
+        }
+    }
 }
 
 private const val ClosedDrawerWidth = 80
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
new file mode 100644
index 0000000..cc97ac8
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItem.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.tv.material3
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.unit.Dp
+
+/**
+ * TV Material Design navigation drawer item.
+ *
+ * A [NavigationDrawerItem] represents a destination within drawers, either [NavigationDrawer] or
+ * [ModalNavigationDrawer]
+ *
+ * @sample androidx.tv.samples.SampleNavigationDrawer
+ * @sample androidx.tv.samples.SampleModalNavigationDrawerWithSolidScrim
+ * @sample androidx.tv.samples.SampleModalNavigationDrawerWithGradientScrim
+ *
+ * @param selected defines whether this composable is selected or not
+ * @param onClick called when this composable is clicked
+ * @param leadingContent the leading content of the list item
+ * @param modifier to be applied to the list item
+ * @param enabled controls the enabled state of this composable. When `false`, this component will
+ * not respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services
+ * @param onLongClick called when this composable is long clicked (long-pressed)
+ * @param supportingContent the content displayed below the headline content
+ * @param trailingContent the trailing meta badge or icon
+ * @param tonalElevation the tonal elevation of this composable
+ * @param shape defines the shape of Composable's container in different interaction states
+ * @param colors defines the background and content colors used in the composable
+ * for different interaction states
+ * @param scale defines the size of the composable relative to its original size in
+ * different interaction states
+ * @param border defines a border around the composable in different interaction states
+ * @param glow defines a shadow to be shown behind the composable for different interaction states
+ * @param interactionSource the [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this component. You can create and pass in your own [remember]ed instance
+ * to observe [Interaction]s and customize the appearance / behavior of this composable in different
+ * states
+ * @param content main content of this composable
+ */
+@ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
+@Composable
+fun NavigationDrawerScope.NavigationDrawerItem(
+    selected: Boolean,
+    onClick: () -> Unit,
+    leadingContent: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    onLongClick: (() -> Unit)? = null,
+    supportingContent: (@Composable () -> Unit)? = null,
+    trailingContent: (@Composable () -> Unit)? = null,
+    tonalElevation: Dp = NavigationDrawerItemDefaults.NavigationDrawerItemElevation,
+    shape: NavigationDrawerItemShape = NavigationDrawerItemDefaults.shape(),
+    colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(),
+    scale: NavigationDrawerItemScale = NavigationDrawerItemScale.None,
+    border: NavigationDrawerItemBorder = NavigationDrawerItemDefaults.border(),
+    glow: NavigationDrawerItemGlow = NavigationDrawerItemDefaults.glow(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    content: @Composable () -> Unit,
+) {
+    val animatedWidth by animateDpAsState(
+        targetValue = if (doesNavigationDrawerHaveFocus) {
+            NavigationDrawerItemDefaults.ExpandedDrawerItemWidth
+        } else {
+            NavigationDrawerItemDefaults.CollapsedDrawerItemWidth
+        },
+        label = "NavigationDrawerItem width open/closed state of the drawer item"
+    )
+    val navDrawerItemHeight = if (supportingContent == null) {
+        NavigationDrawerItemDefaults.ContainerHeightOneLine
+    } else {
+        NavigationDrawerItemDefaults.ContainerHeightTwoLine
+    }
+    ListItem(
+        selected = selected,
+        onClick = onClick,
+        headlineContent = {
+            AnimatedVisibility(
+                visible = doesNavigationDrawerHaveFocus,
+                enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+                exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+            ) {
+                content()
+            }
+        },
+        leadingContent = {
+            Box(Modifier.size(NavigationDrawerItemDefaults.IconSize)) {
+                leadingContent()
+            }
+        },
+        trailingContent = trailingContent?.let {
+            {
+                AnimatedVisibility(
+                    visible = doesNavigationDrawerHaveFocus,
+                    enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+                    exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+                ) {
+                    it()
+                }
+            }
+        },
+        supportingContent = supportingContent?.let {
+            {
+                AnimatedVisibility(
+                    visible = doesNavigationDrawerHaveFocus,
+                    enter = NavigationDrawerItemDefaults.ContentAnimationEnter,
+                    exit = NavigationDrawerItemDefaults.ContentAnimationExit,
+                ) {
+                    it()
+                }
+            }
+        },
+        modifier = modifier
+            .layout { measurable, constraints ->
+                val width = animatedWidth.roundToPx()
+                val height = navDrawerItemHeight.roundToPx()
+                val placeable = measurable.measure(
+                    constraints.copy(
+                        minWidth = width,
+                        maxWidth = width,
+                        minHeight = height,
+                        maxHeight = height,
+                    )
+                )
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
+            },
+        enabled = enabled,
+        onLongClick = onLongClick,
+        tonalElevation = tonalElevation,
+        shape = shape.toToggleableListItemShape(),
+        colors = colors.toToggleableListItemColors(doesNavigationDrawerHaveFocus),
+        scale = scale.toToggleableListItemScale(),
+        border = border.toToggleableListItemBorder(),
+        glow = glow.toToggleableListItemGlow(),
+        interactionSource = interactionSource,
+    )
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemShape.toToggleableListItemShape() =
+    ListItemDefaults.shape(
+        shape = shape,
+        focusedShape = focusedShape,
+        pressedShape = pressedShape,
+        selectedShape = selectedShape,
+        disabledShape = disabledShape,
+        focusedSelectedShape = focusedSelectedShape,
+        focusedDisabledShape = focusedDisabledShape,
+        pressedSelectedShape = pressedSelectedShape,
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemColors.toToggleableListItemColors(
+    doesNavigationDrawerHaveFocus: Boolean
+) =
+    ListItemDefaults.colors(
+        containerColor = containerColor,
+        contentColor = if (doesNavigationDrawerHaveFocus) contentColor else inactiveContentColor,
+        focusedContainerColor = focusedContainerColor,
+        focusedContentColor = focusedContentColor,
+        pressedContainerColor = pressedContainerColor,
+        pressedContentColor = pressedContentColor,
+        selectedContainerColor = selectedContainerColor,
+        selectedContentColor = selectedContentColor,
+        disabledContainerColor = disabledContainerColor,
+        disabledContentColor =
+        if (doesNavigationDrawerHaveFocus) disabledContentColor else disabledInactiveContentColor,
+        focusedSelectedContainerColor = focusedSelectedContainerColor,
+        focusedSelectedContentColor = focusedSelectedContentColor,
+        pressedSelectedContainerColor = pressedSelectedContainerColor,
+        pressedSelectedContentColor = pressedSelectedContentColor,
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemScale.toToggleableListItemScale() =
+    ListItemDefaults.scale(
+        scale = scale,
+        focusedScale = focusedScale,
+        pressedScale = pressedScale,
+        selectedScale = selectedScale,
+        disabledScale = disabledScale,
+        focusedSelectedScale = focusedSelectedScale,
+        focusedDisabledScale = focusedDisabledScale,
+        pressedSelectedScale = pressedSelectedScale,
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemBorder.toToggleableListItemBorder() =
+    ListItemDefaults.border(
+        border = border,
+        focusedBorder = focusedBorder,
+        pressedBorder = pressedBorder,
+        selectedBorder = selectedBorder,
+        disabledBorder = disabledBorder,
+        focusedSelectedBorder = focusedSelectedBorder,
+        focusedDisabledBorder = focusedDisabledBorder,
+        pressedSelectedBorder = pressedSelectedBorder,
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+private fun NavigationDrawerItemGlow.toToggleableListItemGlow() =
+    ListItemDefaults.glow(
+        glow = glow,
+        focusedGlow = focusedGlow,
+        pressedGlow = pressedGlow,
+        selectedGlow = selectedGlow,
+        focusedSelectedGlow = focusedSelectedGlow,
+        pressedSelectedGlow = pressedSelectedGlow,
+    )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
index 44de75e..debfd6f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemDefaults.kt
@@ -38,27 +38,27 @@
 import androidx.tv.material3.tokens.Elevation
 
 /**
- * Contains the default values used by selectable NavigationDrawerItem
+ * Contains the default values used by selectable [NavigationDrawerItem]
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
 object NavigationDrawerItemDefaults {
     /**
-     * The default Icon size used by NavigationDrawerItem
+     * The default Icon size used by [NavigationDrawerItem]
      */
     val IconSize = 24.dp
 
     /**
-     * The size of the NavigationDrawerItem when the drawer is collapsed
+     * The size of the [NavigationDrawerItem] when the drawer is collapsed
      */
     val CollapsedDrawerItemWidth = 56.dp
 
     /**
-     * The size of the NavigationDrawerItem when the drawer is expanded
+     * The size of the [NavigationDrawerItem] when the drawer is expanded
      */
     val ExpandedDrawerItemWidth = 256.dp
 
     /**
-     * The default content padding [PaddingValues] used by NavigationDrawerItem when the drawer
+     * The default content padding [PaddingValues] used by [NavigationDrawerItem] when the drawer
      * is expanded
      */
 
@@ -66,7 +66,7 @@
     val ContainerHeightTwoLine = 64.dp
 
     /**
-     * The default elevation used by NavigationDrawerItem
+     * The default elevation used by [NavigationDrawerItem]
      */
     val NavigationDrawerItemElevation = Elevation.Level0
 
@@ -81,7 +81,7 @@
     val ContentAnimationExit = fadeOut() + slideOut { IntOffset(0, 0) }
 
     /**
-     * Default border used by NavigationDrawerItem
+     * Default border used by [NavigationDrawerItem]
      */
     val DefaultBorder
         @ReadOnlyComposable
@@ -93,28 +93,28 @@
         )
 
     /**
-     * The default container color used by NavigationDrawerItem's trailing badge
+     * The default container color used by [NavigationDrawerItem]'s trailing badge
      */
     val TrailingBadgeContainerColor
         @ReadOnlyComposable
         @Composable get() = MaterialTheme.colorScheme.tertiary
 
     /**
-     * The default text style used by NavigationDrawerItem's trailing badge
+     * The default text style used by [NavigationDrawerItem]'s trailing badge
      */
     val TrailingBadgeTextStyle
         @ReadOnlyComposable
         @Composable get() = MaterialTheme.typography.labelSmall
 
     /**
-     * The default content color used by NavigationDrawerItem's trailing badge
+     * The default content color used by [NavigationDrawerItem]'s trailing badge
      */
     val TrailingBadgeContentColor
         @ReadOnlyComposable
         @Composable get() = MaterialTheme.colorScheme.onTertiary
 
     /**
-     * Creates a trailing badge for NavigationDrawerItem
+     * Creates a trailing badge for [NavigationDrawerItem]
      */
     @Composable
     fun TrailingBadge(
@@ -138,18 +138,18 @@
 
     /**
      * Creates a [NavigationDrawerItemShape] that represents the default container shapes
-     * used in a selectable NavigationDrawerItem
+     * used in a selectable [NavigationDrawerItem]
      *
-     * @param shape the default shape used when the NavigationDrawerItem is enabled
-     * @param focusedShape the shape used when the NavigationDrawerItem is enabled and focused
-     * @param pressedShape the shape used when the NavigationDrawerItem is enabled and pressed
-     * @param selectedShape the shape used when the NavigationDrawerItem is enabled and selected
-     * @param disabledShape the shape used when the NavigationDrawerItem is not enabled
-     * @param focusedSelectedShape the shape used when the NavigationDrawerItem is enabled,
+     * @param shape the default shape used when the [NavigationDrawerItem] is enabled
+     * @param focusedShape the shape used when the [NavigationDrawerItem] is enabled and focused
+     * @param pressedShape the shape used when the [NavigationDrawerItem] is enabled and pressed
+     * @param selectedShape the shape used when the [NavigationDrawerItem] is enabled and selected
+     * @param disabledShape the shape used when the [NavigationDrawerItem] is not enabled
+     * @param focusedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
      * focused and selected
-     * @param focusedDisabledShape the shape used when the NavigationDrawerItem is not enabled
+     * @param focusedDisabledShape the shape used when the [NavigationDrawerItem] is not enabled
      * and focused
-     * @param pressedSelectedShape the shape used when the NavigationDrawerItem is enabled,
+     * @param pressedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
      * pressed and selected
      */
     fun shape(
@@ -174,38 +174,38 @@
 
     /**
      * Creates a [NavigationDrawerItemColors] that represents the default container &
-     * content colors used in a selectable NavigationDrawerItem
+     * content colors used in a selectable [NavigationDrawerItem]
      *
-     * @param containerColor the default container color used when the NavigationDrawerItem is
+     * @param containerColor the default container color used when the [NavigationDrawerItem] is
      * enabled
-     * @param contentColor the default content color used when the NavigationDrawerItem is enabled
+     * @param contentColor the default content color used when the [NavigationDrawerItem] is enabled
      * @param inactiveContentColor the content color used when none of the navigation items have
      * focus
-     * @param focusedContainerColor the container color used when the NavigationDrawerItem is
+     * @param focusedContainerColor the container color used when the [NavigationDrawerItem] is
      * enabled and focused
-     * @param focusedContentColor the content color used when the NavigationDrawerItem is enabled
+     * @param focusedContentColor the content color used when the [NavigationDrawerItem] is enabled
      * and focused
-     * @param pressedContainerColor the container color used when the NavigationDrawerItem is
+     * @param pressedContainerColor the container color used when the [NavigationDrawerItem] is
      * enabled and pressed
-     * @param pressedContentColor the content color used when the NavigationDrawerItem is enabled
+     * @param pressedContentColor the content color used when the [NavigationDrawerItem] is enabled
      * and pressed
-     * @param selectedContainerColor the container color used when the NavigationDrawerItem is
+     * @param selectedContainerColor the container color used when the [NavigationDrawerItem] is
      * enabled and selected
-     * @param selectedContentColor the content color used when the NavigationDrawerItem is
+     * @param selectedContentColor the content color used when the [NavigationDrawerItem] is
      * enabled and selected
-     * @param disabledContainerColor the container color used when the NavigationDrawerItem is
+     * @param disabledContainerColor the container color used when the [NavigationDrawerItem] is
      * not enabled
-     * @param disabledContentColor the content color used when the NavigationDrawerItem is not
+     * @param disabledContentColor the content color used when the [NavigationDrawerItem] is not
      * enabled
      * @param disabledInactiveContentColor the content color used when none of the navigation items
      * have focus and this item is disabled
      * @param focusedSelectedContainerColor the container color used when the
      * NavigationDrawerItem is enabled, focused and selected
-     * @param focusedSelectedContentColor the content color used when the NavigationDrawerItem
+     * @param focusedSelectedContentColor the content color used when the [NavigationDrawerItem]
      * is enabled, focused and selected
      * @param pressedSelectedContainerColor the container color used when the
-     * NavigationDrawerItem is enabled, pressed and selected
-     * @param pressedSelectedContentColor the content color used when the NavigationDrawerItem is
+     * [NavigationDrawerItem] is enabled, pressed and selected
+     * @param pressedSelectedContentColor the content color used when the [NavigationDrawerItem] is
      * enabled, pressed and selected
      */
     @ReadOnlyComposable
@@ -249,22 +249,22 @@
 
     /**
      * Creates a [NavigationDrawerItemScale] that represents the default scales used in a
-     * selectable NavigationDrawerItem
+     * selectable [NavigationDrawerItem]
      *
      * scales are used to modify the size of a composable in different [Interaction] states
      * e.g. `1f` (original) in default state, `1.2f` (scaled up) in focused state, `0.8f` (scaled
      * down) in pressed state, etc.
      *
-     * @param scale the scale used when the NavigationDrawerItem is enabled
-     * @param focusedScale the scale used when the NavigationDrawerItem is enabled and focused
-     * @param pressedScale the scale used when the NavigationDrawerItem is enabled and pressed
-     * @param selectedScale the scale used when the NavigationDrawerItem is enabled and selected
-     * @param disabledScale the scale used when the NavigationDrawerItem is not enabled
-     * @param focusedSelectedScale the scale used when the NavigationDrawerItem is enabled,
+     * @param scale the scale used when the [NavigationDrawerItem] is enabled
+     * @param focusedScale the scale used when the [NavigationDrawerItem] is enabled and focused
+     * @param pressedScale the scale used when the [NavigationDrawerItem] is enabled and pressed
+     * @param selectedScale the scale used when the [NavigationDrawerItem] is enabled and selected
+     * @param disabledScale the scale used when the [NavigationDrawerItem] is not enabled
+     * @param focusedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
      * focused and selected
-     * @param focusedDisabledScale the scale used when the NavigationDrawerItem is not enabled and
+     * @param focusedDisabledScale the scale used when the [NavigationDrawerItem] is not enabled and
      * focused
-     * @param pressedSelectedScale the scale used when the NavigationDrawerItem is enabled,
+     * @param pressedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
      * pressed and selected
      */
     fun scale(
@@ -289,19 +289,19 @@
 
     /**
      * Creates a [NavigationDrawerItemBorder] that represents the default [Border]s
-     * applied on a selectable NavigationDrawerItem in different [Interaction] states
+     * applied on a selectable [NavigationDrawerItem] in different [Interaction] states
      *
-     * @param border the default [Border] used when the NavigationDrawerItem is enabled
-     * @param focusedBorder the [Border] used when the NavigationDrawerItem is enabled and focused
-     * @param pressedBorder the [Border] used when the NavigationDrawerItem is enabled and pressed
-     * @param selectedBorder the [Border] used when the NavigationDrawerItem is enabled and
+     * @param border the default [Border] used when the [NavigationDrawerItem] is enabled
+     * @param focusedBorder the [Border] used when the [NavigationDrawerItem] is enabled and focused
+     * @param pressedBorder the [Border] used when the [NavigationDrawerItem] is enabled and pressed
+     * @param selectedBorder the [Border] used when the [NavigationDrawerItem] is enabled and
      * selected
-     * @param disabledBorder the [Border] used when the NavigationDrawerItem is not enabled
-     * @param focusedSelectedBorder the [Border] used when the NavigationDrawerItem is enabled,
+     * @param disabledBorder the [Border] used when the [NavigationDrawerItem] is not enabled
+     * @param focusedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
      * focused and selected
-     * @param focusedDisabledBorder the [Border] used when the NavigationDrawerItem is not
+     * @param focusedDisabledBorder the [Border] used when the [NavigationDrawerItem] is not
      * enabled and focused
-     * @param pressedSelectedBorder the [Border] used when the NavigationDrawerItem is enabled,
+     * @param pressedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
      * pressed and selected
      */
     @ReadOnlyComposable
@@ -328,16 +328,16 @@
 
     /**
      * Creates a [NavigationDrawerItemGlow] that represents the default [Glow]s used in a
-     * selectable NavigationDrawerItem
+     * selectable [NavigationDrawerItem]
      *
-     * @param glow the [Glow] used when the NavigationDrawerItem is enabled, and has no other
+     * @param glow the [Glow] used when the [NavigationDrawerItem] is enabled, and has no other
      * [Interaction]s
-     * @param focusedGlow the [Glow] used when the NavigationDrawerItem is enabled and focused
-     * @param pressedGlow the [Glow] used when the NavigationDrawerItem is enabled and pressed
-     * @param selectedGlow the [Glow] used when the NavigationDrawerItem is enabled and selected
-     * @param focusedSelectedGlow the [Glow] used when the NavigationDrawerItem is enabled,
+     * @param focusedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and focused
+     * @param pressedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and pressed
+     * @param selectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and selected
+     * @param focusedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
      * focused and selected
-     * @param pressedSelectedGlow the [Glow] used when the NavigationDrawerItem is enabled,
+     * @param pressedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
      * pressed and selected
      */
     fun glow(
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
index 027ac27..f08e865 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerItemStyles.kt
@@ -23,21 +23,21 @@
 import androidx.compose.ui.graphics.Shape
 
 /**
- * Defines [Shape] for all TV [Indication] states of a NavigationDrawerItem
+ * Defines [Shape] for all TV [Indication] states of a [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary shape. See NavigationDrawerItemDefaults.shape
- * for the default shape used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary shape. See [NavigationDrawerItemDefaults.shape]
+ * for the default shape used in a [NavigationDrawerItem]
  *
- * @param shape the default shape used when the NavigationDrawerItem is enabled
- * @param focusedShape the shape used when the NavigationDrawerItem is enabled and focused
- * @param pressedShape the shape used when the NavigationDrawerItem is enabled and pressed
- * @param selectedShape the shape used when the NavigationDrawerItem is enabled and selected
- * @param disabledShape the shape used when the NavigationDrawerItem is not enabled
- * @param focusedSelectedShape the shape used when the NavigationDrawerItem is enabled,
+ * @param shape the default shape used when the [NavigationDrawerItem] is enabled
+ * @param focusedShape the shape used when the [NavigationDrawerItem] is enabled and focused
+ * @param pressedShape the shape used when the [NavigationDrawerItem] is enabled and pressed
+ * @param selectedShape the shape used when the [NavigationDrawerItem] is enabled and selected
+ * @param disabledShape the shape used when the [NavigationDrawerItem] is not enabled
+ * @param focusedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
  * focused and selected
- * @param focusedDisabledShape the shape used when the NavigationDrawerItem is not enabled
+ * @param focusedDisabledShape the shape used when the [NavigationDrawerItem] is not enabled
  * and focused
- * @param pressedSelectedShape the shape used when the NavigationDrawerItem is enabled,
+ * @param pressedSelectedShape the shape used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -95,41 +95,41 @@
 
 /**
  * Defines container & content color [Color] for all TV [Indication] states of a
- * NavigationDrawerItem
+ * [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary colors. See NavigationDrawerItemDefaults.colors
- * for the default colors used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary colors. See [NavigationDrawerItemDefaults.colors]
+ * for the default colors used in a [NavigationDrawerItem]
  *
- * @param containerColor the default container color used when the NavigationDrawerItem is
+ * @param containerColor the default container color used when the [NavigationDrawerItem] is
  * enabled
- * @param contentColor the default content color used when the NavigationDrawerItem is enabled
+ * @param contentColor the default content color used when the [NavigationDrawerItem] is enabled
  * @param inactiveContentColor the content color used when none of the navigation items have
  * focus
- * @param focusedContainerColor the container color used when the NavigationDrawerItem is
+ * @param focusedContainerColor the container color used when the [NavigationDrawerItem] is
  * enabled and focused
- * @param focusedContentColor the content color used when the NavigationDrawerItem is enabled
+ * @param focusedContentColor the content color used when the [NavigationDrawerItem] is enabled
  * and focused
- * @param pressedContainerColor the container color used when the NavigationDrawerItem is
+ * @param pressedContainerColor the container color used when the [NavigationDrawerItem] is
  * enabled and pressed
- * @param pressedContentColor the content color used when the NavigationDrawerItem is enabled
+ * @param pressedContentColor the content color used when the [NavigationDrawerItem] is enabled
  * and pressed
- * @param selectedContainerColor the container color used when the NavigationDrawerItem is
+ * @param selectedContainerColor the container color used when the [NavigationDrawerItem] is
  * enabled and selected
- * @param selectedContentColor the content color used when the NavigationDrawerItem is
+ * @param selectedContentColor the content color used when the [NavigationDrawerItem] is
  * enabled and selected
- * @param disabledContainerColor the container color used when the NavigationDrawerItem is
+ * @param disabledContainerColor the container color used when the [NavigationDrawerItem] is
  * not enabled
- * @param disabledContentColor the content color used when the NavigationDrawerItem is not
+ * @param disabledContentColor the content color used when the [NavigationDrawerItem] is not
  * enabled
  * @param disabledInactiveContentColor the content color used when none of the navigation items
  * have focus and this item is disabled
  * @param focusedSelectedContainerColor the container color used when the
- * NavigationDrawerItem is enabled, focused and selected
- * @param focusedSelectedContentColor the content color used when the NavigationDrawerItem
+ * [NavigationDrawerItem] is enabled, focused and selected
+ * @param focusedSelectedContentColor the content color used when the [NavigationDrawerItem]
  * is enabled, focused and selected
  * @param pressedSelectedContainerColor the container color used when the
- * NavigationDrawerItem is enabled, pressed and selected
- * @param pressedSelectedContentColor the content color used when the NavigationDrawerItem is
+ * [NavigationDrawerItem] is enabled, pressed and selected
+ * @param pressedSelectedContentColor the content color used when the [NavigationDrawerItem] is
  * enabled, pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -215,21 +215,21 @@
 }
 
 /**
- * Defines the scale for all TV [Indication] states of a NavigationDrawerItem
+ * Defines the scale for all TV [Indication] states of a [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary scale. See NavigationDrawerItemDefaults.scale
- * for the default scale used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary scale. See [NavigationDrawerItemDefaults.scale]
+ * for the default scale used in a [NavigationDrawerItem]
  *
- * @param scale the scale used when the NavigationDrawerItem is enabled
- * @param focusedScale the scale used when the NavigationDrawerItem is enabled and focused
- * @param pressedScale the scale used when the NavigationDrawerItem is enabled and pressed
- * @param selectedScale the scale used when the NavigationDrawerItem is enabled and selected
- * @param disabledScale the scale used when the NavigationDrawerItem is not enabled
- * @param focusedSelectedScale the scale used when the NavigationDrawerItem is enabled,
+ * @param scale the scale used when the [NavigationDrawerItem] is enabled
+ * @param focusedScale the scale used when the [NavigationDrawerItem] is enabled and focused
+ * @param pressedScale the scale used when the [NavigationDrawerItem] is enabled and pressed
+ * @param selectedScale the scale used when the [NavigationDrawerItem] is enabled and selected
+ * @param disabledScale the scale used when the [NavigationDrawerItem] is not enabled
+ * @param focusedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
  * focused and selected
- * @param focusedDisabledScale the scale used when the NavigationDrawerItem is not enabled and
+ * @param focusedDisabledScale the scale used when the [NavigationDrawerItem] is not enabled and
  * focused
- * @param pressedSelectedScale the scale used when the NavigationDrawerItem is enabled,
+ * @param pressedSelectedScale the scale used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -286,7 +286,7 @@
 
     companion object {
         /**
-         * Signifies the absence of a [ScaleIndication] in NavigationDrawerItem
+         * Signifies the absence of a [ScaleIndication] in [NavigationDrawerItem]
          */
         val None = NavigationDrawerItemScale(
             scale = 1f,
@@ -302,22 +302,22 @@
 }
 
 /**
- * Defines [Border] for all TV [Indication] states of a NavigationDrawerItem
+ * Defines [Border] for all TV [Indication] states of a [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary border. See NavigationDrawerItemDefaults.border
- * for the default border used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary border. See [NavigationDrawerItemDefaults.border]
+ * for the default border used in a [NavigationDrawerItem]
  *
- * @param border the default [Border] used when the NavigationDrawerItem is enabled
- * @param focusedBorder the [Border] used when the NavigationDrawerItem is enabled and focused
- * @param pressedBorder the [Border] used when the NavigationDrawerItem is enabled and pressed
- * @param selectedBorder the [Border] used when the NavigationDrawerItem is enabled and
+ * @param border the default [Border] used when the [NavigationDrawerItem] is enabled
+ * @param focusedBorder the [Border] used when the [NavigationDrawerItem] is enabled and focused
+ * @param pressedBorder the [Border] used when the [NavigationDrawerItem] is enabled and pressed
+ * @param selectedBorder the [Border] used when the [NavigationDrawerItem] is enabled and
  * selected
- * @param disabledBorder the [Border] used when the NavigationDrawerItem is not enabled
- * @param focusedSelectedBorder the [Border] used when the NavigationDrawerItem is enabled,
+ * @param disabledBorder the [Border] used when the [NavigationDrawerItem] is not enabled
+ * @param focusedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
  * focused and selected
- * @param focusedDisabledBorder the [Border] used when the NavigationDrawerItem is not
+ * @param focusedDisabledBorder the [Border] used when the [NavigationDrawerItem] is not
  * enabled and focused
- * @param pressedSelectedBorder the [Border] used when the NavigationDrawerItem is enabled,
+ * @param pressedSelectedBorder the [Border] used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -374,18 +374,18 @@
 }
 
 /**
- * Defines [Glow] for all TV [Indication] states of a NavigationDrawerItem
+ * Defines [Glow] for all TV [Indication] states of a [NavigationDrawerItem]
  *
- * @constructor create an instance with arbitrary glow. See NavigationDrawerItemDefaults.glow
- * for the default glow used in a NavigationDrawerItem
+ * @constructor create an instance with arbitrary glow. See [NavigationDrawerItemDefaults.glow]
+ * for the default glow used in a [NavigationDrawerItem]
  *
- * @param glow the [Glow] used when the NavigationDrawerItem is enabled
- * @param focusedGlow the [Glow] used when the NavigationDrawerItem is enabled and focused
- * @param pressedGlow the [Glow] used when the NavigationDrawerItem is enabled and pressed
- * @param selectedGlow the [Glow] used when the NavigationDrawerItem is enabled and selected
- * @param focusedSelectedGlow the [Glow] used when the NavigationDrawerItem is enabled,
+ * @param glow the [Glow] used when the [NavigationDrawerItem] is enabled
+ * @param focusedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and focused
+ * @param pressedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and pressed
+ * @param selectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled and selected
+ * @param focusedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
  * focused and selected
- * @param pressedSelectedGlow the [Glow] used when the NavigationDrawerItem is enabled,
+ * @param pressedSelectedGlow the [Glow] used when the [NavigationDrawerItem] is enabled,
  * pressed and selected
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
index ccdcec7..85434a3 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawerScope.kt
@@ -17,7 +17,7 @@
 package androidx.tv.material3
 
 /**
- * [NavigationDrawerScope] is used to provide the isActivated state to the NavigationDrawerItem
+ * [NavigationDrawerScope] is used to provide the isActivated state to the [NavigationDrawerItem]
  * composable
  */
 @ExperimentalTvMaterial3Api // TODO (b/263353219): Remove this before launching beta
@@ -25,10 +25,10 @@
     /**
      * Whether any item within the [NavigationDrawer] or [ModalNavigationDrawer] is focused
      */
-    val isActivated: Boolean
+    val doesNavigationDrawerHaveFocus: Boolean
 }
 
 @OptIn(ExperimentalTvMaterial3Api::class)
-internal class NavigationDrawerScopeImpl constructor(
-    override val isActivated: Boolean
+internal class NavigationDrawerScopeImpl(
+    override val doesNavigationDrawerHaveFocus: Boolean
 ) : NavigationDrawerScope
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
index 3140de6..9b8dbb5 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToRevealTest.kt
@@ -21,9 +21,13 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.TouchInjectionScope
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
@@ -301,6 +305,64 @@
         undoActionModifier = Modifier.testTag(TEST_TAG)
     )
 
+    @Test
+    fun onRightSwipe_dispatchEventsToParent() {
+        var onPreScrollDispatch = 0f
+        rule.setContent {
+            val nestedScrollConnection = remember {
+                object : NestedScrollConnection {
+                    override fun onPreScroll(
+                        available: Offset,
+                        source: NestedScrollSource
+                    ): Offset {
+                        onPreScrollDispatch = available.x
+                        return available
+                    }
+                }
+            }
+            Box(
+                modifier = Modifier.nestedScroll(nestedScrollConnection)
+            ) {
+                swipeToRevealWithDefaults(
+                    modifier = Modifier.testTag(TEST_TAG)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+
+        assert(onPreScrollDispatch > 0)
+    }
+
+    @Test
+    fun onLeftSwipe_dispatchEventsToParent() {
+        var onPreScrollDispatch = 0f
+        rule.setContent {
+            val nestedScrollConnection = remember {
+                object : NestedScrollConnection {
+                    override fun onPreScroll(
+                        available: Offset,
+                        source: NestedScrollSource
+                    ): Offset {
+                        onPreScrollDispatch = available.x
+                        return available
+                    }
+                }
+            }
+            Box(
+                modifier = Modifier.nestedScroll(nestedScrollConnection)
+            ) {
+                swipeToRevealWithDefaults(
+                    modifier = Modifier.testTag(TEST_TAG)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeLeft() }
+
+        assert(onPreScrollDispatch < 0) // Swiping left means the dispatch will be negative
+    }
+
     private fun verifyLastClickAction(
         expectedClickType: RevealActionType,
         initialRevealValue: RevealValue,
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
index 9f96a9d..5c8e950 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeableV2Test.kt
@@ -17,13 +17,22 @@
 package androidx.wear.compose.foundation
 
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.ViewConfiguration
@@ -32,15 +41,23 @@
 import androidx.compose.ui.semantics.SemanticsProperties.VerticalScrollAxisRange
 import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.TouchInjectionScope
 import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.unit.dp
 import kotlin.math.absoluteValue
 import org.junit.Rule
 import org.junit.Test
 
+internal const val CHILD_TEST_TAG = "childTestTag"
+
 // TODO(b/201009199) Some of these tests may need specific values adjusted when swipeableV2
 // supports property nested scrolling, but the tests should all still be valid.
 @OptIn(ExperimentalWearFoundationApi::class)
@@ -221,6 +238,260 @@
             .assert(SemanticsMatcher.keyNotDefined(VerticalScrollAxisRange))
     }
 
+    @Test
+    fun onSwipeLeft_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeLeft() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.x
+            }
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeRight_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeRight() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.x
+            }
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeUp_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeUp() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.y
+            },
+            orientation = Orientation.Vertical
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeDown_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeDown() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.y
+            },
+            orientation = Orientation.Vertical
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeLeft_sendsPostScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeLeft() },
+            consumePostScrollDelta = { offset ->
+                delta = offset.x
+            }
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeRight_sendsPostScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeRight() },
+            consumePostScrollDelta = { offset ->
+                delta = offset.x
+            },
+            reverseAnchors = true //  reverse anchors or else swipeable consumes whole delta
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeUp_sendsPostScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeUp() },
+            consumePostScrollDelta = { offset ->
+                delta = offset.y
+            },
+            orientation = Orientation.Vertical
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeDown_sendsPostScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeDown() },
+            consumePostScrollDelta = { offset ->
+                delta = offset.y
+            },
+            orientation = Orientation.Vertical,
+            reverseAnchors = true //  reverse anchors or else swipeable consumes whole delta
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeLeftToChild_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeLeft() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.x
+            },
+            testTag = CHILD_TEST_TAG
+        )
+
+        assert(delta < 0) {
+            "Expected delta to be negative, was $delta"
+        }
+    }
+
+    @Test
+    fun onSwipeRightToChild_sendsPreScrollEventToParent() {
+        var delta = 0f
+        rule.testSwipe(
+            touchInput = { swipeRight() },
+            consumePreScrollDelta = { offset ->
+                delta = offset.x
+            },
+            testTag = CHILD_TEST_TAG
+        )
+
+        assert(delta > 0) {
+            "Expected delta to be positive, was $delta"
+        }
+    }
+
+    private fun ComposeContentTestRule.testSwipe(
+        touchInput: TouchInjectionScope.() -> Unit,
+        consumePreScrollDelta: (Offset) -> Unit = {},
+        consumePostScrollDelta: (Offset) -> Unit = {},
+        orientation: Orientation = Orientation.Horizontal,
+        reverseAnchors: Boolean = false,
+        testTag: String = TEST_TAG
+    ) {
+        setContent {
+            val nestedScrollConnection = remember {
+                object : NestedScrollConnection {
+                    override fun onPreScroll(
+                        available: Offset,
+                        source: NestedScrollSource
+                    ): Offset {
+                        consumePreScrollDelta(available)
+                        return super.onPreScroll(available, source)
+                    }
+
+                    override fun onPostScroll(
+                        consumed: Offset,
+                        available: Offset,
+                        source: NestedScrollSource
+                    ): Offset {
+                        consumePostScrollDelta(available)
+                        return super.onPostScroll(consumed, available, source)
+                    }
+                }
+            }
+            Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
+                SwipeableContent(
+                    orientation = orientation,
+                    reverseAnchors = reverseAnchors,
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {
+                    Box(modifier = Modifier
+                        .fillMaxSize()
+                        .testTag(CHILD_TEST_TAG)
+                        .nestedScroll(remember { object : NestedScrollConnection {} })
+                        .scrollable(
+                            state = rememberScrollableState { _ ->
+                                0f // Do not consume any delta, just return it
+                            },
+                            orientation = orientation
+                        )
+                    )
+                }
+            }
+        }
+
+        onNodeWithTag(testTag).performTouchInput { touchInput() }
+    }
+
+    @Composable
+    private fun SwipeableContent(
+        modifier: Modifier = Modifier,
+        orientation: Orientation = Orientation.Horizontal,
+        reverseAnchors: Boolean = false,
+        content: @Composable BoxScope.() -> Unit = {}
+    ) {
+        // To participate as a producer of scroll events
+        val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
+        // To participate as a consumer of scroll events
+        val nestedScrollConnection = remember { object : NestedScrollConnection {} }
+        val swipeableV2State = remember {
+            SwipeableV2State(
+                initialValue = false,
+                nestedScrollDispatcher = nestedScrollDispatcher
+            )
+        }
+        val factor = if (reverseAnchors) -1 else 1
+        Box(
+            modifier = modifier
+                .fillMaxSize()
+                .nestedScroll(nestedScrollConnection)
+                .swipeableV2(
+                    swipeableV2State,
+                    orientation
+                )
+                .swipeAnchors(
+                    state = swipeableV2State,
+                    possibleValues = setOf(false, true)
+                ) { value, layoutSize ->
+                    when (value) {
+                        false -> 0f
+                        true -> factor * (
+                            if (orientation == Orientation.Horizontal) layoutSize.width.toFloat()
+                            else layoutSize.height.toFloat()
+                            )
+                    }
+                }
+                .nestedScroll(nestedScrollConnection, nestedScrollDispatcher),
+            content = content
+        )
+    }
+
     /**
      * A square [Box] has the [TEST_TAG] test tag. Touch slop is disabled to make swipe calculations
      * more exact.
diff --git a/wear/compose/compose-foundation/src/main/baseline-prof.txt b/wear/compose/compose-foundation/src/main/baseline-prof.txt
index 00329b5..edfde2d 100644
--- a/wear/compose/compose-foundation/src/main/baseline-prof.txt
+++ b/wear/compose/compose-foundation/src/main/baseline-prof.txt
@@ -30,21 +30,28 @@
 SPLandroidx/wear/compose/foundation/ExpandableItemsDefaults;->**(**)**
 HSPLandroidx/wear/compose/foundation/ExpandableKt**->**(**)**
 HSPLandroidx/wear/compose/foundation/ExpandableState;->**(**)**
-PLandroidx/wear/compose/foundation/FocusNode;->**(**)**
-HPLandroidx/wear/compose/foundation/HierarchicalFocusCoordinatorKt**->**(**)**
-PLandroidx/wear/compose/foundation/InternalMutatorMutex;->**(**)**
+SPLandroidx/wear/compose/foundation/FocusNode;->**(**)**
+HSPLandroidx/wear/compose/foundation/HierarchicalFocusCoordinatorKt**->**(**)**
+SPLandroidx/wear/compose/foundation/InternalMutatorMutex;->**(**)**
+HSPLandroidx/wear/compose/foundation/Modifiers;->**(**)**
 HSPLandroidx/wear/compose/foundation/PaddingWrapper;->**(**)**
 HSPLandroidx/wear/compose/foundation/PartialLayoutInfo;->**(**)**
 Landroidx/wear/compose/foundation/ReduceMotion;
+HSPLandroidx/wear/compose/foundation/ResourcesKt**->**(**)**
+PLandroidx/wear/compose/foundation/RevealActionType;->**(**)**
 HPLandroidx/wear/compose/foundation/RevealScopeImpl;->**(**)**
 HPLandroidx/wear/compose/foundation/RevealState;->**(**)**
 HPLandroidx/wear/compose/foundation/RevealValue;->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeAnchorsModifier;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeAnchorsModifier;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeToDismissBoxKt**->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeToDismissBoxState;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeToDismissKeys;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeToDismissValue;->**(**)**
 PLandroidx/wear/compose/foundation/SwipeToRevealDefaults;->**(**)**
 HPLandroidx/wear/compose/foundation/SwipeToRevealKt**->**(**)**
-PLandroidx/wear/compose/foundation/SwipeableV2Defaults;->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeableV2Kt**->**(**)**
-HPLandroidx/wear/compose/foundation/SwipeableV2State;->**(**)**
+SPLandroidx/wear/compose/foundation/SwipeableV2Defaults;->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeableV2Kt**->**(**)**
+HSPLandroidx/wear/compose/foundation/SwipeableV2State;->**(**)**
 SPLandroidx/wear/compose/foundation/lazy/AutoCenteringParams;->**(**)**
 HSPLandroidx/wear/compose/foundation/lazy/CombinedPaddingValues;->**(**)**
 HSPLandroidx/wear/compose/foundation/lazy/DefaultScalingLazyListItemInfo;->**(**)**
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
index b2a3c52..50f3c3b 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToReveal.kt
@@ -51,6 +51,9 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
@@ -183,6 +186,7 @@
     positionalThreshold: Density.(totalDistance: Float) -> Float,
     internal val anchors: Map<RevealValue, Float>,
     internal val coroutineScope: CoroutineScope,
+    internal val nestedScrollDispatcher: NestedScrollDispatcher
 ) {
     /**
      * [SwipeableV2State] internal instance for the state.
@@ -197,6 +201,7 @@
             )
         },
         positionalThreshold = positionalThreshold,
+        nestedScrollDispatcher = nestedScrollDispatcher
     )
 
     public var lastActionType by mutableStateOf(RevealActionType.None)
@@ -340,9 +345,10 @@
     confirmValueChange: (RevealValue) -> Boolean = { true },
     positionalThreshold: Density.(totalDistance: Float) -> Float =
         SwipeToRevealDefaults.defaultThreshold(),
-    anchors: Map<RevealValue, Float> = createAnchors()
+    anchors: Map<RevealValue, Float> = createAnchors(),
 ): RevealState {
     val coroutineScope = rememberCoroutineScope()
+    val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
     return remember(initialValue, animationSpec) {
         RevealState(
             initialValue = initialValue,
@@ -350,7 +356,8 @@
             confirmValueChange = confirmValueChange,
             positionalThreshold = positionalThreshold,
             anchors = anchors,
-            coroutineScope = coroutineScope
+            coroutineScope = coroutineScope,
+            nestedScrollDispatcher = nestedScrollDispatcher
         )
     }
 }
@@ -404,6 +411,8 @@
     content: @Composable () -> Unit
 ) {
     val revealScope = remember(state) { RevealScopeImpl(state) }
+    // A no-op NestedScrollConnection which does not consume scroll/fling events
+    val noOpNestedScrollConnection = remember { object : NestedScrollConnection {} }
     Box(
         modifier = modifier
             .swipeableV2(
@@ -421,6 +430,10 @@
                 // Multiply the anchor with -1f to get the actual swipeable anchor
                 -state.swipeAnchors[value]!! * swipeableWidth
             }
+            // NestedScrollDispatcher sends the scroll/fling events from the node to its parent
+            // and onwards including the modifier chain. Apply it in the end to let nested scroll
+            // connection applied before this modifier consume the scroll/fling events.
+            .nestedScroll(noOpNestedScrollConnection, state.nestedScrollDispatcher)
     ) {
         val swipeCompleted by remember {
             derivedStateOf { state.currentValue == RevealValue.Revealed }
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
index 784c92f..94200e9 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
@@ -39,6 +39,9 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.layout.LayoutModifier
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
@@ -55,6 +58,7 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import kotlin.math.abs
 import kotlinx.coroutines.CancellationException
@@ -126,6 +130,9 @@
         }
     }
 
+    // Update the orientation in the swipeable state
+    state.orientation = orientation
+
     return this.then(semantics).draggable(
         state = state.swipeDraggableState,
         orientation = orientation,
@@ -218,6 +225,7 @@
     internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
         SwipeableV2Defaults.PositionalThreshold,
     internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
+    private val nestedScrollDispatcher: NestedScrollDispatcher? = null
 ) {
 
     private val swipeMutex = InternalMutatorMutex()
@@ -242,6 +250,11 @@
     }
 
     /**
+     * The orientation in which the swipeable can be swiped.
+     */
+    internal var orientation = Orientation.Horizontal
+
+    /**
      * The current value of the [SwipeableV2State].
      */
     var currentValue: T by mutableStateOf(initialValue)
@@ -427,17 +440,29 @@
      * Find the closest anchor taking into account the velocity and settle at it with an animation.
      */
     suspend fun settle(velocity: Float) {
+        var availableVelocity = velocity
+        // Dispatch the velocity to parent nodes for consuming
+        nestedScrollDispatcher?.let {
+            val consumedVelocity = nestedScrollDispatcher.dispatchPreFling(
+                if (orientation == Orientation.Horizontal) {
+                    Velocity(x = velocity, y = 0f)
+                } else {
+                    Velocity(x = 0f, y = velocity)
+                }
+            )
+            availableVelocity -= (consumedVelocity.x + consumedVelocity.y)
+        }
         val previousValue = this.currentValue
         val targetValue = computeTarget(
             offset = requireOffset(),
             currentValue = previousValue,
-            velocity = velocity
+            velocity = availableVelocity
         )
         if (confirmValueChange(targetValue)) {
-            animateTo(targetValue, velocity)
+            animateTo(targetValue, availableVelocity)
         } else {
             // If the user vetoed the state change, rollback to the previous state.
-            animateTo(previousValue, velocity)
+            animateTo(previousValue, availableVelocity)
         }
     }
 
@@ -447,14 +472,41 @@
      * @return The delta the consumed by the [SwipeableV2State]
      */
     fun dispatchRawDelta(delta: Float): Float {
+        var remainingDelta = delta
+
+        // Dispatch the delta as a scroll event to parent node for consuming it
+        nestedScrollDispatcher?.let {
+            val consumedByParent = nestedScrollDispatcher.dispatchPreScroll(
+                available = offsetWithOrientation(remainingDelta),
+                source = NestedScrollSource.Drag
+            )
+            remainingDelta -= (consumedByParent.x + consumedByParent.y)
+        }
         val currentDragPosition = offset ?: 0f
-        val potentiallyConsumed = currentDragPosition + delta
+        val potentiallyConsumed = currentDragPosition + remainingDelta
         val clamped = potentiallyConsumed.coerceIn(minOffset, maxOffset)
         val deltaToConsume = clamped - currentDragPosition
         if (abs(deltaToConsume) >= 0) {
             offset = ((offset ?: 0f) + deltaToConsume).coerceIn(minOffset, maxOffset)
         }
-        return deltaToConsume
+
+        nestedScrollDispatcher?.let {
+            val consumedDelta = nestedScrollDispatcher.dispatchPostScroll(
+                consumed = offsetWithOrientation(deltaToConsume),
+                available = offsetWithOrientation(delta - deltaToConsume),
+                source = NestedScrollSource.Drag
+            )
+            remainingDelta -= (deltaToConsume + consumedDelta.x + consumedDelta.y)
+        }
+        return remainingDelta
+    }
+
+    private fun offsetWithOrientation(delta: Float): Offset {
+        return if (orientation == Orientation.Horizontal) {
+            Offset(x = delta, y = 0f)
+        } else {
+            Offset(x = 0f, y = delta)
+        }
     }
 
     private fun computeTarget(
diff --git a/wear/compose/compose-material/src/main/baseline-prof.txt b/wear/compose/compose-material/src/main/baseline-prof.txt
index de41148..8c7b94c 100644
--- a/wear/compose/compose-material/src/main/baseline-prof.txt
+++ b/wear/compose/compose-material/src/main/baseline-prof.txt
@@ -67,7 +67,7 @@
 HPLandroidx/wear/compose/material/PlaceholderKt**->**(**)**
 PLandroidx/wear/compose/material/PlaceholderModifier;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderShimmerModifier;->**(**)**
-PLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
+HPLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderState;->**(**)**
 HSPLandroidx/wear/compose/material/PositionIndicatorAlignment;->**(**)**
 HSPLandroidx/wear/compose/material/PositionIndicatorKt**->**(**)**
@@ -146,7 +146,12 @@
 HSPLandroidx/wear/compose/materialcore/IconKt**->**(**)**
 HPLandroidx/wear/compose/materialcore/RangeDefaults;->**(**)**
 PLandroidx/wear/compose/materialcore/RangeIcons;->**(**)**
+HPLandroidx/wear/compose/materialcore/RepeatableClickableKt**->**(**)**
+HSPLandroidx/wear/compose/materialcore/ResourcesKt**->**(**)**
+HPLandroidx/wear/compose/materialcore/SelectionControlsKt**->**(**)**
+PLandroidx/wear/compose/materialcore/SelectionStage;->**(**)**
 HPLandroidx/wear/compose/materialcore/SliderKt**->**(**)**
 PLandroidx/wear/compose/materialcore/StepperDefaults;->**(**)**
 HPLandroidx/wear/compose/materialcore/StepperKt**->**(**)**
 HSPLandroidx/wear/compose/materialcore/TextKt**->**(**)**
+HSPLandroidx/wear/compose/materialcore/ToggleButtonKt**->**(**)**
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index ee7dddf..a13d6d5 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -170,6 +170,9 @@
                 ComposableDemo("Swipe To Reveal - Undo") {
                     SwipeToRevealWithDifferentUndo()
                 },
+                ComposableDemo("S2R + EdgeSwipeToDismiss") { params ->
+                    SwipeToRevealWithEdgeSwipeToDismiss(params.navigateBack)
+                },
                 ComposableDemo("Material S2R Chip") {
                     SwipeToRevealChipSample()
                 },
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
index a8029d3..e37c1fc 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToRevealDemo.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.compose.integration.demos
 
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -43,12 +44,15 @@
 import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
 import androidx.wear.compose.foundation.RevealActionType
 import androidx.wear.compose.foundation.RevealValue
+import androidx.wear.compose.foundation.SwipeToDismissBox
 import androidx.wear.compose.foundation.createAnchors
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
 import androidx.wear.compose.foundation.expandableItem
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.rememberExpandableState
 import androidx.wear.compose.foundation.rememberExpandableStateMapping
 import androidx.wear.compose.foundation.rememberRevealState
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.material.AppCard
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
@@ -240,6 +244,45 @@
     }
 }
 
+@OptIn(ExperimentalWearMaterialApi::class, ExperimentalWearFoundationApi::class)
+@Composable
+fun SwipeToRevealWithEdgeSwipeToDismiss(
+    navigateBack: () -> Unit
+) {
+    val swipeToDismissBoxState = rememberSwipeToDismissBoxState()
+    SwipeToDismissBox(
+        state = swipeToDismissBoxState,
+        onDismissed = navigateBack
+    ) {
+        ScalingLazyColumn(
+            contentPadding = PaddingValues(0.dp)
+        ) {
+            repeat(5) {
+                item {
+                    SwipeToRevealChip(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .edgeSwipeToDismiss(swipeToDismissBoxState),
+                        primaryAction = SwipeToRevealDefaults.primaryAction(
+                            icon = { Icon(SwipeToRevealDefaults.Delete, "Delete") },
+                            label = { Text("Delete") }),
+                        revealState = rememberRevealState()
+                    ) {
+                        Chip(
+                            onClick = { /*TODO*/ },
+                            colors = ChipDefaults.secondaryChipColors(),
+                            modifier = Modifier.fillMaxWidth(),
+                            label = {
+                                Text("S2R Chip with defaults")
+                            }
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
 @OptIn(ExperimentalWearFoundationApi::class, ExperimentalWearMaterialApi::class)
 @Composable
 private fun SwipeToRevealChipExpandable(
diff --git a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
index c9871fe..25d3741 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/main/java/androidx/wear/compose/integration/macrobenchmark/SwipeBenchmark.kt
@@ -40,8 +40,6 @@
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
-    private lateinit var device: UiDevice
-
     @Before
     fun setUp() {
         val instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -64,8 +62,8 @@
             val swipeToDismissBox = device.findObject(By.desc(CONTENT_DESCRIPTION))
             // Setting a gesture margin is important otherwise gesture nav is triggered.
             swipeToDismissBox.setGestureMargin(device.displayWidth / 5)
-            repeat(10) {
-                swipeToDismissBox.swipe(Direction.RIGHT, 0.75f)
+            repeat(3) {
+                swipeToDismissBox.swipe(Direction.RIGHT, 0.75f, SWIPE_SPEED)
                 device.waitForIdle()
             }
         }
@@ -80,4 +78,7 @@
         @JvmStatic
         fun parameters() = createCompilationParams()
     }
+
+    private lateinit var device: UiDevice
+    private val SWIPE_SPEED = 500
 }
diff --git a/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/AsyncListenableWatchFaceServiceTest.kt b/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/AsyncListenableWatchFaceServiceTest.kt
index b12573c..77b9215 100644
--- a/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/AsyncListenableWatchFaceServiceTest.kt
+++ b/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/AsyncListenableWatchFaceServiceTest.kt
@@ -102,6 +102,7 @@
     private var surfaceHolderOverride: SurfaceHolder,
 ) : ListenableWatchFaceRuntimeService() {
     lateinit var lastResourceOnlyWatchFacePackageName: String
+    val lastResourceOnlyWatchFacePackageNameLatch = CountDownLatch(1)
 
     init {
         attachBaseContext(testContext)
@@ -117,6 +118,7 @@
         resourceOnlyWatchFacePackageName: String
     ): ListenableFuture<WatchFace> {
         lastResourceOnlyWatchFacePackageName = resourceOnlyWatchFacePackageName
+        lastResourceOnlyWatchFacePackageNameLatch.countDown()
 
         val future = SettableFuture.create<WatchFace>()
         // Post a task to resolve the future.
@@ -210,9 +212,12 @@
 
         val client = awaitWithTimeout(deferredClient)
 
-        // To avoid a race condition, we need to wait for the watchface to be fully created, which
-        // this does.
-        client.complicationSlotsState
+        Assert.assertTrue(
+            service.lastResourceOnlyWatchFacePackageNameLatch.await(
+                TIME_OUT_MILLIS,
+                TimeUnit.MILLISECONDS
+            )
+        )
 
         assertThat(service.lastResourceOnlyWatchFacePackageName).isEqualTo("com.example.wf")
 
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index c36b80e..e8f57d7 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -28,6 +28,7 @@
 import androidx.annotation.Px
 import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
 import androidx.wear.watchface.RenderParameters.HighlightedElement
 import androidx.wear.watchface.complications.ComplicationSlotBounds
@@ -243,8 +244,8 @@
     public const val ROUND_RECT: Int = 0
 
     /**
-     * For a full screen image complication slot drawn behind the watch face. Note you can only
-     * have a single background complication slot.
+     * For a full screen image complication slot drawn behind the watch face. Note you can only have
+     * a single background complication slot.
      */
     public const val BACKGROUND: Int = 1
 
@@ -342,21 +343,6 @@
  * expanded by [ComplicationSlotBounds.perComplicationTypeMargins]. Expanded bounds can overlap so
  * the [ComplicationSlot] with the lowest id that intersects the coordinates, if any, is selected.
  *
- * @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
- *   content description labels.
- * @param bounds The complication slot's [ComplicationSlotBounds].
- * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot, must be
- *   non-empty. During complication data source selection, each item in this list is compared in
- *   turn with entries from a data source's data source's supported types. The first matching entry
- *   from `supportedTypes` is chosen. If there are no matches then that data source is not eligible
- *   to be selected in this slot.
- * @param defaultPolicy The [DefaultComplicationDataSourcePolicy] which controls the initial
- *   complication data source when the watch face is first installed.
- * @param defaultDataSourceType The default [ComplicationType] for the default complication data
- *   source.
- * @param configExtras Extras to be merged into the Intent sent when invoking the complication data
- *   source chooser activity. This features is intended for OEM watch faces where they have elements
- *   that behave like a complication but are in fact entirely watch face specific.
  * @property id The Watch Face's ID for the complication slot.
  * @property boundsType The [ComplicationSlotBoundsTypeIntDef] of the complication slot.
  * @property canvasComplicationFactory The [CanvasComplicationFactory] used to generate a
@@ -373,6 +359,25 @@
  *   complication slot.
  */
 public class ComplicationSlot
+/**
+ * Constructs a [ComplicationSlot].
+ *
+ * @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
+ *   content description labels.
+ * @param bounds The complication slot's [ComplicationSlotBounds].
+ * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot, must be
+ *   non-empty. During complication data source selection, each item in this list is compared in
+ *   turn with entries from a data source's data source's supported types. The first matching entry
+ *   from `supportedTypes` is chosen. If there are no matches then that data source is not eligible
+ *   to be selected in this slot.
+ * @param defaultPolicy The [DefaultComplicationDataSourcePolicy] which controls the initial
+ *   complication data source when the watch face is first installed.
+ * @param defaultDataSourceType The default [ComplicationType] for the default complication data
+ *   source.
+ * @param configExtras Extras to be merged into the Intent sent when invoking the complication data
+ *   source chooser activity. This features is intended for OEM watch faces where they have elements
+ *   that behave like a complication but are in fact entirely watch face specific.
+ */
 @ComplicationExperimental
 internal constructor(
     public val id: Int,
@@ -391,8 +396,7 @@
     screenReaderNameResourceId: Int?,
     // TODO(b/230364881): This should really be public but some metalava bug is preventing
     // @ComplicationExperimental from working on the getter so it's currently hidden.
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public val boundingArc: BoundingArc?
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public val boundingArc: BoundingArc?
 ) {
     /**
      * The [ComplicationSlotsManager] this is attached to. Only set after the
@@ -430,7 +434,8 @@
 
     private var lastComplicationUpdate = Instant.EPOCH
 
-    private class ComplicationDataHistoryEntry(
+    @VisibleForTesting
+    internal class ComplicationDataHistoryEntry(
         val complicationData: ComplicationData,
         val time: Instant
     )
@@ -439,7 +444,8 @@
      * There doesn't seem to be a convenient ring buffer in the standard library so implement our
      * own one.
      */
-    private class RingBuffer(val size: Int) : Iterable<ComplicationDataHistoryEntry> {
+    @VisibleForTesting
+    internal class RingBuffer(val size: Int) : Iterable<ComplicationDataHistoryEntry> {
         private val entries = arrayOfNulls<ComplicationDataHistoryEntry>(size)
         private var readIndex = 0
         private var writeIndex = 0
@@ -467,9 +473,11 @@
 
     /**
      * In userdebug builds maintain a history of the last [MAX_COMPLICATION_HISTORY_ENTRIES]-1
-     * complications, which is logged in dumpsys to help debug complication issues.
+     * complications sent by the system, which is logged in dumpsys to help debug complication
+     * issues.
      */
-    private val complicationHistory =
+    @VisibleForTesting
+    internal val complicationHistory =
         if (Build.TYPE.equals("userdebug")) {
             RingBuffer(MAX_COMPLICATION_HISTORY_ENTRIES)
         } else {
@@ -666,8 +674,11 @@
             )
     }
 
+    /** Builder for constructing [ComplicationSlot]s. */
+    @OptIn(ComplicationExperimental::class)
+    public class Builder
     /**
-     * Builder for constructing [ComplicationSlot]s.
+     * Constructs a [Builder].
      *
      * @param id The watch face's ID for this complication. Can be any integer but should be unique
      *   within the watch face.
@@ -683,8 +694,6 @@
      * @param complicationTapFilter The [ComplicationTapFilter] used to perform hit testing for this
      *   complication.
      */
-    @OptIn(ComplicationExperimental::class)
-    public class Builder
     internal constructor(
         private val id: Int,
         private val canvasComplicationFactory: CanvasComplicationFactory,
@@ -1010,18 +1019,38 @@
 
     /**
      * Sets the current [ComplicationData] and if it's a timeline, the correct override for
-     * [instant] is chosen.
+     * [instant] is chosen. Any images associated with the complication are loaded asynchronously
+     * and the complication history is updated.
      */
-    internal fun setComplicationData(
-        complicationData: ComplicationData,
-        loadDrawablesAsynchronous: Boolean,
-        instant: Instant
-    ) {
+    internal fun setComplicationData(complicationData: ComplicationData, instant: Instant) {
         lastComplicationUpdate = instant
         complicationHistory?.push(ComplicationDataHistoryEntry(complicationData, instant))
         timelineComplicationData = complicationData
         timelineEntries = complicationData.asWireComplicationData().timelineEntries?.toList()
-        selectComplicationDataForInstant(instant, loadDrawablesAsynchronous, true)
+        selectComplicationDataForInstant(
+            instant,
+            loadDrawablesAsynchronous = true,
+            forceUpdate = true
+        )
+    }
+
+    /**
+     * Sets the current [ComplicationData] and if it's a timeline, the correct override for
+     * [instant] is chosen. Any images are loaded synchronously. The complication history is not
+     * updated.
+     */
+    internal fun setComplicationDataForScreenshot(
+        complicationData: ComplicationData,
+        instant: Instant
+    ) {
+        lastComplicationUpdate = instant
+        timelineComplicationData = complicationData
+        timelineEntries = complicationData.asWireComplicationData().timelineEntries?.toList()
+        selectComplicationDataForInstant(
+            instant,
+            loadDrawablesAsynchronous = false,
+            forceUpdate = true
+        )
     }
 
     /**
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 d9cdbd9..22d34a0 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
@@ -327,14 +327,14 @@
             return
         }
         complication.dataDirty = complication.dataDirty || (complication.renderer.getData() != data)
-        complication.setComplicationData(data, true, instant)
+        complication.setComplicationData(data, instant)
     }
 
     /**
      * For use by screen shot code which will reset the data afterwards, hence dirty bit not set.
      */
     @UiThread
-    internal fun setComplicationDataUpdateSync(
+    internal fun setComplicationDataUpdateForScreenshot(
         complicationSlotId: Int,
         data: ComplicationData,
         instant: Instant
@@ -348,7 +348,7 @@
             )
             return
         }
-        complication.setComplicationData(data, false, instant)
+        complication.setComplicationDataForScreenshot(data, instant)
     }
 
     /**
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index b2e238e..74af498 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -252,8 +252,9 @@
                     }
                     .apply { setContext(context) }
 
-            val engine = watchFaceService.createHeadlessEngine(componentName)
-                as WatchFaceService.EngineWrapper
+            val engine =
+                watchFaceService.createHeadlessEngine(componentName)
+                    as WatchFaceService.EngineWrapper
             val headlessWatchFaceImpl = engine.createHeadlessInstance(params)
             return engine.deferredWatchFaceImpl.await().WFEditorDelegate(headlessWatchFaceImpl)
         }
@@ -472,10 +473,7 @@
         }
     }
 
-    /**
-     * The [OverlayStyle]. This feature is unimplemented on any platform, and will be
-     * removed.
-     */
+    /** The [OverlayStyle]. This feature is unimplemented on any platform, and will be removed. */
     @Deprecated("OverlayStyle will be removed in a future release.")
     @Suppress("Deprecation")
     public var overlayStyle: OverlayStyle = OverlayStyle()
@@ -655,8 +653,7 @@
     private val tapListener = watchface.tapListener
     internal var complicationDeniedDialogIntent = watchface.complicationDeniedDialogIntent
     internal var complicationRationaleDialogIntent = watchface.complicationRationaleDialogIntent
-    @Suppress("Deprecation")
-    internal var overlayStyle = watchface.overlayStyle
+    @Suppress("Deprecation") internal var overlayStyle = watchface.overlayStyle
 
     private var mockTime = MockTime(1.0, 0, Long.MAX_VALUE)
 
@@ -671,27 +668,25 @@
 
     init {
         val context = watchFaceHostApi.getContext()
-        val displayManager =
-                context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+        val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
         displayManager.registerDisplayListener(
-                object : DisplayManager.DisplayListener {
-                    override fun onDisplayAdded(displayId: Int) {}
+            object : DisplayManager.DisplayListener {
+                override fun onDisplayAdded(displayId: Int) {}
 
-                    override fun onDisplayChanged(displayId: Int) {
-                        val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!!
-                        if (display.state == Display.STATE_OFF &&
-                                watchState.isVisible.value == false) {
-                            // We want to avoid a glimpse of a stale time when transitioning from
-                            // hidden to visible, so we render two black frames to clear the buffers
-                            // when the display has been turned off and the watch is not visible.
-                            renderer.renderBlackFrame()
-                            renderer.renderBlackFrame()
-                        }
+                override fun onDisplayChanged(displayId: Int) {
+                    val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!!
+                    if (display.state == Display.STATE_OFF && watchState.isVisible.value == false) {
+                        // We want to avoid a glimpse of a stale time when transitioning from
+                        // hidden to visible, so we render two black frames to clear the buffers
+                        // when the display has been turned off and the watch is not visible.
+                        renderer.renderBlackFrame()
+                        renderer.renderBlackFrame()
                     }
+                }
 
-                    override fun onDisplayRemoved(displayId: Int) {}
-                },
-                watchFaceHostApi.getUiThreadHandler()
+                override fun onDisplayRemoved(displayId: Int) {}
+            },
+            watchFaceHostApi.getUiThreadHandler()
         )
     }
 
@@ -893,8 +888,11 @@
             get() = watchFaceHostApi.getComplicationRationaleIntent()
 
         override var editorObscuresWatchFace: Boolean
-            get() = InteractiveInstanceManager
-                .getCurrentInteractiveInstance()?.engine?.editorObscuresWatchFace ?: false
+            get() =
+                InteractiveInstanceManager.getCurrentInteractiveInstance()
+                    ?.engine
+                    ?.editorObscuresWatchFace
+                    ?: false
             set(value) {
                 InteractiveInstanceManager.getCurrentInteractiveInstance()?.engine?.let {
                     it.editorObscuresWatchFace = value
@@ -915,7 +913,7 @@
 
                 slotIdToComplicationData?.let {
                     for ((id, complicationData) in it) {
-                        complicationSlotsManager.setComplicationDataUpdateSync(
+                        complicationSlotsManager.setComplicationDataUpdateForScreenshot(
                             id,
                             complicationData,
                             instant
@@ -930,7 +928,7 @@
                 slotIdToComplicationData?.let {
                     val now = getNow()
                     for ((id, complicationData) in oldComplicationData) {
-                        complicationSlotsManager.setComplicationDataUpdateSync(
+                        complicationSlotsManager.setComplicationDataUpdateForScreenshot(
                             id,
                             complicationData,
                             now
@@ -994,8 +992,11 @@
         // Separate calls are issued to deliver the state of isAmbient and isVisible, so during init
         // we might not yet know the state of both (which is required by the shouldAnimate logic).
         // If the editor is obscuring the watch face, there's no need to schedule a frame.
-        if (!watchState.isAmbient.hasValue() || !watchState.isVisible.hasValue() ||
-            editorObscuresWatchFace) {
+        if (
+            !watchState.isAmbient.hasValue() ||
+                !watchState.isVisible.hasValue() ||
+                editorObscuresWatchFace
+        ) {
             return
         }
 
@@ -1194,7 +1195,7 @@
 
             params.idAndComplicationDatumWireFormats?.let {
                 for (idAndData in it) {
-                    complicationSlotsManager.setComplicationDataUpdateSync(
+                    complicationSlotsManager.setComplicationDataUpdateForScreenshot(
                         idAndData.id,
                         idAndData.complicationData.toApiComplicationData(),
                         instant
@@ -1218,7 +1219,7 @@
                 if (params.idAndComplicationDatumWireFormats != null) {
                     val now = getNow()
                     for ((id, complicationData) in oldComplicationData) {
-                        complicationSlotsManager.setComplicationDataUpdateSync(
+                        complicationSlotsManager.setComplicationDataUpdateForScreenshot(
                             id,
                             complicationData,
                             now
@@ -1272,20 +1273,21 @@
 
                 // Compute the bounds of the complication based on the display rather than
                 // the headless renderer (which may be smaller).
-                val bounds = it.computeBounds(
-                    Rect(
-                    0,
-                    0,
-                        Resources.getSystem().displayMetrics.widthPixels,
-                        Resources.getSystem().displayMetrics.heightPixels
+                val bounds =
+                    it.computeBounds(
+                        Rect(
+                            0,
+                            0,
+                            Resources.getSystem().displayMetrics.widthPixels,
+                            Resources.getSystem().displayMetrics.heightPixels
+                        )
                     )
-                )
 
                 var prevData: ComplicationData? = null
                 val screenshotComplicationData = params.complicationData
                 if (screenshotComplicationData != null) {
                     prevData = it.renderer.getData()
-                    complicationSlotsManager.setComplicationDataUpdateSync(
+                    complicationSlotsManager.setComplicationDataUpdateForScreenshot(
                         params.complicationSlotId,
                         screenshotComplicationData.toApiComplicationData(),
                         instant
@@ -1303,12 +1305,13 @@
                         params.complicationSlotId
                     )
                     picture.endRecording()
-                    complicationBitmap = Api28CreateBitmapHelper.createBitmap(
-                        picture,
-                        bounds.width(),
-                        bounds.height(),
-                        Bitmap.Config.ARGB_8888
-                    )
+                    complicationBitmap =
+                        Api28CreateBitmapHelper.createBitmap(
+                            picture,
+                            bounds.width(),
+                            bounds.height(),
+                            Bitmap.Config.ARGB_8888
+                        )
                 } else {
                     complicationBitmap =
                         Bitmap.createBitmap(
@@ -1330,7 +1333,7 @@
                     // Restore previous ComplicationData & style if required.
                     if (prevData != null) {
                         val now = getNow()
-                        complicationSlotsManager.setComplicationDataUpdateSync(
+                        complicationSlotsManager.setComplicationDataUpdateForScreenshot(
                             params.complicationSlotId,
                             prevData,
                             now
@@ -1364,6 +1367,16 @@
         )
         writer.println("currentUserStyleRepository.schema=${currentUserStyleRepository.schema}")
         writer.println("editorObscuresWatchFace=$editorObscuresWatchFace")
+        writer.println("additionalContentDescriptionLabels:")
+        writer.increaseIndent()
+        for (label in renderer.additionalContentDescriptionLabels) {
+            if (Build.TYPE.equals("userdebug")) {
+                writer.println("${label.first}: ${label.second}")
+            } else {
+                writer.println("${label.first}: Redacted")
+            }
+        }
+        writer.decreaseIndent()
         overlayStyle.dump(writer)
         watchState.dump(writer)
         complicationSlotsManager.dump(writer)
@@ -1421,7 +1434,7 @@
 
             params.idAndComplicationDatumWireFormats?.let {
                 for (idAndData in it) {
-                    watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateSync(
+                    watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateForScreenshot(
                         idAndData.id,
                         idAndData.complicationData.toApiComplicationData(),
                         instant
@@ -1443,7 +1456,7 @@
             if (params.idAndComplicationDatumWireFormats != null) {
                 val now = watchFaceImpl.getNow()
                 for ((id, complicationData) in oldComplicationData) {
-                    watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateSync(
+                    watchFaceImpl.complicationSlotsManager.setComplicationDataUpdateForScreenshot(
                         id,
                         complicationData,
                         now
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 3a6c4ec..f131d12 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -70,6 +70,7 @@
 import androidx.wear.watchface.complications.data.ShortTextComplicationData
 import androidx.wear.watchface.complications.data.TimeDifferenceComplicationText
 import androidx.wear.watchface.complications.data.TimeDifferenceStyle
+import androidx.wear.watchface.complications.data.toApiComplicationData
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
 import androidx.wear.watchface.control.HeadlessWatchFaceImpl
@@ -142,6 +143,7 @@
 import org.mockito.kotlin.verifyNoMoreInteractions
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowBuild
 
 private const val INTERACTIVE_UPDATE_RATE_MS = 16L
 private const val LEFT_COMPLICATION_ID = 1000
@@ -728,6 +730,7 @@
 
     @Before
     public fun setUp() {
+        ShadowBuild.setType("userdebug")
         Assume.assumeTrue("This test suite assumes API 26", Build.VERSION.SDK_INT >= 26)
 
         `when`(handler.getLooper()).thenReturn(Looper.myLooper())
@@ -758,9 +761,11 @@
                  (TODO: b/264994539) - Explicitly releasing the mSurfaceControl field,
                  accessed via reflection. Remove when a proper fix is found
                 */
-                val mSurfaceControlObject: Field = WatchFaceService.EngineWrapper::class
-                    .java.superclass // android.service.wallpaper.WallpaperService$Engine
-                    .getDeclaredField("mSurfaceControl")
+                val mSurfaceControlObject: Field =
+                    WatchFaceService.EngineWrapper::class
+                        .java
+                        .superclass // android.service.wallpaper.WallpaperService$Engine
+                        .getDeclaredField("mSurfaceControl")
                 mSurfaceControlObject.isAccessible = true
                 (mSurfaceControlObject.get(engineWrapper) as SurfaceControl).release()
             }
@@ -1317,11 +1322,7 @@
                 null
             )
         verify(tapListener)
-            .onTapEvent(
-                TapType.UP,
-                TapEvent(10, 200, Instant.ofEpochMilli(looperTimeMillis)),
-                null
-            )
+            .onTapEvent(TapType.UP, TapEvent(10, 200, Instant.ofEpochMilli(looperTimeMillis)), null)
     }
 
     @Test
@@ -2858,6 +2859,48 @@
 
     @Test
     @Config(sdk = [Build.VERSION_CODES.O_MR1])
+    public fun updateComplicationData_appendsToHistory() {
+        initWallpaperInteractiveWatchFaceInstance(
+            complicationSlots = listOf(leftComplication)
+        )
+        // Validate that the history is initially empty.
+        assertThat(leftComplication.complicationHistory!!.iterator().asSequence().toList())
+            .isEmpty()
+        val longTextComplication =
+            WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
+                .build()
+
+        interactiveWatchFaceInstance.updateComplicationData(
+            listOf(IdAndComplicationDataWireFormat(LEFT_COMPLICATION_ID, longTextComplication))
+        )
+
+        assertThat(leftComplication.complicationHistory.toList().map { it.complicationData })
+            .containsExactly(longTextComplication.toApiComplicationData())
+    }
+
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
+    public fun setComplicationDataUpdateForScreenshot_doesNotAppendToHistory() {
+        initWallpaperInteractiveWatchFaceInstance(
+            complicationSlots = listOf(leftComplication)
+        )
+
+        complicationSlotsManager.setComplicationDataUpdateForScreenshot(
+            LEFT_COMPLICATION_ID,
+            WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT"))
+                .build()
+                .toApiComplicationData(),
+            Instant.now()
+        )
+
+        assertThat(leftComplication.complicationHistory!!.iterator().asSequence().toList())
+            .isEmpty()
+    }
+
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.O_MR1])
     public fun complicationCache() {
         val complicationCache = HashMap<String, ByteArray>()
         val instanceParams =