Merge "Fix CaptureFailedRetryQuirk failing for previous request info" into androidx-main
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
index 72087b1..a2f8213 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
@@ -77,7 +77,7 @@
 
         val navController = findNavController(R.id.nav_host_fragment_activity_main)
         val appBarConfiguration = AppBarConfiguration(
-            setOf(R.id.navigation_home, R.id.navigation_scanner, R.id.navigation_advertiser)
+            setOf(R.id.navigation_scanner, R.id.navigation_advertiser)
         )
         setupActionBarWithNavController(navController, appBarConfiguration)
         navView.setupWithNavController(navController)
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/BluetoothLe.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/BluetoothLe.kt
index 54d477c..91afd29 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/BluetoothLe.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/BluetoothLe.kt
@@ -23,15 +23,8 @@
 import android.bluetooth.BluetoothGattServerCallback
 import android.bluetooth.BluetoothGattService
 import android.bluetooth.BluetoothManager
-import android.bluetooth.le.AdvertiseCallback
-import android.bluetooth.le.AdvertiseData
-import android.bluetooth.le.AdvertiseSettings
-import android.bluetooth.le.ScanCallback
-import android.bluetooth.le.ScanResult
-import android.bluetooth.le.ScanSettings
 import android.content.Context
 import android.util.Log
-import java.util.UUID
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
@@ -48,92 +41,6 @@
     private val bluetoothManager =
         context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
 
-    // Permissions are handled by MainActivity requestBluetoothPermissions
-    @SuppressLint("MissingPermission")
-    fun scan(
-        settings: ScanSettings
-    ): Flow<ScanResult> =
-        callbackFlow {
-            val callback = object : ScanCallback() {
-                override fun onScanResult(callbackType: Int, result: ScanResult) {
-                    trySend(result)
-                }
-
-                override fun onScanFailed(errorCode: Int) {
-                    Log.d(TAG, "onScanFailed() called with: errorCode = $errorCode")
-                }
-            }
-
-            val bluetoothAdapter = bluetoothManager?.adapter
-            val bleScanner = bluetoothAdapter?.bluetoothLeScanner
-
-            bleScanner?.startScan(null, settings, callback)
-
-            awaitClose {
-                Log.d(TAG, "awaitClose() called")
-                bleScanner?.stopScan(callback)
-            }
-        }
-
-    // Permissions are handled by MainActivity requestBluetoothPermissions
-    @SuppressLint("MissingPermission")
-    fun advertise(
-        settings: AdvertiseSettings,
-        data: AdvertiseData
-    ): Flow<AdvertiseResult> =
-        callbackFlow {
-            val callback = object : AdvertiseCallback() {
-                override fun onStartFailure(errorCode: Int) {
-                    // TODO(ofy) Map to proper errorCodes
-                    Log.d(TAG, "onStartFailure() called with: errorCode = $errorCode")
-                    trySend(AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR)
-                }
-
-                override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
-                    trySend(AdvertiseResult.ADVERTISE_STARTED)
-                }
-            }
-
-            val bluetoothAdapter = bluetoothManager?.adapter
-            val bleAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser
-
-            bleAdvertiser?.startAdvertising(settings, data, callback)
-
-            awaitClose {
-                Log.d(TAG, "awaitClose() called")
-                bleAdvertiser?.stopAdvertising(callback)
-            }
-        }
-
-    interface GattClientScope {
-
-        fun getServices(): List<BluetoothGattService>
-        fun getService(uuid: UUID): BluetoothGattService?
-
-        suspend fun readCharacteristic(characteristic: BluetoothGattCharacteristic):
-            Result<ByteArray>
-        suspend fun writeCharacteristic(
-            characteristic: BluetoothGattCharacteristic,
-            value: ByteArray,
-            writeType: Int
-        ): Result<Unit>
-        suspend fun readDescriptor(descriptor: BluetoothGattDescriptor): Result<ByteArray>
-        suspend fun writeDescriptor(
-            descriptor: BluetoothGattDescriptor,
-            value: ByteArray
-        ): Result<Unit>
-        fun subscribeToCharacteristic(characteristic: BluetoothGattCharacteristic): Flow<ByteArray>
-        suspend fun awaitClose(onClosed: () -> Unit)
-    }
-
-    suspend fun <R> connectGatt(
-        context: Context,
-        device: BluetoothDevice,
-        block: suspend GattClientScope.() -> R
-    ): R? {
-        return GattClientImpl().connect(context, device, block)
-    }
-
     @SuppressLint("MissingPermission")
     fun openGattServer(
         services: List<BluetoothGattService> = emptyList()
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/GattClientImpl.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/GattClientImpl.kt
deleted file mode 100644
index 4c61aa6..0000000
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/GattClientImpl.kt
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
- * 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.bluetooth.integration.testapp.experimental
-
-import android.annotation.SuppressLint
-import android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothGatt
-import android.bluetooth.BluetoothGatt.GATT_SUCCESS
-import android.bluetooth.BluetoothGattCallback
-import android.bluetooth.BluetoothGattCharacteristic
-import android.bluetooth.BluetoothGattDescriptor
-import android.bluetooth.BluetoothGattService
-import android.content.Context
-import android.util.Log
-import androidx.collection.arrayMapOf
-import java.util.UUID
-import kotlin.coroutines.cancellation.CancellationException
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-
-internal class GattClientImpl {
-
-    private companion object {
-        private const val TAG = "GattClientImpl"
-        private const val GATT_MAX_MTU = 517
-        private val CCCD_UID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
-    }
-
-    private sealed interface CallbackResult {
-        class OnCharacteristicRead(
-            val characteristic: BluetoothGattCharacteristic,
-            val value: ByteArray,
-            val status: Int
-        ) : CallbackResult
-
-        class OnCharacteristicWrite(
-            val characteristic: BluetoothGattCharacteristic,
-            val status: Int
-        ) : CallbackResult
-
-        class OnDescriptorRead(
-            val descriptor: BluetoothGattDescriptor,
-            val value: ByteArray,
-            val status: Int
-        ) : CallbackResult
-        class OnDescriptorWrite(
-            val descriptor: BluetoothGattDescriptor,
-            val status: Int
-        ) : CallbackResult
-    }
-
-    private interface SubscribeListener {
-        fun onCharacteristicNotification(value: ByteArray)
-        fun finish()
-    }
-
-    @SuppressLint("MissingPermission")
-    suspend fun <R> connect(
-        context: Context,
-        device: BluetoothDevice,
-        block: suspend BluetoothLe.GattClientScope.() -> R
-    ): R? = coroutineScope {
-        val connectResult = CompletableDeferred<Boolean>(parent = coroutineContext.job)
-        val finished = Job(parent = coroutineContext.job)
-        val callbackResultsFlow = MutableSharedFlow<CallbackResult>(
-            extraBufferCapacity = Int.MAX_VALUE)
-        val subscribeMap: MutableMap<BluetoothGattCharacteristic, SubscribeListener> = arrayMapOf()
-        val subscribeMutex = Mutex()
-
-        val callback = object : BluetoothGattCallback() {
-            override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
-                if (newState == BluetoothGatt.STATE_CONNECTED) {
-                    gatt?.requestMtu(GATT_MAX_MTU)
-                } else {
-                    connectResult.complete(false)
-                    // TODO(b/270492198): throw precise exception
-                    finished.completeExceptionally(IllegalStateException("connect failed"))
-                }
-            }
-
-            override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
-                Log.d(TAG, "onMtuChanged() called with: gatt = $gatt, mtu = $mtu, status = $status")
-                if (status == GATT_SUCCESS) {
-                    gatt?.discoverServices()
-                } else {
-                    connectResult.complete(false)
-                    // TODO(b/270492198): throw precise exception
-                    finished.completeExceptionally(IllegalStateException("mtu request failed"))
-                }
-            }
-
-            override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
-                connectResult.complete(status == GATT_SUCCESS)
-            }
-
-            override fun onCharacteristicRead(
-                gatt: BluetoothGatt,
-                characteristic: BluetoothGattCharacteristic,
-                value: ByteArray,
-                status: Int
-            ) {
-                callbackResultsFlow.tryEmit(
-                    CallbackResult.OnCharacteristicRead(characteristic, value, status))
-            }
-
-            override fun onCharacteristicWrite(
-                gatt: BluetoothGatt,
-                characteristic: BluetoothGattCharacteristic,
-                status: Int
-            ) {
-                callbackResultsFlow.tryEmit(
-                    CallbackResult.OnCharacteristicWrite(characteristic, status))
-            }
-
-            override fun onDescriptorRead(
-                gatt: BluetoothGatt,
-                descriptor: BluetoothGattDescriptor,
-                status: Int,
-                value: ByteArray
-            ) {
-                callbackResultsFlow.tryEmit(
-                    CallbackResult.OnDescriptorRead(descriptor, value, status))
-            }
-
-            override fun onDescriptorWrite(
-                gatt: BluetoothGatt,
-                descriptor: BluetoothGattDescriptor,
-                status: Int
-            ) {
-                callbackResultsFlow.tryEmit(
-                    CallbackResult.OnDescriptorWrite(descriptor, status))
-            }
-
-            override fun onCharacteristicChanged(
-                gatt: BluetoothGatt,
-                characteristic: BluetoothGattCharacteristic,
-                value: ByteArray
-            ) {
-                launch {
-                    subscribeMutex.withLock {
-                        subscribeMap[characteristic]?.onCharacteristicNotification(value)
-                    }
-                }
-            }
-        }
-        val bluetoothGatt = device.connectGatt(context, /*autoConnect=*/false, callback)
-
-        if (!connectResult.await()) {
-            Log.w(TAG, "Failed to connect to the remote GATT server")
-            return@coroutineScope null
-        }
-        val gattScope = object : BluetoothLe.GattClientScope {
-            val taskMutex = Mutex()
-            suspend fun<R> runTask(block: suspend () -> R): R {
-                taskMutex.withLock {
-                    return block()
-                }
-            }
-
-            override fun getServices(): List<BluetoothGattService> {
-                return bluetoothGatt.services
-            }
-
-            override fun getService(uuid: UUID): BluetoothGattService? {
-                return bluetoothGatt.getService(uuid)
-            }
-
-            override suspend fun readCharacteristic(characteristic: BluetoothGattCharacteristic):
-                Result<ByteArray> {
-                return runTask {
-                    bluetoothGatt.readCharacteristic(characteristic)
-                    val res = takeMatchingResult<CallbackResult.OnCharacteristicRead>(
-                        callbackResultsFlow) {
-                        it.characteristic == characteristic
-                    }
-
-                    if (res.status == GATT_SUCCESS) Result.success(res.value)
-                    else Result.failure(RuntimeException("fail"))
-                }
-            }
-
-            override suspend fun writeCharacteristic(
-                characteristic: BluetoothGattCharacteristic,
-                value: ByteArray,
-                writeType: Int
-            ): Result<Unit> {
-                return runTask {
-                    bluetoothGatt.writeCharacteristic(characteristic, value, writeType)
-                    val res = takeMatchingResult<CallbackResult.OnCharacteristicWrite>(
-                        callbackResultsFlow) {
-                        it.characteristic == characteristic
-                    }
-                    if (res.status == GATT_SUCCESS) Result.success(Unit)
-                    else Result.failure(RuntimeException("fail"))
-                }
-            }
-
-            override suspend fun readDescriptor(descriptor: BluetoothGattDescriptor):
-                Result<ByteArray> {
-                return runTask {
-                    bluetoothGatt.readDescriptor(descriptor)
-                    val res = takeMatchingResult<CallbackResult.OnDescriptorRead>(
-                        callbackResultsFlow) {
-                        it.descriptor == descriptor
-                    }
-
-                    if (res.status == GATT_SUCCESS) Result.success(res.value)
-                    else Result.failure(RuntimeException("fail"))
-                }
-            }
-
-            override suspend fun writeDescriptor(
-                descriptor: BluetoothGattDescriptor,
-                value: ByteArray
-            ): Result<Unit> {
-                return runTask {
-                    bluetoothGatt.writeDescriptor(descriptor, value)
-                    val res = takeMatchingResult<CallbackResult.OnDescriptorWrite>(
-                        callbackResultsFlow) {
-                        it.descriptor == descriptor
-                    }
-                    if (res.status == GATT_SUCCESS) Result.success(Unit)
-                    else Result.failure(RuntimeException("fail"))
-                }
-            }
-
-            override fun subscribeToCharacteristic(characteristic: BluetoothGattCharacteristic):
-                Flow<ByteArray> {
-                val cccd = characteristic.getDescriptor(CCCD_UID) ?: return emptyFlow()
-
-                return callbackFlow {
-                    val listener = object : SubscribeListener {
-                        override fun onCharacteristicNotification(value: ByteArray) {
-                            trySend(value)
-                        }
-                        override fun finish() {
-                            cancel("finished")
-                        }
-                    }
-                    if (!registerSubscribeListener(characteristic, listener)) {
-                        cancel("already subscribed")
-                    }
-
-                    runTask {
-                        bluetoothGatt.setCharacteristicNotification(characteristic, /*enable=*/true)
-                        bluetoothGatt.writeDescriptor(
-                            cccd,
-                            BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
-                        )
-                        val res = takeMatchingResult<CallbackResult.OnDescriptorWrite>(
-                            callbackResultsFlow) {
-                            it.descriptor == cccd
-                        }
-                        if (res.status != GATT_SUCCESS) {
-                            cancel(CancellationException("failed to set notification"))
-                        }
-                    }
-
-                    this.awaitClose {
-                        launch {
-                            unregisterSubscribeListener(characteristic)
-                        }
-                        bluetoothGatt.setCharacteristicNotification(characteristic,
-                            /*enable=*/false)
-                        bluetoothGatt.writeDescriptor(
-                            cccd,
-                            BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
-                        )
-                    }
-                }
-            }
-
-            override suspend fun awaitClose(onClosed: () -> Unit) {
-                try {
-                    // Wait for queued tasks done
-                    taskMutex.withLock {
-                        subscribeMutex.withLock {
-                            subscribeMap.values.forEach { it.finish() }
-                        }
-                    }
-                } finally {
-                    onClosed()
-                }
-            }
-
-            private suspend fun registerSubscribeListener(
-                characteristic: BluetoothGattCharacteristic,
-                callback: SubscribeListener
-            ): Boolean {
-                subscribeMutex.withLock {
-                    if (subscribeMap.containsKey(characteristic)) {
-                        return false
-                    }
-                    subscribeMap[characteristic] = callback
-                    return true
-                }
-            }
-
-            private suspend fun unregisterSubscribeListener(
-                characteristic: BluetoothGattCharacteristic
-            ) {
-                subscribeMutex.withLock {
-                    subscribeMap.remove(characteristic)
-                }
-            }
-        }
-        gattScope.block()
-    }
-
-    private suspend inline fun<reified R : CallbackResult> takeMatchingResult(
-        flow: SharedFlow<CallbackResult>,
-        crossinline predicate: (R) -> Boolean
-    ): R {
-        return flow.filter { it is R && predicate(it) }.first() as R
-    }
-}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeFragment.kt
deleted file mode 100644
index bb72d7c..0000000
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeFragment.kt
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- * 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.bluetooth.integration.testapp.ui.home
-
-import android.annotation.SuppressLint
-import android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY
-import android.bluetooth.BluetoothGattCharacteristic.PROPERTY_READ
-import android.bluetooth.le.AdvertiseData
-import android.bluetooth.le.AdvertiseSettings
-import android.bluetooth.le.ScanResult
-import android.bluetooth.le.ScanSettings
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.bluetooth.integration.testapp.R
-import androidx.bluetooth.integration.testapp.databinding.FragmentHomeBinding
-import androidx.bluetooth.integration.testapp.experimental.AdvertiseResult
-import androidx.bluetooth.integration.testapp.experimental.BluetoothLe
-import androidx.bluetooth.integration.testapp.experimental.GattServerCallback
-import androidx.bluetooth.integration.testapp.ui.common.ScanResultAdapter
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-
-class HomeFragment : Fragment() {
-
-    companion object {
-        private const val TAG = "HomeFragment"
-    }
-
-    private var scanResultAdapter: ScanResultAdapter? = null
-
-    private lateinit var bluetoothLe: BluetoothLe
-
-    private val viewModel: HomeViewModel by viewModels()
-
-    private var _binding: FragmentHomeBinding? = null
-    private val binding get() = _binding!!
-
-    override fun onCreateView(
-        inflater: LayoutInflater,
-        container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View {
-        _binding = FragmentHomeBinding.inflate(inflater, container, false)
-        return binding.root
-    }
-
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        super.onViewCreated(view, savedInstanceState)
-
-        bluetoothLe = BluetoothLe(requireContext())
-
-        scanResultAdapter = ScanResultAdapter { scanResult -> onClickScanResult(scanResult) }
-        binding.recyclerView.adapter = scanResultAdapter
-
-        binding.buttonScan.setOnClickListener {
-            if (scanJob?.isActive == true) {
-                scanJob?.cancel()
-                binding.buttonScan.text = getString(R.string.scan_using_androidx_bluetooth)
-            } else {
-                startScan()
-            }
-        }
-
-        binding.switchAdvertise.setOnCheckedChangeListener { _, isChecked ->
-            if (isChecked) startAdvertise()
-            else advertiseJob?.cancel()
-        }
-
-        binding.switchGattServer.setOnCheckedChangeListener { _, isChecked ->
-            if (isChecked) openGattServer()
-            else gattServerJob?.cancel()
-        }
-    }
-
-    override fun onDestroyView() {
-        super.onDestroyView()
-        _binding = null
-        scanJob?.cancel()
-        advertiseJob?.cancel()
-        gattServerJob?.cancel()
-    }
-
-    private val scanScope = CoroutineScope(Dispatchers.Main + Job())
-    private var scanJob: Job? = null
-
-    private val connectScope = CoroutineScope(Dispatchers.Default + Job())
-    private var connectJob: Job? = null
-
-    private fun startScan() {
-        Log.d(TAG, "startScan() called")
-
-        val scanSettings = ScanSettings.Builder()
-            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
-            .build()
-
-        scanJob = scanScope.launch {
-            Toast.makeText(context, getString(R.string.scan_start_message), Toast.LENGTH_SHORT)
-                .show()
-
-            binding.buttonScan.text = getString(R.string.stop_scanning)
-
-            bluetoothLe.scan(scanSettings)
-                .collect {
-                    Log.d(TAG, "ScanResult collected: $it")
-
-                    if (it.scanRecord?.serviceUuids?.isEmpty() == false)
-                        viewModel.scanResults[it.device.address] = it
-                    scanResultAdapter?.submitList(viewModel.scanResults.values.toMutableList())
-                    scanResultAdapter?.notifyItemInserted(viewModel.scanResults.size)
-                }
-        }
-    }
-
-    private fun onClickScanResult(scanResult: ScanResult) {
-        scanJob?.cancel()
-        connectJob?.cancel()
-        connectJob = connectScope.launch {
-            bluetoothLe.connectGatt(requireContext(), scanResult.device) {
-                for (srv in getServices()) {
-                    for (char in srv.characteristics) {
-                        if (char.properties.and(PROPERTY_READ) == 0) continue
-                        launch {
-                            val value = readCharacteristic(char).getOrNull()
-                            if (value != null) {
-                                Log.d(TAG, "Successfully read characteristic value=$value")
-                            }
-                        }
-                        launch {
-                            if (char.properties.and(PROPERTY_NOTIFY) != 0) {
-                                val value = subscribeToCharacteristic(char).first()
-                                Log.d(TAG, "Successfully get characteristic value=$value")
-                            }
-                        }
-                    }
-                }
-                awaitClose {
-                    Log.d(TAG, "GATT client is closed")
-                    connectJob = null
-                }
-            }
-        }
-    }
-
-    private val advertiseScope = CoroutineScope(Dispatchers.Main + Job())
-    private var advertiseJob: Job? = null
-
-    // Permissions are handled by MainActivity requestBluetoothPermissions
-    @SuppressLint("MissingPermission")
-    private fun startAdvertise() {
-        Log.d(TAG, "startAdvertise() called")
-
-        val advertiseSettings = AdvertiseSettings.Builder()
-            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
-            .setTimeout(0)
-            .build()
-
-        val advertiseData = AdvertiseData.Builder()
-            .setIncludeDeviceName(true)
-            .build()
-
-        advertiseJob = advertiseScope.launch {
-            bluetoothLe.advertise(advertiseSettings, advertiseData)
-                .collect {
-                    Log.d(TAG, "advertiseResult received: $it")
-
-                    when (it) {
-                        AdvertiseResult.ADVERTISE_STARTED -> {
-                            Toast.makeText(
-                                context,
-                                getString(R.string.advertise_start_message), Toast.LENGTH_SHORT
-                            )
-                                .show()
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_ALREADY_STARTED -> {
-                            Log.d(
-                                TAG, "advertise onStartFailure() called with: " +
-                                    "${AdvertiseResult.ADVERTISE_FAILED_ALREADY_STARTED}"
-                            )
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE -> {
-                            Log.d(
-                                TAG, "advertise onStartFailure() called with: " +
-                                    "${AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE}"
-                            )
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> {
-                            Log.d(
-                                TAG, "advertise onStartFailure() called with: " +
-                                    "${AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED}"
-                            )
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR -> {
-                            Log.d(
-                                TAG, "advertise onStartFailure() called with: " +
-                                    "${AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR}"
-                            )
-                        }
-                        AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
-                            Log.d(
-                                TAG, "advertise onStartFailure() called with: " +
-                                    "${AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS}"
-                            )
-                        }
-                    }
-                }
-        }
-    }
-
-    private val gattServerScope = CoroutineScope(Dispatchers.Main + Job())
-    private var gattServerJob: Job? = null
-
-    // Permissions are handled by MainActivity requestBluetoothPermissions
-    @SuppressLint("MissingPermission")
-    private fun openGattServer() {
-        Log.d(TAG, "openGattServer() called")
-
-        gattServerJob = gattServerScope.launch {
-            bluetoothLe.openGattServer().collect { gattServerCallback ->
-                when (gattServerCallback) {
-                    is GattServerCallback.OnCharacteristicReadRequest -> {
-                        val onCharacteristicReadRequest:
-                            GattServerCallback.OnCharacteristicReadRequest = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onCharacteristicReadRequest = $onCharacteristicReadRequest"
-                        )
-                    }
-                    is GattServerCallback.OnCharacteristicWriteRequest -> {
-                        val onCharacteristicWriteRequest:
-                            GattServerCallback.OnCharacteristicWriteRequest = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onCharacteristicWriteRequest = $onCharacteristicWriteRequest"
-                        )
-                    }
-                    is GattServerCallback.OnConnectionStateChange -> {
-                        val onConnectionStateChange:
-                            GattServerCallback.OnConnectionStateChange = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onConnectionStateChange = $onConnectionStateChange"
-                        )
-                    }
-                    is GattServerCallback.OnDescriptorReadRequest -> {
-                        val onDescriptorReadRequest:
-                            GattServerCallback.OnDescriptorReadRequest = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onDescriptorReadRequest = $onDescriptorReadRequest"
-                        )
-                    }
-                    is GattServerCallback.OnDescriptorWriteRequest -> {
-                        val onDescriptorWriteRequest:
-                            GattServerCallback.OnDescriptorWriteRequest = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onDescriptorWriteRequest = $onDescriptorWriteRequest"
-                        )
-                    }
-                    is GattServerCallback.OnExecuteWrite -> {
-                        val onExecuteWrite:
-                            GattServerCallback.OnExecuteWrite = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onExecuteWrite = $onExecuteWrite"
-                        )
-                    }
-                    is GattServerCallback.OnMtuChanged -> {
-                        val onMtuChanged:
-                            GattServerCallback.OnMtuChanged = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onMtuChanged = $onMtuChanged"
-                        )
-                    }
-                    is GattServerCallback.OnNotificationSent -> {
-                        val onNotificationSent:
-                            GattServerCallback.OnNotificationSent = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onNotificationSent = $onNotificationSent"
-                        )
-                    }
-                    is GattServerCallback.OnPhyRead -> {
-                        val onPhyRead:
-                            GattServerCallback.OnPhyRead = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onPhyRead = $onPhyRead"
-                        )
-                    }
-                    is GattServerCallback.OnPhyUpdate -> {
-                        val onPhyUpdate:
-                            GattServerCallback.OnPhyUpdate = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onPhyUpdate = $onPhyUpdate"
-                        )
-                    }
-                    is GattServerCallback.OnServiceAdded -> {
-                        val onServiceAdded:
-                            GattServerCallback.OnServiceAdded = gattServerCallback
-                        Log.d(
-                            TAG,
-                            "openGattServer() called with: " +
-                                "onServiceAdded = $onServiceAdded"
-                        )
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/bluetooth/integration-tests/testapp/src/main/res/drawable/ic_bluetooth_24.xml b/bluetooth/integration-tests/testapp/src/main/res/drawable/ic_bluetooth_24.xml
deleted file mode 100644
index 34e9311..0000000
--- a/bluetooth/integration-tests/testapp/src/main/res/drawable/ic_bluetooth_24.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<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="M17.71,7.71L12,2h-1v7.59L6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 11,14.41L11,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,5.83l1.88,1.88L13,9.59L13,5.83zM14.88,16.29L13,18.17v-3.76l1.88,1.88z"/>
-</vector>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_home.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_home.xml
deleted file mode 100644
index 9b242ce..0000000
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_home.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright 2022 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-<androidx.constraintlayout.widget.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context=".ui.home.HomeFragment">
-
-    <Button
-        android:id="@+id/button_scan"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="16dp"
-        android:text="@string/scan_using_androidx_bluetooth"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <androidx.appcompat.widget.SwitchCompat
-        android:id="@+id/switch_advertise"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="16dp"
-        android:text="@string/advertise_using_androidx_bluetooth"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/button_scan" />
-
-    <androidx.appcompat.widget.SwitchCompat
-        android:id="@+id/switch_gatt_server"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="16dp"
-        android:text="@string/open_gatt_server_using_androidx_bluetooth"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/switch_advertise" />
-
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/recycler_view"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        app:layoutManager="LinearLayoutManager"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/switch_gatt_server"
-        tools:itemCount="3"
-        tools:listitem="@layout/scan_result_item" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/menu/bottom_nav_menu.xml b/bluetooth/integration-tests/testapp/src/main/res/menu/bottom_nav_menu.xml
index 68f6353..d3c5b8f 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/menu/bottom_nav_menu.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/menu/bottom_nav_menu.xml
@@ -17,11 +17,6 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item
-        android:id="@+id/navigation_home"
-        android:icon="@drawable/ic_bluetooth_24"
-        android:title="@string/title_home" />
-
-    <item
         android:id="@+id/navigation_scanner"
         android:icon="@drawable/baseline_bluetooth_searching_24"
         android:title="@string/title_scanner" />
diff --git a/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml b/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
index b3c74a1..488e0f06 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
@@ -18,24 +18,18 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/nav_graph"
-    app:startDestination="@id/navigation_home">
-
-    <fragment
-        android:id="@+id/navigation_home"
-        android:name="androidx.bluetooth.integration.testapp.ui.home.HomeFragment"
-        android:label="@string/title_home"
-        tools:layout="@layout/fragment_home" />
+    app:startDestination="@id/navigation_scanner">
 
     <fragment
         android:id="@+id/navigation_scanner"
         android:name="androidx.bluetooth.integration.testapp.ui.scanner.ScannerFragment"
         android:label="@string/title_scanner"
-        tools:layout="@layout/fragment_home" />
+        tools:layout="@layout/fragment_scanner" />
 
     <fragment
         android:id="@+id/navigation_advertiser"
         android:name="androidx.bluetooth.integration.testapp.ui.advertiser.AdvertiserFragment"
         android:label="@string/title_advertiser"
-        tools:layout="@layout/fragment_home" />
+        tools:layout="@layout/fragment_advertiser" />
 
 </navigation>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index 7c47b75..06dffe7 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -17,7 +17,6 @@
 <resources>
     <string name="app_name">AndroidX Bluetooth Test App</string>
 
-    <string name="title_home">AndroidX Bluetooth</string>
     <string name="title_scanner">Scanner</string>
     <string name="title_advertiser">Advertiser</string>
 
@@ -70,13 +69,4 @@
     <string name="gatt_server">Gatt Server</string>
     <string name="open_gatt_server">Open Gatt Server</string>
     <string name="stop_gatt_server">Stop Gatt Server</string>
-
-    <string name="scan_using_androidx_bluetooth">Scan using AndroidX Bluetooth APIs</string>
-    <string name="scan_start_message">Scan started. Results are in Logcat</string>
-
-    <string name="advertise_using_androidx_bluetooth">Advertise using AndroidX Bluetooth APIs</string>
-    <string name="advertise_start_message">Advertise started</string>
-
-    <string name="open_gatt_server_using_androidx_bluetooth">Open GATT Server using AndroidX Bluetooth APIs</string>
-    <string name="gatt_server_open">GATT Server open</string>
 </resources>
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index d5f554e..dd99ca8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -105,6 +105,7 @@
 import androidx.camera.core.impl.MutableOptionsBundle;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.SessionProcessor;
 import androidx.camera.core.impl.StreamSpec;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
@@ -1154,6 +1155,37 @@
     }
 
     /**
+     * Returns an estimate of the capture and processing sequence duration based on the current
+     * camera configuration and scene conditions. The value will vary as the scene and/or camera
+     * configuration change.
+     *
+     * <p>The processing estimate can vary based on device processing load.
+     *
+     * <p>If the image capture latency estimate is not supported then
+     * {@link ImageCaptureLatencyEstimate#UNDEFINED_IMAGE_CAPTURE_LATENCY} is returned. If the
+     * capture latency is not supported then the capture latency component will be
+     * {@link ImageCaptureLatencyEstimate#UNDEFINED_CAPTURE_LATENCY}. If the processing
+     * latency is not supported then the processing latency component will be
+     * {@link ImageCaptureLatencyEstimate#UNDEFINED_PROCESSING_LATENCY}.
+     **/
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public ImageCaptureLatencyEstimate getRealtimeCaptureLatencyEstimate() {
+        final CameraInternal camera = getCamera();
+        if (camera == null) {
+            return ImageCaptureLatencyEstimate.UNDEFINED_IMAGE_CAPTURE_LATENCY;
+        }
+
+        final CameraConfig config = camera.getExtendedConfig();
+        final SessionProcessor sessionProcessor = config.getSessionProcessor();
+        final Pair<Long, Long> latencyEstimate = sessionProcessor.getRealtimeCaptureLatency();
+        if (latencyEstimate == null) {
+            return ImageCaptureLatencyEstimate.UNDEFINED_IMAGE_CAPTURE_LATENCY;
+        }
+        return new ImageCaptureLatencyEstimate(latencyEstimate.first, latencyEstimate.second);
+    }
+
+    /**
      * Describes the error that occurred during an image capture operation (such as {@link
      * ImageCapture#takePicture(Executor, OnImageCapturedCallback)}).
      *
@@ -2367,6 +2399,7 @@
          * Sets the {@link DynamicRange}.
          *
          * <p>This is currently only exposed to internally set the dynamic range to SDR.
+         *
          * @return The current Builder.
          * @see DynamicRange
          */
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureLatencyEstimate.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureLatencyEstimate.java
new file mode 100644
index 0000000..4c5c02d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCaptureLatencyEstimate.java
@@ -0,0 +1,86 @@
+/*
+ * 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 androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.util.Objects;
+
+/**
+ * Defines the estimated duration an image capture will take capturing and processing for the
+ * current scene condition and/or camera configuration.
+ *
+ * <p>The estimate comprises of two components: {@link #captureLatencyMillis},
+ * {@link #processingLatencyMillis}
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ImageCaptureLatencyEstimate {
+    /** The capture latency is unsupported or undefined */
+    public static final long UNDEFINED_CAPTURE_LATENCY = -1;
+
+    /** The processing latency is unsupported or undefined */
+    public static final long UNDEFINED_PROCESSING_LATENCY = -1;
+
+    /** The image capture latency estimate is unsupported or undefined */
+    @NonNull
+    public static final ImageCaptureLatencyEstimate UNDEFINED_IMAGE_CAPTURE_LATENCY =
+            new ImageCaptureLatencyEstimate(UNDEFINED_CAPTURE_LATENCY,
+                    UNDEFINED_PROCESSING_LATENCY);
+
+    /**
+     * The estimated duration in milliseconds from when the camera begins capturing frames to the
+     * moment the camera has completed capturing frames. If this estimate is not supported or not
+     * available then it will be {@link #UNDEFINED_CAPTURE_LATENCY}.
+     */
+    public final long captureLatencyMillis;
+
+    /**
+     * The estimated duration in milliseconds from when the processing begins until the processing
+     * has completed and the final processed capture is available. If this estimate is not supported
+     * or not available then it will be {@link #UNDEFINED_PROCESSING_LATENCY}.
+     */
+    public final long processingLatencyMillis;
+
+    ImageCaptureLatencyEstimate(long captureLatencyMillis, long processingLatencyMillis) {
+        this.captureLatencyMillis = captureLatencyMillis;
+        this.processingLatencyMillis = processingLatencyMillis;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof ImageCaptureLatencyEstimate)) return false;
+        ImageCaptureLatencyEstimate that = (ImageCaptureLatencyEstimate) o;
+        return captureLatencyMillis == that.captureLatencyMillis
+                && processingLatencyMillis == that.processingLatencyMillis;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(captureLatencyMillis, processingLatencyMillis);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "captureLatencyMillis=" + captureLatencyMillis
+                + ", processingLatencyMillis=" + processingLatencyMillis;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
index 70b5c2b..f644291 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionProcessor.java
@@ -18,6 +18,7 @@
 
 import android.hardware.camera2.CaptureResult;
 import android.media.ImageReader;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -137,6 +138,30 @@
     }
 
     /**
+     * Returns the dynamically calculated capture latency pair in milliseconds.
+     *
+     * The measurement is expected to take in to account dynamic parameters such as the current
+     * scene, the state of 3A algorithms, the state of internal HW modules and return a more
+     * accurate assessment of the capture and/or processing latency.</p>
+     *
+     * @return pair that includes the estimated input frame/frames camera capture latency as the
+     * first field. This is the time between {@link CaptureCallback#onCaptureStarted} and
+     * {@link CaptureCallback#onCaptureProcessStarted}. The second field value includes the
+     * estimated post-processing latency. This is the time between
+     * {@link CaptureCallback#onCaptureProcessStarted} until the processed frame returns back to the
+     * client registered surface.
+     * Both first and second values will be in milliseconds. The total still capture latency will be
+     * the sum of both the first and second values of the pair.
+     * The pair is expected to be null if the dynamic latency estimation is not supported.
+     * If clients have not configured a still capture output, then this method can also return a
+     * null pair.
+     */
+    @Nullable
+    default Pair<Long, Long> getRealtimeCaptureLatency() {
+        return null;
+    }
+
+    /**
      * Callback for {@link #startRepeating} and {@link #startCapture}.
      */
     interface CaptureCallback {
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
index eb342e6..0570e39 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/AdvancedSessionProcessorTest.kt
@@ -44,6 +44,7 @@
 import android.media.ImageReader
 import android.media.ImageWriter
 import android.os.Build
+import android.util.Pair
 import android.util.Size
 import android.view.Surface
 import androidx.annotation.RequiresApi
@@ -225,6 +226,22 @@
         fakeSessionProcessImpl.assertStartTriggerIsCalledWithParameters(parametersMap)
     }
 
+    @Test
+    fun getRealtimeLatencyEstimate_advancedSessionProcessorInvokesSessionProcessorImpl() =
+        runBlocking {
+            val fakeSessionProcessImpl = object : SessionProcessorImpl by FakeSessionProcessImpl() {
+                override fun getRealtimeCaptureLatency(): Pair<Long, Long> = Pair(1000L, 10L)
+            }
+            val advancedSessionProcessor = AdvancedSessionProcessor(
+                fakeSessionProcessImpl, emptyList(), context
+            )
+
+            val realtimeCaptureLatencyEstimate = advancedSessionProcessor.realtimeCaptureLatency
+
+            assertThat(realtimeCaptureLatencyEstimate?.first).isEqualTo(1000L)
+            assertThat(realtimeCaptureLatencyEstimate?.second).isEqualTo(10L)
+        }
+
     private suspend fun assumeAllowsSharedSurface() = withContext(Dispatchers.Main) {
         val imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2)
         val maxSharedSurfaceCount =
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index 1634996..b1b7d73 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -337,6 +337,20 @@
         )
     }
 
+    @Test
+    fun getRealtimeCaptureLatencyEstimate_invokesCaptureExtenderImpl(): Unit = runBlocking {
+        assumeTrue(hasCaptureProcessor)
+        fakeCaptureExtenderImpl = object : FakeImageCaptureExtenderImpl(hasCaptureProcessor) {
+            override fun getRealtimeCaptureLatency(): Pair<Long, Long> = Pair(1000L, 10L)
+        }
+
+        basicExtenderSessionProcessor = BasicExtenderSessionProcessor(
+            fakePreviewExtenderImpl, fakeCaptureExtenderImpl, emptyList(), emptyList(), context
+        )
+
+        assertThat(basicExtenderSessionProcessor.realtimeCaptureLatency).isEqualTo(Pair(1000L, 10L))
+    }
+
     class ResultMonitor {
         private var latch: CountDownLatch? = null
         private var keyToCheck: CaptureRequest.Key<*>? = null
@@ -833,7 +847,7 @@
         }
     }
 
-    private class FakeImageCaptureExtenderImpl(
+    private open class FakeImageCaptureExtenderImpl(
         private val hasCaptureProcessor: Boolean = false,
         private val throwErrorOnProcess: Boolean = false
     ) : ImageCaptureExtenderImpl, FakeExtenderStateListener() {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/Version.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/Version.java
index c740199..f34287a 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/Version.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/Version.java
@@ -39,6 +39,7 @@
     public static final Version VERSION_1_1 = Version.create(1, 1, 0, "");
     public static final Version VERSION_1_2 = Version.create(1, 2, 0, "");
     public static final Version VERSION_1_3 = Version.create(1, 3, 0, "");
+    public static final Version VERSION_1_4 = Version.create(1, 4, 0, "");
 
     private static final Pattern VERSION_STRING_PATTERN =
             Pattern.compile("(\\d+)(?:\\.(\\d+))(?:\\.(\\d+))(?:\\-(.+))?");
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
index a9c4432..85c344d 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/AdvancedSessionProcessor.java
@@ -23,6 +23,7 @@
 import android.hardware.camera2.CaptureResult;
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
+import android.util.Pair;
 import android.util.Size;
 import android.view.Surface;
 
@@ -187,6 +188,16 @@
         mImpl.abortCapture(captureSequenceId);
     }
 
+    @Nullable
+    @Override
+    public Pair<Long, Long> getRealtimeCaptureLatency() {
+        if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
+                && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)) {
+            return mImpl.getRealtimeCaptureLatency();
+        }
+        return null;
+    }
+
     /**
      * Adapter to transform a {@link OutputSurface} to a {@link OutputSurfaceImpl}.
      */
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
index 649640c..51fe764 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -647,4 +647,14 @@
 
         return captureSequenceId;
     }
+
+    @Nullable
+    @Override
+    public Pair<Long, Long> getRealtimeCaptureLatency() {
+        if (ClientVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)
+                && ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_4)) {
+            return mImageCaptureExtenderImpl.getRealtimeCaptureLatency();
+        }
+        return null;
+    }
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
index 6ed8f4d..f235fbe 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/ImageCaptureExtenderImpl.java
@@ -170,4 +170,27 @@
      */
     @NonNull
     List<CaptureResult.Key> getAvailableCaptureResultKeys();
+
+    /**
+     * Returns the dynamically calculated capture latency pair in milliseconds.
+     *
+     * <p>In contrast to {@link #getEstimatedCaptureLatencyRange} this method is guaranteed to be
+     * called after the camera capture session is initialized and camera preview is enabled.
+     * The measurement is expected to take in to account dynamic parameters such as the current
+     * scene, the state of 3A algorithms, the state of internal HW modules and return a more
+     * accurate assessment of the still capture latency.</p>
+     *
+     * @return pair that includes the estimated input frame/frames camera capture latency as the
+     * first field and the estimated post-processing latency {@link CaptureProcessorImpl#process}
+     * as the second pair field. Both first and second fields will be in milliseconds. The total
+     * still capture latency will be the sum of both the first and second values.
+     * The pair is expected to be null if the dynamic latency estimation is not supported.
+     * If clients have not configured a still capture output, then this method can also return a
+     * null pair.
+     * @since 1.4
+     */
+    @Nullable
+    default Pair<Long, Long> getRealtimeCaptureLatency() {
+        return null;
+    };
 }
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
index fabfc2b..e3ace72 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/advanced/SessionProcessorImpl.java
@@ -21,8 +21,11 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.CaptureResult;
+import android.util.Pair;
 import android.view.Surface;
 
+import androidx.annotation.Nullable;
+
 import java.util.Map;
 
 /**
@@ -193,6 +196,32 @@
     void abortCapture(int captureSequenceId);
 
     /**
+     * Returns the dynamically calculated capture latency pair in milliseconds.
+     *
+     * <p>In contrast to {@link AdvancedExtenderImpl#getEstimatedCaptureLatencyRange} this method is
+     * guaranteed to be called after {@link #onCaptureSessionStart}.
+     * The measurement is expected to take in to account dynamic parameters such as the current
+     * scene, the state of 3A algorithms, the state of internal HW modules and return a more
+     * accurate assessment of the still capture latency.</p>
+     *
+     * @return pair that includes the estimated input frame/frames camera capture latency as the
+     * first field. This is the time between {@link #onCaptureStarted} and
+     * {@link #onCaptureProcessStarted}. The second field value includes the estimated
+     * post-processing latency. This is the time between {@link #onCaptureProcessStarted} until
+     * the processed frame returns back to the client registered surface.
+     * Both first and second values will be in milliseconds. The total still capture latency will be
+     * the sum of both the first and second values of the pair.
+     * The pair is expected to be null if the dynamic latency estimation is not supported.
+     * If clients have not configured a still capture output, then this method can also return a
+     * null pair.
+     * @since 1.4
+     */
+    @Nullable
+    default Pair<Long, Long> getRealtimeCaptureLatency() {
+        return null;
+    };
+
+    /**
      * Callback for notifying the status of {@link #startCapture(CaptureCallback)} and
      * {@link #startRepeating(CaptureCallback)}.
      */
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/audio/AudioSourceTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/audio/AudioSourceTest.kt
index 47dadac..eeaf74b 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/internal/audio/AudioSourceTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/audio/AudioSourceTest.kt
@@ -33,6 +33,7 @@
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit.NANOSECONDS
 import org.junit.After
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
@@ -62,6 +63,7 @@
         }
     }
 
+    @Ignore("b/289918974")
     @Test
     fun canStartAndStopAudioSource() {
         // Arrange.
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.kt
index 65ac2b3..c4bfa90 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewBitmapTest.kt
@@ -16,12 +16,16 @@
 package androidx.camera.view
 
 import android.content.Context
+import android.os.Build
+import android.view.WindowManager
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.Preview
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.Api27Impl.setShowWhenLocked
+import androidx.camera.testing.Api27Impl.setTurnScreenOn
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
@@ -305,6 +309,20 @@
             previewView.implementationMode = mode
             previewView.scaleType = scaleType
             activityRule.scenario.onActivity { activity: FakeActivity ->
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+                    activity.setShowWhenLocked()
+                    activity.setTurnScreenOn()
+                    activity.window.addFlags(
+                        WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                    )
+                } else {
+                    @Suppress("DEPRECATION")
+                    activity.window.addFlags(
+                        WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                            or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                            or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                    )
+                }
                 activity.setContentView(
                     previewView
                 )
@@ -348,6 +366,7 @@
         private const val CAMERA_LENS = CameraSelector.LENS_FACING_BACK
 
         @BeforeClass
+        @JvmStatic
         fun classSetUp() {
             CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
         }
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 f93db30..066282a 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
@@ -30,6 +30,7 @@
 import android.os.Build
 import android.os.Environment
 import android.provider.MediaStore
+import android.util.Pair
 import android.util.Rational
 import android.util.Size
 import android.view.Surface
@@ -1634,6 +1635,40 @@
         capturedImage_withHighResolutionEnabled(preview, imageAnalysis)
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 28)
+    fun getRealtimeCaptureLatencyEstimate_whenSessionProcessorSupportsRealtimeLatencyEstimate() =
+        runBlocking {
+            val expectedCaptureLatencyMillis = 1000L
+            val expectedProcessingLatencyMillis = 100L
+            val sessionProcessor = object : SessionProcessor by FakeSessionProcessor(
+                inputFormatPreview = null, // null means using the same output surface
+                inputFormatCapture = null
+            ) {
+                override fun getRealtimeCaptureLatency(): Pair<Long, Long> =
+                    Pair(expectedCaptureLatencyMillis, expectedProcessingLatencyMillis)
+            }
+
+            val imageCapture = ImageCapture.Builder().build()
+            val preview = Preview.Builder().build()
+
+            withContext(Dispatchers.Main) {
+                preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+                val cameraSelector =
+                    getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
+                cameraProvider.bindToLifecycle(
+                    fakeLifecycleOwner, cameraSelector, imageCapture, preview
+                )
+            }
+
+            val latencyEstimate = imageCapture.realtimeCaptureLatencyEstimate
+            // Check the realtime latency estimate is correct.
+            assertThat(latencyEstimate.captureLatencyMillis).isEqualTo(expectedCaptureLatencyMillis)
+            assertThat(latencyEstimate.processingLatencyMillis).isEqualTo(
+                expectedProcessingLatencyMillis
+            )
+        }
+
     private fun capturedImage_withHighResolutionEnabled(
         preview: Preview? = null,
         imageAnalysis: ImageAnalysis? = null
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerNestedScrollContentTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerNestedScrollContentTest.kt
index 23b0547..5a638f2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerNestedScrollContentTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerNestedScrollContentTest.kt
@@ -36,6 +36,7 @@
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusDirection
@@ -54,6 +55,7 @@
 import kotlin.math.abs
 import kotlin.math.absoluteValue
 import kotlin.test.assertTrue
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Test
@@ -384,7 +386,6 @@
 
         // Assert: Check we're settled.
         rule.runOnIdle {
-            assertThat(pagerState.currentPage).isEqualTo(5)
             assertThat(pagerState.currentPageOffsetFraction).isEqualTo(0.0f)
         }
 
@@ -395,6 +396,7 @@
         rule.runOnIdle { assertThat(focusItems).contains("page=5-item=3") }
 
         // Act: Move focus in inner scrollable
+        val previousPage = pagerState.currentPage
         rule.runOnIdle {
             assertTrue {
                 if (vertical) {
@@ -408,7 +410,7 @@
         // Assert: Check we actually scrolled, but didn't move pages.
         rule.runOnIdle {
             assertThat(focusItems).contains("page=5-item=4")
-            assertThat(pagerState.currentPage).isEqualTo(5)
+            assertThat(pagerState.currentPage).isEqualTo(previousPage)
             assertThat(pagerState.currentPageOffsetFraction).isEqualTo(0.0f)
         }
 
@@ -428,11 +430,63 @@
 
         // Assert: Check we moved pages.
         rule.runOnIdle {
-            assertThat(pagerState.currentPage).isEqualTo(6)
+            assertThat(focusItems).contains("page=6-item=0")
             assertThat(pagerState.currentPageOffsetFraction).isEqualTo(0.0f)
         }
     }
 
+    @Test
+    fun focusableContentInPage_focusMoveShouldNotLeavePagesInIntermediateState() {
+        lateinit var pagerFocusRequester: FocusRequester
+
+        createPager(
+            modifier = Modifier.fillMaxSize(),
+            pageCount = { DefaultPageCount },
+            initialPage = 3
+        ) { page ->
+            val focusRequester = remember {
+                FocusRequester().apply {
+                    if (page == 5) pagerFocusRequester = this
+                }
+            }
+
+            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                Box(
+                    modifier = Modifier
+                        .size(64.dp)
+                        .focusRequester(focusRequester)
+                        .focusable()
+                )
+            }
+        }
+
+        // Assert: Pager is settled
+        assertThat(pagerState.currentPageOffsetFraction).isEqualTo(0.0f)
+        assertThat(pagerState.currentPage).isEqualTo(3)
+
+        // Scroll to a page
+        rule.runOnIdle {
+            scope.launch {
+                pagerState.scrollToPage(5)
+            }
+        }
+
+        // Assert: Pager is settled
+        assertThat(pagerState.currentPageOffsetFraction).isEqualTo(0.0f)
+        assertThat(pagerState.currentPage).isEqualTo(5)
+
+        // Act: Request focus.
+        rule.runOnIdle {
+            pagerFocusRequester.requestFocus()
+        }
+
+        // Assert: Pager is settled
+        rule.runOnIdle {
+            assertThat(pagerState.currentPageOffsetFraction).isEqualTo(0.0f)
+            assertThat(pagerState.currentPage).isEqualTo(5)
+        }
+    }
+
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "{0}")
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
index ffb2dea..9f49b62 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
@@ -135,6 +135,8 @@
         orientation == Orientation.Vertical
     )
 
+    val pagerBringIntoViewScroller = remember(state) { PagerBringIntoViewScroller(state) }
+
     LazyLayout(
         modifier = modifier
             .then(state.remeasurementModifier)
@@ -167,7 +169,7 @@
                 state = state,
                 overscrollEffect = overscrollEffect,
                 enabled = userScrollEnabled,
-                bringIntoViewScroller = PagerBringIntoViewScroller
+                bringIntoViewScroller = pagerBringIntoViewScroller
             )
             .dragDirectionDetector(state)
             .nestedScroll(pageNestedScrollConnection),
@@ -286,26 +288,37 @@
     }
 
 @OptIn(ExperimentalFoundationApi::class)
-private val PagerBringIntoViewScroller = object : BringIntoViewScroller {
+private class PagerBringIntoViewScroller(val pagerState: PagerState) : BringIntoViewScroller {
 
     override val scrollAnimationSpec: AnimationSpec<Float> = spring()
 
+    /**
+     * [calculateScrollDistance] for Pager behaves differently than in a normal list. We must
+     * always respect the snapped pages over bringing a child into view. The logic here will
+     * behave like so:
+     *
+     * 1) If a child is outside of the view, start bringing it into view.
+     * 2) If a child's trailing edge is outside of the page bounds and the child is smaller than
+     * the page, scroll until the trailing edge is in view.
+     * 3) Once a child is fully in view, if it is smaller than the page, scroll until the page is
+     * settled.
+     * 4) If the child is larger than the page, scroll until it is partially in view and continue
+     * scrolling until the page is settled.
+     */
     override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float {
-        val trailingEdge = offset + size
-        val leadingEdge = offset
-
-        val sizeOfItemRequestingFocus = (trailingEdge - leadingEdge).absoluteValue
-        val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
-        val initialTargetForLeadingEdge = 0.0f
-        val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
-
-        val targetForLeadingEdge =
-            if (childSmallerThanParent && spaceAvailableToShowItem < sizeOfItemRequestingFocus) {
-                containerSize - sizeOfItemRequestingFocus
+        return if (offset >= containerSize || offset < 0) {
+            offset
+        } else {
+            if (size <= containerSize && (offset + size) > containerSize) {
+                offset // bring into view
             } else {
-                initialTargetForLeadingEdge
+                // are we in a settled position?
+                if (pagerState.currentPageOffsetFraction.absoluteValue == 0.0f) {
+                    0f
+                } else {
+                    offset
+                }
             }
-
-        return leadingEdge - targetForLeadingEdge
+        }
     }
 }
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 9cfcca0..d1c0de9 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -362,6 +362,11 @@
          */
         private val invalidated = IdentityArraySet<Any>()
 
+        /**
+         * Reusable vector for re-recording states inside [recordInvalidation]
+         */
+        private val statesToReread = mutableVectorOf<DerivedState<*>>()
+
         // derived state handling
 
         /**
@@ -555,7 +560,7 @@
                             }
                         } else {
                             // Re-read state to ensure its dependencies are up-to-date
-                            rereadDerivedState(derivedState)
+                            statesToReread.add(derivedState)
                         }
                     }
                 }
@@ -566,6 +571,13 @@
                 }
             }
 
+            if (statesToReread.isNotEmpty()) {
+                statesToReread.forEach {
+                    rereadDerivedState(it)
+                }
+                statesToReread.clear()
+            }
+
             return hasValues
         }
 
diff --git a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsJvm.kt b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsJvm.kt
index 02d3ff8..4aff6ca 100644
--- a/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsJvm.kt
+++ b/compose/runtime/runtime/src/nonEmulatorJvmTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsJvm.kt
@@ -207,6 +207,41 @@
         }
     }
 
+    @Test
+    fun rereadingDerivedState_whenDependenciesChanged() {
+        var changes = 0
+        val changeBlock: (Any) -> Unit = { changes++ }
+
+        val states =
+            List(2) { mutableStateOf(true) }
+                .sortedBy { System.identityHashCode(it) }
+
+        val derivedStates =
+            List(10) {
+                derivedStateOf {
+                    if (states[1].value) {
+                        states[0].value
+                    }
+                }
+            }
+
+        runSimpleTest { stateObserver, _ ->
+            // record observation for a draw scope
+            stateObserver.observeReads("draw", changeBlock) {
+                derivedStates.forEach { it.value }
+            }
+
+            Snapshot.sendApplyNotifications()
+
+            // flip the states to force re-recording value
+            states[0].value = false
+            states[1].value = false
+
+            Snapshot.sendApplyNotifications()
+        }
+        assertEquals(0, changes)
+    }
+
     private fun runSimpleTest(
         block: (modelObserver: SnapshotStateObserver, data: MutableState<Int>) -> Unit
     ) {
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 0ff4e01..9172836 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -144,7 +144,7 @@
     enum_constant public static final androidx.paging.LoadType REFRESH;
   }
 
-  @kotlin.jvm.JvmDefaultWithCompatibility public interface Logger {
+  public interface Logger {
     method public boolean isLoggable(int level);
     method public void log(int level, String message, optional Throwable? tr);
   }
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 0ff4e01..9172836 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -144,7 +144,7 @@
     enum_constant public static final androidx.paging.LoadType REFRESH;
   }
 
-  @kotlin.jvm.JvmDefaultWithCompatibility public interface Logger {
+  public interface Logger {
     method public boolean isLoggable(int level);
     method public void log(int level, String message, optional Throwable? tr);
   }
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/CachedPageEventFlow.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CachedPageEventFlow.kt
similarity index 99%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/CachedPageEventFlow.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/CachedPageEventFlow.kt
index 30ccada..c9b5d84 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/CachedPageEventFlow.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CachedPageEventFlow.kt
@@ -23,7 +23,6 @@
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.onSubscription
 import kotlinx.coroutines.flow.takeWhile
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/CachedPagingData.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CachedPagingData.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/CachedPagingData.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/CachedPagingData.kt
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeViewModel.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CompatLegacyPagingSource.kt
similarity index 62%
copy from bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeViewModel.kt
copy to paging/paging-common/src/commonMain/kotlin/androidx/paging/CompatLegacyPagingSource.kt
index 88b1c1c..951049d 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeViewModel.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/CompatLegacyPagingSource.kt
@@ -14,16 +14,17 @@
  * limitations under the License.
  */
 
-package androidx.bluetooth.integration.testapp.ui.home
+package androidx.paging
 
-import android.bluetooth.le.ScanResult
-import androidx.lifecycle.ViewModel
+import androidx.annotation.RestrictTo
 
-class HomeViewModel : ViewModel() {
+/**
+ * Backwards compatible interface for Paging2 LegacyPagingSource
+ *
+ * This interface allows PageFetcher to continue support for LegacyPagingSource in KMP structure
+ */
+internal interface CompatLegacyPagingSource {
 
-    private companion object {
-        private const val TAG = "HomeViewModel"
-    }
-
-    val scanResults = mutableMapOf<String, ScanResult>()
-}
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun setPageSize(pageSize: Int)
+}
\ No newline at end of file
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/ExperimentalPagingApi.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/ExperimentalPagingApi.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/ExperimentalPagingApi.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/ExperimentalPagingApi.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/FlowExt.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/FlowExt.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/FlowExt.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/FlowExt.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/HintHandler.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/HintHandler.kt
similarity index 98%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/HintHandler.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/HintHandler.kt
index 536688d..76f77a0 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/HintHandler.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/HintHandler.kt
@@ -22,7 +22,7 @@
 import androidx.paging.LoadType.APPEND
 import androidx.paging.LoadType.PREPEND
 import co.touchlab.stately.concurrency.Lock
-import kotlin.concurrent.withLock
+import co.touchlab.stately.concurrency.withLock
 import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/HintReceiver.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/HintReceiver.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/HintReceiver.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/HintReceiver.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt
similarity index 96%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt
index fdeb52f..e23d525 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidateCallbackTracker.kt
@@ -18,7 +18,7 @@
 
 import androidx.annotation.VisibleForTesting
 import co.touchlab.stately.concurrency.Lock
-import kotlin.concurrent.withLock
+import co.touchlab.stately.concurrency.withLock
 
 /**
  * Helper class for thread-safe invalidation callback tracking + triggering on registration.
@@ -74,7 +74,7 @@
     internal fun invalidate(): Boolean {
         if (invalid) return false
 
-        var callbacksToInvoke: List<T>?
+        var callbacksToInvoke: List<T>? = null
         lock.withLock {
             if (invalid) return false
 
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/Logger.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/Logger.kt
similarity index 97%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/Logger.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/Logger.kt
index b1564b8..fd5f074 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/Logger.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/Logger.kt
@@ -28,7 +28,6 @@
 
 public const val LOG_TAG: String = "Paging"
 
-@JvmDefaultWithCompatibility
 /**
  */
 public interface Logger {
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/MutableCombinedLoadStateCollection.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/MutableCombinedLoadStateCollection.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/MutableCombinedLoadStateCollection.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/MutableCombinedLoadStateCollection.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageEvent.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageEvent.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageEvent.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PageEvent.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageFetcher.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcher.kt
similarity index 99%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageFetcher.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcher.kt
index 1847a6b..88e80cc 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageFetcher.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcher.kt
@@ -208,7 +208,7 @@
         previousPagingSource: PagingSource<Key, Value>?
     ): PagingSource<Key, Value> {
         val pagingSource = pagingSourceFactory()
-        if (pagingSource is LegacyPagingSource) {
+        if (pagingSource is CompatLegacyPagingSource) {
             pagingSource.setPageSize(config.pageSize)
         }
         // Ensure pagingSourceFactory produces a new instance of PagingSource.
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageFetcherSnapshot.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcherSnapshot.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageFetcherSnapshot.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcherSnapshot.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageFetcherSnapshotState.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcherSnapshotState.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageFetcherSnapshotState.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PageFetcherSnapshotState.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagePresenter.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagePresenter.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagePresenter.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PagePresenter.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/Pager.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/Pager.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/Pager.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/Pager.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingConfig.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingConfig.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingConfig.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingConfig.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingData.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingData.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingData.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingData.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataDiffer.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataDiffer.kt
similarity index 99%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataDiffer.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataDiffer.kt
index 0635cfa..c3c8408 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataDiffer.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataDiffer.kt
@@ -17,7 +17,6 @@
 package androidx.paging
 
 import androidx.annotation.IntRange
-import androidx.annotation.MainThread
 import androidx.annotation.RestrictTo
 import androidx.paging.LoadType.APPEND
 import androidx.paging.LoadType.PREPEND
diff --git a/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataTransforms.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataTransforms.kt
new file mode 100644
index 0000000..4ec32fa
--- /dev/null
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingDataTransforms.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("PagingDataTransforms")
+@file:JvmMultifileClass
+
+package androidx.paging
+
+import androidx.annotation.CheckResult
+import androidx.paging.TerminalSeparatorType.FULLY_COMPLETE
+import kotlinx.coroutines.flow.map
+
+internal inline fun <T : Any, R : Any> PagingData<T>.transform(
+    crossinline transform: suspend (PageEvent<T>) -> PageEvent<R>
+) = PagingData(
+    flow = flow.map { transform(it) },
+    uiReceiver = uiReceiver,
+    hintReceiver = hintReceiver,
+)
+
+/**
+ * Returns a [PagingData] containing the result of applying the given [transform] to each
+ * element, as it is loaded.
+ */
+@CheckResult
+@JvmSynthetic
+public fun <T : Any, R : Any> PagingData<T>.map(
+    transform: suspend (T) -> R
+): PagingData<R> = transform { it.map(transform) }
+
+/**
+ * Returns a [PagingData] of all elements returned from applying the given [transform]
+ * to each element, as it is loaded.
+ */
+@CheckResult
+@JvmSynthetic
+public fun <T : Any, R : Any> PagingData<T>.flatMap(
+    transform: suspend (T) -> Iterable<R>
+): PagingData<R> = transform { it.flatMap(transform) }
+
+/**
+ * Returns a [PagingData] containing only elements matching the given [predicate]
+ */
+@CheckResult
+@JvmSynthetic
+public fun <T : Any> PagingData<T>.filter(
+    predicate: suspend (T) -> Boolean
+): PagingData<T> = transform { it.filter(predicate) }
+
+/**
+ * Returns a [PagingData] containing each original element, with an optional separator
+ * generated by [generator], given the elements before and after (or null, in boundary
+ * conditions).
+ *
+ * Note that this transform is applied asynchronously, as pages are loaded. Potential
+ * separators between pages are only computed once both pages are loaded.
+ *
+ * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
+ * footer are added.
+ *
+ * @param generator Generator function used to construct a separator item given the item before
+ * and the item after. For terminal separators (header and footer), the arguments passed to the
+ * generator, `before` and `after`, will be `null` respectively. In cases where the fully paginated
+ * list is empty, a single separator will be added where both `before` and `after` items are `null`.
+ *
+ * @sample androidx.paging.samples.insertSeparatorsSample
+ * @sample androidx.paging.samples.insertSeparatorsUiModelSample
+ */
+@CheckResult
+@JvmSynthetic
+public fun <T : R, R : Any> PagingData<T>.insertSeparators(
+    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
+    generator: suspend (T?, T?) -> R?,
+): PagingData<R> {
+    // This function must be an extension method, as it indirectly imposes a constraint on
+    // the type of T (because T extends R). Ideally it would be declared not be an
+    // extension, to make this method discoverable for Java callers, but we need to support
+    // the common UI model pattern for separators:
+    //     class UiModel
+    //     class ItemModel: UiModel
+    //     class SeparatorModel: UiModel
+    return PagingData(
+        flow = flow.insertEventSeparators(terminalSeparatorType, generator),
+        uiReceiver = uiReceiver,
+        hintReceiver = hintReceiver
+    )
+}
+
+/**
+ * Returns a [PagingData] containing each original element, with the passed header [item] added
+ * to the start of the list.
+ *
+ * The header [item] is added to a loaded page which marks the end of the data stream in the
+ * [LoadType.PREPEND] direction by returning null in [PagingSource.LoadResult.Page.prevKey]. It
+ * will be removed if the first page in the list is dropped, which can happen in the case of loaded
+ * pages exceeding [PagingConfig.maxSize].
+ *
+ * Note: This operation is not idempotent, calling it multiple times will continually add
+ * more headers to the start of the list, which can be useful if multiple header items are
+ * required.
+ *
+ * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
+ * footer are added.
+ *
+ * @param item The header to add to the front of the list once it is fully loaded in the
+ * [LoadType.PREPEND] direction.
+ *
+ * @see [insertFooterItem]
+ */
+@CheckResult
+@JvmOverloads
+public fun <T : Any> PagingData<T>.insertHeaderItem(
+    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
+    item: T,
+): PagingData<T> = insertSeparators(terminalSeparatorType) { before, _ ->
+    if (before == null) item else null
+}
+
+/**
+ * Returns a [PagingData] containing each original element, with the passed footer [item] added
+ * to the end of the list.
+ *
+ * The footer [item] is added to a loaded page which marks the end of the data stream in the
+ * [LoadType.APPEND] direction, either by returning null in [PagingSource.LoadResult.Page.nextKey].
+ * It will be removed if the last page in the list is dropped, which can happen in the case of
+ * loaded pages exceeding [PagingConfig.maxSize].
+ *
+ * Note: This operation is not idempotent, calling it multiple times will continually add
+ * more footers to the end of the list, which can be useful if multiple footer items are
+ * required.
+ *
+ * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
+ * footer are added.
+ *
+ * @param item The footer to add to the end of the list once it is fully loaded in the
+ * [LoadType.APPEND] direction.
+ *
+ * @see [insertHeaderItem]
+ */
+@CheckResult
+@JvmOverloads
+public fun <T : Any> PagingData<T>.insertFooterItem(
+    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
+    item: T,
+): PagingData<T> = insertSeparators(terminalSeparatorType) { _, after ->
+    if (after == null) item else null
+}
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingSource.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingSource.kt
similarity index 97%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingSource.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingSource.kt
index 61d0718..1a95da2 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingSource.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingSource.kt
@@ -17,21 +17,9 @@
 package androidx.paging
 
 import androidx.annotation.IntRange
-import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import androidx.paging.LoadType.REFRESH
 
-/** @suppress */
-@Suppress("DEPRECATION")
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun <Key : Any> PagedList.Config.toRefreshLoadParams(
-    key: Key?
-): PagingSource.LoadParams<Key> = PagingSource.LoadParams.Refresh(
-    key,
-    initialLoadSizeHint,
-    enablePlaceholders,
-)
-
 /**
  * Base class for an abstraction of pageable static data from some source, where loading pages
  * of data is typically an expensive operation. Some examples of common [PagingSource]s might be
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingSourceFactory.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingSourceFactory.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingSourceFactory.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingSourceFactory.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingState.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingState.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingState.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/PagingState.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/RemoteMediator.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediator.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/RemoteMediator.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediator.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt
similarity index 99%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt
index b6f5b1a..f2725a1 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/RemoteMediatorAccessor.kt
@@ -21,7 +21,7 @@
 import androidx.paging.AccessorState.BlockState.UNBLOCKED
 import androidx.paging.RemoteMediator.MediatorResult
 import co.touchlab.stately.concurrency.Lock
-import kotlin.concurrent.withLock
+import co.touchlab.stately.concurrency.withLock
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/Separators.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/Separators.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/Separators.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/Separators.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/SingleRunner.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/SingleRunner.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/SingleRunner.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/SingleRunner.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/TransformablePage.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/TransformablePage.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/TransformablePage.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/TransformablePage.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/ViewportHint.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/ViewportHint.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/ViewportHint.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/ViewportHint.kt
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt b/paging/paging-common/src/commonMain/kotlin/androidx/paging/annotation.kt
similarity index 66%
rename from bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt
rename to paging/paging-common/src/commonMain/kotlin/androidx/paging/annotation.kt
index f2fb27d..5d73136 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt
+++ b/paging/paging-common/src/commonMain/kotlin/androidx/paging/annotation.kt
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.bluetooth.integration.testapp.experimental
+package androidx.paging
 
-enum class AdvertiseResult {
-    ADVERTISE_STARTED,
-    ADVERTISE_FAILED_ALREADY_STARTED,
-    ADVERTISE_FAILED_DATA_TOO_LARGE,
-    ADVERTISE_FAILED_FEATURE_UNSUPPORTED,
-    ADVERTISE_FAILED_INTERNAL_ERROR,
-    ADVERTISE_FAILED_TOO_MANY_ADVERTISERS
-}
+public expect annotation class MainThread()
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/CachedPageEventFlowLeakTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/CachedPageEventFlowLeakTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/CachedPageEventFlowLeakTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/CachedPageEventFlowLeakTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/CachedPageEventFlowTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/CachedPageEventFlowTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/CachedPageEventFlowTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/CachedPageEventFlowTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/CachingTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/CachingTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/CachingTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/CachingTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/FailDispatcher.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/FailDispatcher.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/FailDispatcher.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/FailDispatcher.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/FlattenedPageEventStorageTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/FlattenedPageEventStorageTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/FlattenedPageEventStorageTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/FlattenedPageEventStorageTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/FlowExtTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/FlowExtTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/FlowExtTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/FlowExtTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/GarbageCollectionTestHelper.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/GarbageCollectionTestHelper.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/HeaderFooterTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/HeaderFooterTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/HeaderFooterTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/HeaderFooterTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/HintHandlerTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/HintHandlerTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/HintHandlerTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/HintHandlerTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PageEventTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PageEventTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PageEventTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PageEventTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PageFetcherSnapshotStateTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PageFetcherSnapshotStateTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PageFetcherSnapshotStateTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PageFetcherSnapshotStateTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PageFetcherSnapshotTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PageFetcherTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PageFetcherTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PageFetcherTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PageFetcherTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagePresenterTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagePresenterTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagePresenterTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PagePresenterTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingConfigTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingConfigTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingConfigTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingConfigTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingDataDifferTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataDifferTest.kt
similarity index 99%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingDataDifferTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataDifferTest.kt
index d9fd717..0128918 100644
--- a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingDataDifferTest.kt
+++ b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingDataDifferTest.kt
@@ -1428,7 +1428,7 @@
         ) {
             TestPagingSource().also {
                 if (generation == 0) {
-                    it.nextLoadResult = PagingSource.LoadResult.Invalid()
+                    it.nextLoadResult = LoadResult.Invalid()
                 }
                 generation++
             }
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingSourceTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingSourceTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingSourceTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingSourceTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingStateTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingStateTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/PagingStateTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/PagingStateTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/ProcessPageEventCallbackCapture.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/ProcessPageEventCallbackCapture.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/ProcessPageEventCallbackCapture.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/ProcessPageEventCallbackCapture.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/SeparatorsTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/SeparatorsTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/SeparatorsTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/SeparatorsTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/SimpleChannelFlowTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/SimpleChannelFlowTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/SimpleChannelFlowTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/SimpleChannelFlowTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/SimpleTransformLatestTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/SimpleTransformLatestTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/SimpleTransformLatestTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/SimpleTransformLatestTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/SingleRunnerTest.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/SingleRunnerTest.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/SingleRunnerTest.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/SingleRunnerTest.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/StateChange.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/StateChange.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/StateChange.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/StateChange.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/TestPagingSourceExt.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/TestPagingSourceExt.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/TestPagingSourceExt.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/TestPagingSourceExt.kt
diff --git a/paging/paging-common/src/jvmTest/kotlin/androidx/paging/TestUtils.kt b/paging/paging-common/src/commonTest/kotlin/androidx/paging/TestUtils.kt
similarity index 100%
rename from paging/paging-common/src/jvmTest/kotlin/androidx/paging/TestUtils.kt
rename to paging/paging-common/src/commonTest/kotlin/androidx/paging/TestUtils.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/ContiguousPagedList.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/ContiguousPagedList.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/ContiguousPagedList.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/ContiguousPagedList.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/DataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/DataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/DataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/DataSource.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/InitialDataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/InitialDataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/InitialDataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/InitialDataSource.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/InitialPagedList.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/InitialPagedList.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/InitialPagedList.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/InitialPagedList.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/ItemKeyedDataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/ItemKeyedDataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/ItemKeyedDataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/ItemKeyedDataSource.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPageFetcher.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPageFetcher.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPageFetcher.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPageFetcher.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPagingSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPagingSource.jvm.kt
similarity index 97%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPagingSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPagingSource.jvm.kt
index b3625ac1..a3d9b27 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPagingSource.kt
+++ b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/LegacyPagingSource.jvm.kt
@@ -38,7 +38,7 @@
 public class LegacyPagingSource<Key : Any, Value : Any>(
     private val fetchContext: CoroutineContext,
     internal val dataSource: DataSource<Key, Value>
-) : PagingSource<Key, Value>() {
+) : PagingSource<Key, Value>(), CompatLegacyPagingSource {
     private var pageSize: Int = PAGE_SIZE_NOT_SET
 
     init {
@@ -54,7 +54,7 @@
     /**
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public fun setPageSize(pageSize: Int) {
+    public override fun setPageSize(pageSize: Int) {
         check(this.pageSize == PAGE_SIZE_NOT_SET || pageSize == this.pageSize) {
             "Page size is already set to ${this.pageSize}."
         }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/MainThread.kt
similarity index 66%
copy from bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt
copy to paging/paging-common/src/jvmMain/kotlin/androidx/paging/MainThread.kt
index f2fb27d..7705b0d 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt
+++ b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/MainThread.kt
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.bluetooth.integration.testapp.experimental
+package androidx.paging
 
-enum class AdvertiseResult {
-    ADVERTISE_STARTED,
-    ADVERTISE_FAILED_ALREADY_STARTED,
-    ADVERTISE_FAILED_DATA_TOO_LARGE,
-    ADVERTISE_FAILED_FEATURE_UNSUPPORTED,
-    ADVERTISE_FAILED_INTERNAL_ERROR,
-    ADVERTISE_FAILED_TOO_MANY_ADVERTISERS
-}
+public actual typealias MainThread = androidx.annotation.MainThread
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageKeyedDataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageKeyedDataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageKeyedDataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/PageKeyedDataSource.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagedList.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagedList.kt
index 47706b6..2a5d881 100644
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagedList.kt
+++ b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagedList.kt
@@ -1311,3 +1311,14 @@
         .setInitialKey(initialKey)
         .build()
 }
+
+/** @suppress */
+@Suppress("DEPRECATION")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun <Key : Any> PagedList.Config.toRefreshLoadParams(
+    key: Key?
+): PagingSource.LoadParams<Key> = PagingSource.LoadParams.Refresh(
+    key,
+    initialLoadSizeHint,
+    enablePlaceholders,
+)
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagedStorage.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagedStorage.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagedStorage.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagedStorage.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataTransforms.jvm.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataTransforms.jvm.kt
new file mode 100644
index 0000000..0f3be5f
--- /dev/null
+++ b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataTransforms.jvm.kt
@@ -0,0 +1,190 @@
+/*
+ * 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:JvmName("PagingDataTransforms")
+@file:JvmMultifileClass
+
+package androidx.paging
+
+import androidx.annotation.CheckResult
+import java.util.concurrent.Executor
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * Returns a [PagingData] containing the result of applying the given [transform] to each
+ * element, as it is loaded.
+ *
+ * @see PagingData.map
+ */
+@CheckResult
+public fun <T : Any, R : Any> PagingData<T>.map(
+    executor: Executor,
+    transform: (T) -> R,
+): PagingData<R> = transform { event ->
+    withContext(executor.asCoroutineDispatcher()) {
+        event.map { transform(it) }
+    }
+}
+
+/**
+ * Returns a [PagingData] of all elements returned from applying the given [transform]
+ * to each element, as it is loaded.
+ *
+ * @see flatMap
+ */
+@CheckResult
+public fun <T : Any, R : Any> PagingData<T>.flatMap(
+    executor: Executor,
+    transform: (T) -> Iterable<R>
+): PagingData<R> = transform { event ->
+    withContext(executor.asCoroutineDispatcher()) {
+        event.flatMap { transform(it) }
+    }
+}
+
+/**
+ * Returns a [PagingData] containing only elements matching the given [predicate].
+ *
+ * @see filter
+ */
+@CheckResult
+@JvmName("filter")
+public fun <T : Any> PagingData<T>.filter(
+    executor: Executor,
+    predicate: (T) -> Boolean
+): PagingData<T> = transform { event ->
+    withContext(executor.asCoroutineDispatcher()) {
+        event.filter { predicate(it) }
+    }
+}
+
+// NOTE: samples in the doc below are manually imported from Java code in the samples
+// project, since Java cannot be linked with @sample.
+// DO NOT CHANGE THE BELOW COMMENT WITHOUT MAKING THE CORRESPONDING CHANGE IN `samples/`
+/**
+ *
+ * Returns a [PagingData] containing each original element, with an optional separator
+ * generated by [generator], given the elements before and after (or null, in boundary
+ * conditions).
+ *
+ * Note that this transform is applied asynchronously, as pages are loaded. Potential
+ * separators between pages are only computed once both pages are loaded.
+ *
+ * **Kotlin callers should instead use the suspending extension function variant of
+ * insertSeparators**
+ *
+ * ```
+ * /*
+ *  * Create letter separators in an alphabetically sorted list.
+ *  *
+ *  * For example, if the input is:
+ *  *     "apple", "apricot", "banana", "carrot"
+ *  *
+ *  * The operator would output:
+ *  *     "A", "apple", "apricot", "B", "banana", "C", "carrot"
+ *  */
+ * pagingDataStream.map(pagingData ->
+ *         // map outer stream, so we can perform transformations on each paging generation
+ *         PagingDataTransforms.insertSeparators(pagingData, bgExecutor,
+ *                 (@Nullable String before, @Nullable String after) -> {
+ *                     if (after != null && (before == null
+ *                             || before.charAt(0) != after.charAt(0))) {
+ *                         // separator - after is first item that starts with its first
+ *                         // letter
+ *                         return Character.toString(
+ *                                 Character.toUpperCase(after.charAt(0)));
+ *                     } else {
+ *                         // no separator - either end of list, or first
+ *                         // letters of items are the same
+ *                         return null;
+ *                     }
+ *                 }));
+ *
+ * /*
+ *  * Create letter separators in an alphabetically sorted list of Items, with UiModel
+ *  * objects.
+ *  *
+ *  * For example, if the input is (each an `Item`):
+ *  *     "apple", "apricot", "banana", "carrot"
+ *  *
+ *  * The operator would output a list of UiModels corresponding to:
+ *  *     "A", "apple", "apricot", "B", "banana", "C", "carrot"
+ *  */
+ * pagingDataStream.map(itemPagingData -> {
+ *     // map outer stream, so we can perform transformations on each paging generation
+ *
+ *     // first convert items in stream to UiModel.Item
+ *     PagingData<UiModel.ItemModel> itemModelPagingData = PagingDataTransforms.map(
+ *             itemPagingData, bgExecutor, UiModel.ItemModel::new);
+ *
+ *     // Now insert UiModel.Separators, which makes the PagingData of generic type UiModel
+ *     return PagingDataTransforms.insertSeparators(
+ *             itemModelPagingData, bgExecutor,
+ *             (@Nullable UiModel.ItemModel before, @Nullable UiModel.ItemModel after) -> {
+ *                 if (after != null && (before == null
+ *                         || before.item.label.charAt(0) != after.item.label.charAt(0))) {
+ *                     // separator - after is first item that starts with its first letter
+ *                     return new UiModel.SeparatorModel(
+ *                             Character.toUpperCase(after.item.label.charAt(0)));
+ *                 } else {
+ *                     // no separator - either end of list, or first
+ *                     // letters of items are the same
+ *                     return null;
+ *                 }
+ *             });
+ * });
+ *
+ * public class UiModel {
+ *     static class ItemModel extends UiModel {
+ *         public Item item;
+ *         ItemModel(Item item) {
+ *             this.item = item;
+ *         }
+ *     }
+ *     static class SeparatorModel extends UiModel {
+ *         public char character;
+ *         SeparatorModel(char character) {
+ *             this.character = character;
+ *         }
+ *     }
+ * }
+ * ```
+ *
+ * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
+ * footer are added.
+ *
+ * @param executor [Executor] to run the [generator] function in.
+ *
+ * @param generator Generator function used to construct a separator item given the item before
+ * and the item after. For terminal separators (header and footer), the arguments passed to the
+ * generator, `before` and `after`, will be `null` respectively. In cases where the fully paginated
+ * list is empty, a single separator will be added where both `before` and `after` items are `null`.
+ *
+ */
+@CheckResult
+@JvmOverloads
+public fun <R : Any, T : R> PagingData<T>.insertSeparators(
+    terminalSeparatorType: TerminalSeparatorType = TerminalSeparatorType.FULLY_COMPLETE,
+    executor: Executor,
+    generator: (T?, T?) -> R?,
+): PagingData<R> {
+    return insertSeparators(terminalSeparatorType) { before, after ->
+        withContext(executor.asCoroutineDispatcher()) {
+            generator(before, after)
+        }
+    }
+}
\ No newline at end of file
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataTransforms.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataTransforms.kt
deleted file mode 100644
index 0a31f4e..0000000
--- a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PagingDataTransforms.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * 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.
- */
-
-@file:JvmName("PagingDataTransforms")
-
-package androidx.paging
-
-import androidx.annotation.CheckResult
-import androidx.paging.TerminalSeparatorType.FULLY_COMPLETE
-import java.util.concurrent.Executor
-import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.withContext
-
-private inline fun <T : Any, R : Any> PagingData<T>.transform(
-    crossinline transform: suspend (PageEvent<T>) -> PageEvent<R>
-) = PagingData(
-    flow = flow.map { transform(it) },
-    uiReceiver = uiReceiver,
-    hintReceiver = hintReceiver,
-)
-
-/**
- * Returns a [PagingData] containing the result of applying the given [transform] to each
- * element, as it is loaded.
- */
-@CheckResult
-@JvmSynthetic
-public fun <T : Any, R : Any> PagingData<T>.map(
-    transform: suspend (T) -> R
-): PagingData<R> = transform { it.map(transform) }
-
-/**
- * Returns a [PagingData] containing the result of applying the given [transform] to each
- * element, as it is loaded.
- *
- * @see PagingData.map
- */
-@CheckResult
-public fun <T : Any, R : Any> PagingData<T>.map(
-    executor: Executor,
-    transform: (T) -> R,
-): PagingData<R> = transform { event ->
-    withContext(executor.asCoroutineDispatcher()) {
-        event.map { transform(it) }
-    }
-}
-
-/**
- * Returns a [PagingData] of all elements returned from applying the given [transform]
- * to each element, as it is loaded.
- */
-@CheckResult
-@JvmSynthetic
-public fun <T : Any, R : Any> PagingData<T>.flatMap(
-    transform: suspend (T) -> Iterable<R>
-): PagingData<R> = transform { it.flatMap(transform) }
-
-/**
- * Returns a [PagingData] of all elements returned from applying the given [transform]
- * to each element, as it is loaded.
- *
- * @see flatMap
- */
-@CheckResult
-public fun <T : Any, R : Any> PagingData<T>.flatMap(
-    executor: Executor,
-    transform: (T) -> Iterable<R>
-): PagingData<R> = transform { event ->
-    withContext(executor.asCoroutineDispatcher()) {
-        event.flatMap { transform(it) }
-    }
-}
-
-/**
- * Returns a [PagingData] containing only elements matching the given [predicate]
- */
-@CheckResult
-@JvmSynthetic
-public fun <T : Any> PagingData<T>.filter(
-    predicate: suspend (T) -> Boolean
-): PagingData<T> = transform { it.filter(predicate) }
-
-/**
- * Returns a [PagingData] containing only elements matching the given [predicate].
- *
- * @see filter
- */
-@CheckResult
-@JvmName("filter")
-public fun <T : Any> PagingData<T>.filter(
-    executor: Executor,
-    predicate: (T) -> Boolean
-): PagingData<T> = transform { event ->
-    withContext(executor.asCoroutineDispatcher()) {
-        event.filter { predicate(it) }
-    }
-}
-
-/**
- * Returns a [PagingData] containing each original element, with an optional separator
- * generated by [generator], given the elements before and after (or null, in boundary
- * conditions).
- *
- * Note that this transform is applied asynchronously, as pages are loaded. Potential
- * separators between pages are only computed once both pages are loaded.
- *
- * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
- * footer are added.
- *
- * @param generator Generator function used to construct a separator item given the item before
- * and the item after. For terminal separators (header and footer), the arguments passed to the
- * generator, `before` and `after`, will be `null` respectively. In cases where the fully paginated
- * list is empty, a single separator will be added where both `before` and `after` items are `null`.
- *
- * @sample androidx.paging.samples.insertSeparatorsSample
- * @sample androidx.paging.samples.insertSeparatorsUiModelSample
- */
-@CheckResult
-@JvmSynthetic
-public fun <T : R, R : Any> PagingData<T>.insertSeparators(
-    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
-    generator: suspend (T?, T?) -> R?,
-): PagingData<R> {
-    // This function must be an extension method, as it indirectly imposes a constraint on
-    // the type of T (because T extends R). Ideally it would be declared not be an
-    // extension, to make this method discoverable for Java callers, but we need to support
-    // the common UI model pattern for separators:
-    //     class UiModel
-    //     class ItemModel: UiModel
-    //     class SeparatorModel: UiModel
-    return PagingData(
-        flow = flow.insertEventSeparators(terminalSeparatorType, generator),
-        uiReceiver = uiReceiver,
-        hintReceiver = hintReceiver
-    )
-}
-
-// NOTE: samples in the doc below are manually imported from Java code in the samples
-// project, since Java cannot be linked with @sample.
-// DO NOT CHANGE THE BELOW COMMENT WITHOUT MAKING THE CORRESPONDING CHANGE IN `samples/`
-/**
- *
- * Returns a [PagingData] containing each original element, with an optional separator
- * generated by [generator], given the elements before and after (or null, in boundary
- * conditions).
- *
- * Note that this transform is applied asynchronously, as pages are loaded. Potential
- * separators between pages are only computed once both pages are loaded.
- *
- * **Kotlin callers should instead use the suspending extension function variant of
- * insertSeparators**
- *
- * ```
- * /*
- *  * Create letter separators in an alphabetically sorted list.
- *  *
- *  * For example, if the input is:
- *  *     "apple", "apricot", "banana", "carrot"
- *  *
- *  * The operator would output:
- *  *     "A", "apple", "apricot", "B", "banana", "C", "carrot"
- *  */
- * pagingDataStream.map(pagingData ->
- *         // map outer stream, so we can perform transformations on each paging generation
- *         PagingDataTransforms.insertSeparators(pagingData, bgExecutor,
- *                 (@Nullable String before, @Nullable String after) -> {
- *                     if (after != null && (before == null
- *                             || before.charAt(0) != after.charAt(0))) {
- *                         // separator - after is first item that starts with its first
- *                         // letter
- *                         return Character.toString(
- *                                 Character.toUpperCase(after.charAt(0)));
- *                     } else {
- *                         // no separator - either end of list, or first
- *                         // letters of items are the same
- *                         return null;
- *                     }
- *                 }));
- *
- * /*
- *  * Create letter separators in an alphabetically sorted list of Items, with UiModel
- *  * objects.
- *  *
- *  * For example, if the input is (each an `Item`):
- *  *     "apple", "apricot", "banana", "carrot"
- *  *
- *  * The operator would output a list of UiModels corresponding to:
- *  *     "A", "apple", "apricot", "B", "banana", "C", "carrot"
- *  */
- * pagingDataStream.map(itemPagingData -> {
- *     // map outer stream, so we can perform transformations on each paging generation
- *
- *     // first convert items in stream to UiModel.Item
- *     PagingData<UiModel.ItemModel> itemModelPagingData = PagingDataTransforms.map(
- *             itemPagingData, bgExecutor, UiModel.ItemModel::new);
- *
- *     // Now insert UiModel.Separators, which makes the PagingData of generic type UiModel
- *     return PagingDataTransforms.insertSeparators(
- *             itemModelPagingData, bgExecutor,
- *             (@Nullable UiModel.ItemModel before, @Nullable UiModel.ItemModel after) -> {
- *                 if (after != null && (before == null
- *                         || before.item.label.charAt(0) != after.item.label.charAt(0))) {
- *                     // separator - after is first item that starts with its first letter
- *                     return new UiModel.SeparatorModel(
- *                             Character.toUpperCase(after.item.label.charAt(0)));
- *                 } else {
- *                     // no separator - either end of list, or first
- *                     // letters of items are the same
- *                     return null;
- *                 }
- *             });
- * });
- *
- * public class UiModel {
- *     static class ItemModel extends UiModel {
- *         public Item item;
- *         ItemModel(Item item) {
- *             this.item = item;
- *         }
- *     }
- *     static class SeparatorModel extends UiModel {
- *         public char character;
- *         SeparatorModel(char character) {
- *             this.character = character;
- *         }
- *     }
- * }
- * ```
- *
- * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
- * footer are added.
- *
- * @param executor [Executor] to run the [generator] function in.
- *
- * @param generator Generator function used to construct a separator item given the item before
- * and the item after. For terminal separators (header and footer), the arguments passed to the
- * generator, `before` and `after`, will be `null` respectively. In cases where the fully paginated
- * list is empty, a single separator will be added where both `before` and `after` items are `null`.
- *
- */
-@CheckResult
-@JvmOverloads
-public fun <R : Any, T : R> PagingData<T>.insertSeparators(
-    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
-    executor: Executor,
-    generator: (T?, T?) -> R?,
-): PagingData<R> {
-    return insertSeparators(terminalSeparatorType) { before, after ->
-        withContext(executor.asCoroutineDispatcher()) {
-            generator(before, after)
-        }
-    }
-}
-
-/**
- * Returns a [PagingData] containing each original element, with the passed header [item] added
- * to the start of the list.
- *
- * The header [item] is added to a loaded page which marks the end of the data stream in the
- * [LoadType.PREPEND] direction by returning null in [PagingSource.LoadResult.Page.prevKey]. It
- * will be removed if the first page in the list is dropped, which can happen in the case of loaded
- * pages exceeding [PagedList.Config.maxSize].
- *
- * Note: This operation is not idempotent, calling it multiple times will continually add
- * more headers to the start of the list, which can be useful if multiple header items are
- * required.
- *
- * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
- * footer are added.
- *
- * @param item The header to add to the front of the list once it is fully loaded in the
- * [LoadType.PREPEND] direction.
- *
- * @see [insertFooterItem]
- */
-@CheckResult
-@JvmOverloads
-public fun <T : Any> PagingData<T>.insertHeaderItem(
-    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
-    item: T,
-): PagingData<T> = insertSeparators(terminalSeparatorType) { before, _ ->
-    if (before == null) item else null
-}
-
-/**
- * Returns a [PagingData] containing each original element, with the passed footer [item] added
- * to the end of the list.
- *
- * The footer [item] is added to a loaded page which marks the end of the data stream in the
- * [LoadType.APPEND] direction, either by returning null in [PagingSource.LoadResult.Page.nextKey].
- * It will be removed if the last page in the list is dropped, which can happen in the case of
- * loaded pages exceeding [PagedList.Config.maxSize].
- *
- * Note: This operation is not idempotent, calling it multiple times will continually add
- * more footers to the end of the list, which can be useful if multiple footer items are
- * required.
- *
- * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
- * footer are added.
- *
- * @param item The footer to add to the end of the list once it is fully loaded in the
- * [LoadType.APPEND] direction.
- *
- * @see [insertHeaderItem]
- */
-@CheckResult
-@JvmOverloads
-public fun <T : Any> PagingData<T>.insertFooterItem(
-    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
-    item: T,
-): PagingData<T> = insertSeparators(terminalSeparatorType) { _, after ->
-    if (after == null) item else null
-}
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PositionalDataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/PositionalDataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/PositionalDataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/PositionalDataSource.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/SnapshotPagedList.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/SnapshotPagedList.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/SnapshotPagedList.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/SnapshotPagedList.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperDataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperDataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperDataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperDataSource.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperItemKeyedDataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperItemKeyedDataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperItemKeyedDataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperItemKeyedDataSource.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperPageKeyedDataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperPageKeyedDataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperPageKeyedDataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperPageKeyedDataSource.jvm.kt
diff --git a/paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperPositionalDataSource.kt b/paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperPositionalDataSource.jvm.kt
similarity index 100%
rename from paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperPositionalDataSource.kt
rename to paging/paging-common/src/jvmMain/kotlin/androidx/paging/WrapperPositionalDataSource.jvm.kt
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt b/paging/paging-common/src/nativeMain/kotlin/androidx/paging/annotation.kt
similarity index 66%
copy from bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt
copy to paging/paging-common/src/nativeMain/kotlin/androidx/paging/annotation.kt
index f2fb27d..ce1d1a6 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt
+++ b/paging/paging-common/src/nativeMain/kotlin/androidx/paging/annotation.kt
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.bluetooth.integration.testapp.experimental
+package androidx.paging
 
-enum class AdvertiseResult {
-    ADVERTISE_STARTED,
-    ADVERTISE_FAILED_ALREADY_STARTED,
-    ADVERTISE_FAILED_DATA_TOO_LARGE,
-    ADVERTISE_FAILED_FEATURE_UNSUPPORTED,
-    ADVERTISE_FAILED_INTERNAL_ERROR,
-    ADVERTISE_FAILED_TOO_MANY_ADVERTISERS
-}
+public actual annotation class MainThread()
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index 1dc67e0..498d6de 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -26,7 +26,10 @@
     api(libs.kotlinStdlib)
     api("androidx.annotation:annotation:1.1.0")
 
-    implementation("androidx.core:core:1.7.0")
+    // For BundleCompat#putBinder.
+    // TODO(b/280561849): Use stable version when available.
+    implementation("androidx.core:core:1.12.0-alpha05")
+
     implementation("androidx.lifecycle:lifecycle-common:2.2.0")
     implementation project(path: ':privacysandbox:sdkruntime:sdkruntime-client')
     implementation project(path: ':privacysandbox:ui:ui-core')
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt
index ef932dd..2d4e082 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt
@@ -16,16 +16,12 @@
 
 @file:JvmName("SdkActivityLaunchers")
 
-// TODO(b/282918396): Stop using app.BundleCompat and change it to os.BundleCompat when permission
-// issue is fixed.
-@file:Suppress("DEPRECATION")
-
 package androidx.privacysandbox.ui.client
 
 import android.app.Activity
 import android.os.Bundle
 import android.os.IBinder
-import androidx.core.app.BundleCompat
+import androidx.core.os.BundleCompat
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import androidx.privacysandbox.sdkruntime.client.SdkSandboxManagerCompat
diff --git a/privacysandbox/ui/ui-provider/build.gradle b/privacysandbox/ui/ui-provider/build.gradle
index 3ff3156..68339e0 100644
--- a/privacysandbox/ui/ui-provider/build.gradle
+++ b/privacysandbox/ui/ui-provider/build.gradle
@@ -27,7 +27,9 @@
     api("androidx.annotation:annotation:1.1.0")
 
     implementation project(path: ':privacysandbox:ui:ui-core')
-    implementation("androidx.core:core:1.7.0")
+    // For BundleCompat#getBinder.
+    // TODO(b/280561849): Use stable version when available.
+    implementation("androidx.core:core:1.12.0-alpha05")
     implementation(libs.kotlinCoroutinesCore)
 
     androidTestImplementation(libs.junit)
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt
index 622a46f..9d73f49 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt
@@ -14,15 +14,11 @@
  * limitations under the License.
  */
 
-// TODO(b/282918396): Stop using app.BundleCompat and change it to os.BundleCompat when permission
-// issue is fixed.
-@file:Suppress("DEPRECATION")
-
 package androidx.privacysandbox.ui.provider
 
 import android.os.Bundle
 import android.os.IBinder
-import androidx.core.app.BundleCompat
+import androidx.core.os.BundleCompat
 import androidx.privacysandbox.ui.core.ISdkActivityLauncher
 import androidx.privacysandbox.ui.core.ISdkActivityLauncherCallback
 import androidx.privacysandbox.ui.core.ProtocolConstants.sdkActivityLauncherBinderKey
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 123b07a..2283ff7 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -28,7 +28,11 @@
 
   public final class CompositionLocalsKt {
     method @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.foundation.ReduceMotion> getLocalReduceMotion();
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalSwipeToDismissBackgroundScrimColor();
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalSwipeToDismissContentScrimColor();
     property @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.foundation.ReduceMotion> LocalReduceMotion;
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalSwipeToDismissBackgroundScrimColor;
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalSwipeToDismissContentScrimColor;
   }
 
   public interface CurvedAlignment {
@@ -249,6 +253,46 @@
     property public final int Revealing;
   }
 
+  public final class SwipeToDismissBoxDefaults {
+    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
+    method public float getEdgeWidth();
+    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> AnimationSpec;
+    property public final float EdgeWidth;
+    field public static final androidx.wear.compose.foundation.SwipeToDismissBoxDefaults INSTANCE;
+  }
+
+  public final class SwipeToDismissBoxKt {
+    method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional androidx.compose.ui.Modifier modifier, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissed, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
+    method public static androidx.compose.ui.Modifier edgeSwipeToDismiss(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.SwipeToDismissBoxState swipeToDismissBoxState, optional float edgeWidth);
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.SwipeToDismissBoxState rememberSwipeToDismissBoxState(optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.SwipeToDismissValue,java.lang.Boolean> confirmStateChange);
+  }
+
+  @androidx.compose.runtime.Stable public final class SwipeToDismissBoxState {
+    ctor public SwipeToDismissBoxState(optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.SwipeToDismissValue,java.lang.Boolean> confirmStateChange);
+    method public androidx.wear.compose.foundation.SwipeToDismissValue getCurrentValue();
+    method public androidx.wear.compose.foundation.SwipeToDismissValue getTargetValue();
+    method public boolean isAnimationRunning();
+    method public suspend Object? snapTo(androidx.wear.compose.foundation.SwipeToDismissValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final androidx.wear.compose.foundation.SwipeToDismissValue currentValue;
+    property public final boolean isAnimationRunning;
+    property public final androidx.wear.compose.foundation.SwipeToDismissValue targetValue;
+  }
+
+  public enum SwipeToDismissKeys {
+    method public static androidx.wear.compose.foundation.SwipeToDismissKeys valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method public static androidx.wear.compose.foundation.SwipeToDismissKeys[] values();
+    enum_constant public static final androidx.wear.compose.foundation.SwipeToDismissKeys Background;
+    enum_constant public static final androidx.wear.compose.foundation.SwipeToDismissKeys Content;
+  }
+
+  public enum SwipeToDismissValue {
+    method public static androidx.wear.compose.foundation.SwipeToDismissValue valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method public static androidx.wear.compose.foundation.SwipeToDismissValue[] values();
+    enum_constant public static final androidx.wear.compose.foundation.SwipeToDismissValue Default;
+    enum_constant public static final androidx.wear.compose.foundation.SwipeToDismissValue Dismissed;
+  }
+
   public final class SwipeToRevealKt {
     method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static void SwipeToReveal(kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit> action, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onFullSwipe, optional androidx.wear.compose.foundation.RevealState state, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit>? additionalAction, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit>? undoAction, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static java.util.Map<androidx.wear.compose.foundation.RevealValue,java.lang.Float> createAnchors(optional float coveredAnchor, optional float revealingAnchor, optional float revealedAnchor);
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 123b07a..2283ff7 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -28,7 +28,11 @@
 
   public final class CompositionLocalsKt {
     method @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.foundation.ReduceMotion> getLocalReduceMotion();
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalSwipeToDismissBackgroundScrimColor();
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalSwipeToDismissContentScrimColor();
     property @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.wear.compose.foundation.ReduceMotion> LocalReduceMotion;
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalSwipeToDismissBackgroundScrimColor;
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalSwipeToDismissContentScrimColor;
   }
 
   public interface CurvedAlignment {
@@ -249,6 +253,46 @@
     property public final int Revealing;
   }
 
+  public final class SwipeToDismissBoxDefaults {
+    method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
+    method public float getEdgeWidth();
+    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> AnimationSpec;
+    property public final float EdgeWidth;
+    field public static final androidx.wear.compose.foundation.SwipeToDismissBoxDefaults INSTANCE;
+  }
+
+  public final class SwipeToDismissBoxKt {
+    method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional androidx.compose.ui.Modifier modifier, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissed, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
+    method public static androidx.compose.ui.Modifier edgeSwipeToDismiss(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.SwipeToDismissBoxState swipeToDismissBoxState, optional float edgeWidth);
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.foundation.SwipeToDismissBoxState rememberSwipeToDismissBoxState(optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.SwipeToDismissValue,java.lang.Boolean> confirmStateChange);
+  }
+
+  @androidx.compose.runtime.Stable public final class SwipeToDismissBoxState {
+    ctor public SwipeToDismissBoxState(optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.SwipeToDismissValue,java.lang.Boolean> confirmStateChange);
+    method public androidx.wear.compose.foundation.SwipeToDismissValue getCurrentValue();
+    method public androidx.wear.compose.foundation.SwipeToDismissValue getTargetValue();
+    method public boolean isAnimationRunning();
+    method public suspend Object? snapTo(androidx.wear.compose.foundation.SwipeToDismissValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public final androidx.wear.compose.foundation.SwipeToDismissValue currentValue;
+    property public final boolean isAnimationRunning;
+    property public final androidx.wear.compose.foundation.SwipeToDismissValue targetValue;
+  }
+
+  public enum SwipeToDismissKeys {
+    method public static androidx.wear.compose.foundation.SwipeToDismissKeys valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method public static androidx.wear.compose.foundation.SwipeToDismissKeys[] values();
+    enum_constant public static final androidx.wear.compose.foundation.SwipeToDismissKeys Background;
+    enum_constant public static final androidx.wear.compose.foundation.SwipeToDismissKeys Content;
+  }
+
+  public enum SwipeToDismissValue {
+    method public static androidx.wear.compose.foundation.SwipeToDismissValue valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method public static androidx.wear.compose.foundation.SwipeToDismissValue[] values();
+    enum_constant public static final androidx.wear.compose.foundation.SwipeToDismissValue Default;
+    enum_constant public static final androidx.wear.compose.foundation.SwipeToDismissValue Dismissed;
+  }
+
   public final class SwipeToRevealKt {
     method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static void SwipeToReveal(kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit> action, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onFullSwipe, optional androidx.wear.compose.foundation.RevealState state, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit>? additionalAction, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.RevealScope,kotlin.Unit>? undoAction, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @SuppressCompatibility @androidx.wear.compose.foundation.ExperimentalWearFoundationApi public static java.util.Map<androidx.wear.compose.foundation.RevealValue,java.lang.Float> createAnchors(optional float coveredAnchor, optional float revealingAnchor, optional float revealedAnchor);
diff --git a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/SwipeToDismissBoxSample.kt b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/SwipeToDismissBoxSample.kt
new file mode 100644
index 0000000..9de2efa
--- /dev/null
+++ b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/SwipeToDismissBoxSample.kt
@@ -0,0 +1,189 @@
+/*
+ * 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.wear.compose.foundation.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.SwipeToDismissBox
+import androidx.wear.compose.foundation.SwipeToDismissValue
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
+import androidx.wear.compose.material.Icon
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.SplitToggleChip
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.ToggleChipDefaults
+
+@Sampled
+@Composable
+fun SimpleSwipeToDismissBox(
+    navigateBack: () -> Unit
+) {
+    val state = rememberSwipeToDismissBoxState()
+    SwipeToDismissBox(
+        state = state,
+        onDismissed = navigateBack
+    ) { isBackground ->
+        if (isBackground) {
+            Box(modifier = Modifier
+                .fillMaxSize()
+                .background(MaterialTheme.colors.secondaryVariant))
+        } else {
+            Column(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(MaterialTheme.colors.primary),
+                horizontalAlignment = Alignment.CenterHorizontally,
+                verticalArrangement = Arrangement.Center,
+            ) {
+                Text("Swipe right to dismiss", color = MaterialTheme.colors.onPrimary)
+            }
+        }
+    }
+}
+
+@Sampled
+@Composable
+fun StatefulSwipeToDismissBox() {
+    // State for managing a 2-level navigation hierarchy between
+    // MainScreen and ItemScreen composables.
+    // Alternatively, use SwipeDismissableNavHost from wear.compose.navigation.
+    var showMainScreen by remember { mutableStateOf(true) }
+    val saveableStateHolder = rememberSaveableStateHolder()
+
+    // Swipe gesture dismisses ItemScreen to return to MainScreen.
+    val state = rememberSwipeToDismissBoxState()
+    LaunchedEffect(state.currentValue) {
+        if (state.currentValue == SwipeToDismissValue.Dismissed) {
+            state.snapTo(SwipeToDismissValue.Default)
+            showMainScreen = !showMainScreen
+        }
+    }
+
+    // Hierarchy is ListScreen -> ItemScreen, so we show ListScreen as the background behind
+    // the ItemScreen, otherwise there's no background to show.
+    SwipeToDismissBox(
+        state = state,
+        userSwipeEnabled = !showMainScreen,
+        backgroundKey = if (!showMainScreen) "MainKey" else "Background",
+        contentKey = if (showMainScreen) "MainKey" else "ItemKey",
+    ) { isBackground ->
+
+        if (isBackground || showMainScreen) {
+            // Best practice would be to use State Hoisting and leave this composable stateless.
+            // Here, we want to support MainScreen being shown from different destinations
+            // (either in the foreground or in the background during swiping) - that can be achieved
+            // using SaveableStateHolder and rememberSaveable as shown below.
+            saveableStateHolder.SaveableStateProvider(
+                key = "MainKey",
+                content = {
+                    // Composable that maintains its own state
+                    // and can be shown in foreground or background.
+                    val checked = rememberSaveable { mutableStateOf(true) }
+                    Column(
+                        modifier = Modifier
+                            .fillMaxSize()
+                            .padding(horizontal = 8.dp, vertical = 8.dp),
+                        verticalArrangement =
+                        Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
+                    ) {
+                        SplitToggleChip(
+                            checked = checked.value,
+                            label = { Text("Item details") },
+                            modifier = Modifier.height(40.dp),
+                            onCheckedChange = { v -> checked.value = v },
+                            onClick = { showMainScreen = false },
+                            toggleControl = {
+                                Icon(
+                                    imageVector = ToggleChipDefaults.checkboxIcon(
+                                        checked = checked.value
+                                    ),
+                                    contentDescription = null,
+                                )
+                            }
+                        )
+                    }
+                }
+            )
+        } else {
+            Column(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(MaterialTheme.colors.primary),
+                horizontalAlignment = Alignment.CenterHorizontally,
+                verticalArrangement = Arrangement.Center,
+            ) {
+                Text("Show details here...", color = MaterialTheme.colors.onPrimary)
+                Text("Swipe right to dismiss", color = MaterialTheme.colors.onPrimary)
+            }
+        }
+    }
+}
+
+@Sampled
+@Composable
+fun EdgeSwipeForSwipeToDismiss(
+    navigateBack: () -> Unit
+) {
+    val state = rememberSwipeToDismissBoxState()
+
+    // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the
+    // modifier applies exists within a SwipeToDismissBox which shares the same state.
+    SwipeToDismissBox(
+        state = state,
+        onDismissed = navigateBack
+    ) { isBackground ->
+        val horizontalScrollState = rememberScrollState(0)
+        if (isBackground) {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(MaterialTheme.colors.secondaryVariant)
+            )
+        } else {
+            Box(modifier = Modifier.fillMaxSize()) {
+                Text(
+                    modifier = Modifier
+                        .align(Alignment.Center)
+                        .edgeSwipeToDismiss(state)
+                        .horizontalScroll(horizontalScrollState),
+                    text = "This text can be scrolled horizontally - to dismiss, swipe " +
+                        "right from the left edge of the screen (called Edge Swiping)",
+                )
+            }
+        }
+    }
+}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt
new file mode 100644
index 0000000..dfcca4b
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt
@@ -0,0 +1,585 @@
+/*
+ * 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.wear.compose.foundation
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.SaveableStateHolder
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.assertTextContains
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipe
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import java.lang.Math.sin
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+class SwipeToDismissBoxTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun supports_testtag() {
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                BasicText("Testing")
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun dismisses_when_swiped_right() =
+        verifySwipe(gesture = { swipeRight() }, expectedToDismiss = true)
+
+    @Test
+    fun does_not_dismiss_when_swiped_left() =
+        // Swipe left is met with resistance and is not a swipe-to-dismiss.
+        verifySwipe(gesture = { swipeLeft() }, expectedToDismiss = false)
+
+    @Test
+    fun does_not_dismiss_when_swipe_right_incomplete() =
+    // Execute a partial swipe over a longer-than-default duration so that there
+        // is insufficient velocity to perform a 'fling'.
+        verifySwipe(
+            gesture = { swipeRight(startX = 0f, endX = width / 4f, durationMillis = LONG_SWIPE) },
+            expectedToDismiss = false
+        )
+
+    @Test
+    fun does_not_display_background_without_swipe() {
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) { isBackground ->
+                if (isBackground) BasicText(BACKGROUND_MESSAGE) else MessageContent()
+            }
+        }
+
+        rule.onNodeWithText(BACKGROUND_MESSAGE).assertDoesNotExist()
+    }
+
+    @Test
+    fun does_not_dismiss_if_userSwipeEnabled_is_false() {
+        var dismissed = false
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            LaunchedEffect(state.currentValue) {
+                dismissed =
+                    state.currentValue == SwipeToDismissValue.Dismissed
+            }
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG),
+                userSwipeEnabled = false
+            ) {
+                BasicText(text = CONTENT_MESSAGE, color = { Color.Red })
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+
+        rule.runOnIdle {
+            assertEquals(false, dismissed)
+        }
+    }
+
+    @Test
+    fun remembers_saved_state() {
+        val showCounterForContent = mutableStateOf(true)
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            val holder = rememberSaveableStateHolder()
+            LaunchedEffect(state.currentValue) {
+                if (state.currentValue == SwipeToDismissValue.Dismissed) {
+                    showCounterForContent.value = !showCounterForContent.value
+                    state.snapTo(SwipeToDismissValue.Default)
+                }
+            }
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG),
+                backgroundKey = if (showCounterForContent.value) TOGGLE_SCREEN else COUNTER_SCREEN,
+                contentKey = if (showCounterForContent.value) COUNTER_SCREEN else TOGGLE_SCREEN,
+                content = { isBackground ->
+                    if (showCounterForContent.value xor isBackground)
+                        counterScreen(holder)
+                    else
+                        toggleScreen(holder)
+                }
+            )
+        }
+
+        // Start with foreground showing Counter screen.
+        rule.onNodeWithTag(COUNTER_SCREEN).assertTextContains("0")
+        rule.onNodeWithTag(COUNTER_SCREEN).performClick()
+        rule.waitForIdle()
+        rule.onNodeWithTag(COUNTER_SCREEN).assertTextContains("1")
+
+        // Swipe to switch to Toggle screen
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TOGGLE_SCREEN).assertIsOff()
+        rule.onNodeWithTag(TOGGLE_SCREEN).performClick()
+        rule.waitForIdle()
+        rule.onNodeWithTag(TOGGLE_SCREEN).assertIsOn()
+
+        // Swipe back to Counter screen
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+        rule.waitForIdle()
+        rule.onNodeWithTag(COUNTER_SCREEN).assertTextContains("1")
+
+        // Swipe back to Toggle screen
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TOGGLE_SCREEN).assertIsOn()
+    }
+
+    @Test
+    fun gives_top_swipe_box_gestures_when_nested() {
+        var outerDismissed = false
+        var innerDismissed = false
+        rule.setContent {
+            val outerState = rememberSwipeToDismissBoxState()
+            LaunchedEffect(outerState.currentValue) {
+                outerDismissed =
+                    outerState.currentValue == SwipeToDismissValue.Dismissed
+            }
+            SwipeToDismissBox(
+                state = outerState,
+                modifier = Modifier.testTag("OUTER"),
+                userSwipeEnabled = true
+            ) {
+                BasicText("Outer", color = { Color.Red })
+                val innerState = rememberSwipeToDismissBoxState()
+                LaunchedEffect(innerState.currentValue) {
+                    innerDismissed =
+                        innerState.currentValue == SwipeToDismissValue.Dismissed
+                }
+                SwipeToDismissBox(
+                    state = innerState,
+                    modifier = Modifier.testTag("INNER"),
+                    userSwipeEnabled = true
+                ) {
+                    BasicText(
+                        text = "Inner",
+                        color = { Color.Red },
+                        modifier = Modifier.testTag(TEST_TAG)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+
+        rule.runOnIdle {
+            assertEquals(true, innerDismissed)
+            assertEquals(false, outerDismissed)
+        }
+    }
+
+    @Composable
+    fun toggleScreen(saveableStateHolder: SaveableStateHolder) {
+        saveableStateHolder.SaveableStateProvider(TOGGLE_SCREEN) {
+            var toggle by rememberSaveable { mutableStateOf(false) }
+            Box(
+                modifier = Modifier
+                    .testTag(TOGGLE_SCREEN)
+                    .toggleable(toggle) { toggle = !toggle }
+            ) {
+                BasicText(text = if (toggle) TOGGLE_ON else TOGGLE_OFF)
+            }
+        }
+    }
+
+    @Composable
+    fun counterScreen(saveableStateHolder: SaveableStateHolder) {
+        saveableStateHolder.SaveableStateProvider(COUNTER_SCREEN) {
+            var counter by rememberSaveable { mutableStateOf(0) }
+            Box(
+                modifier = Modifier
+                    .testTag(COUNTER_SCREEN)
+                    .clickable { ++counter }
+            ) {
+                BasicText(text = "$counter")
+            }
+        }
+    }
+
+    @Test
+    fun displays_background_during_swipe() =
+        verifyPartialSwipe(expectedMessage = BACKGROUND_MESSAGE)
+
+    @Test
+    fun displays_content_during_swipe() =
+        verifyPartialSwipe(expectedMessage = CONTENT_MESSAGE)
+
+    @Test
+    fun calls_ondismissed_after_swipe_when_supplied() {
+        var dismissed = false
+        rule.setContent {
+            SwipeToDismissBox(
+                onDismissed = { dismissed = true },
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                BasicText(CONTENT_MESSAGE, color = { Color.Red })
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+
+        rule.runOnIdle {
+            assertEquals(true, dismissed)
+        }
+    }
+
+    @Test
+    fun edgeswipe_modifier_edge_swiped_right_dismissed() {
+        verifyEdgeSwipeWithNestedScroll(
+            gesture = { swipeRight() },
+            expectedToDismiss = true
+        )
+    }
+
+    @Test
+    fun edgeswipe_non_edge_swiped_right_with_offset_not_dismissed() {
+        verifyEdgeSwipeWithNestedScroll(
+            gesture = { swipeRight(200f, 400f) },
+            expectedToDismiss = false,
+            initialScrollState = 200
+        )
+    }
+
+    @Test
+    fun edgeswipe_non_edge_swiped_right_without_offset_not_dismissed() {
+        verifyEdgeSwipeWithNestedScroll(
+            gesture = { swipeRight(200f, 400f) },
+            expectedToDismiss = false,
+            initialScrollState = 0
+        )
+    }
+
+    @Test
+    fun edgeswipe_edge_swiped_left_not_dismissed() {
+        verifyEdgeSwipeWithNestedScroll(
+            gesture = { swipeLeft(20f, -40f) },
+            expectedToDismiss = false
+        )
+    }
+
+    @Test
+    fun edgeswipe_non_edge_swiped_left_not_dismissed() {
+        verifyEdgeSwipeWithNestedScroll(
+            gesture = { swipeLeft(200f, 0f) },
+            expectedToDismiss = false
+        )
+    }
+
+    @Test
+    fun edgeswipe_swipe_edge_content_was_not_swiped_right() {
+        val initialScrollState = 200
+        lateinit var horizontalScrollState: ScrollState
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            horizontalScrollState = rememberScrollState(initialScrollState)
+
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                NestedScrollContent(state, horizontalScrollState)
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight(0f, 200f) }
+        rule.runOnIdle {
+            assert(horizontalScrollState.value == initialScrollState)
+        }
+    }
+
+    @Test
+    fun edgeswipe_swipe_non_edge_content_was_swiped_right() {
+        val initialScrollState = 200
+        lateinit var horizontalScrollState: ScrollState
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            horizontalScrollState = rememberScrollState(initialScrollState)
+
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                NestedScrollContent(state, horizontalScrollState)
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight(200f, 400f) }
+        rule.runOnIdle {
+            assert(horizontalScrollState.value < initialScrollState)
+        }
+    }
+
+    @Test
+    fun edgeswipe_swipe_edge_content_right_then_left_no_scroll() {
+        testBothDirectionScroll(
+            initialTouch = 10,
+            duration = 2000,
+            amplitude = 100,
+            startLeft = false
+        ) { scrollState ->
+            assertEquals(scrollState.value, 200)
+        }
+    }
+
+    @Test
+    fun edgeswipe_fling_edge_content_right_then_left_no_scroll() {
+        testBothDirectionScroll(
+            initialTouch = 10,
+            duration = 100,
+            amplitude = 100,
+            startLeft = false
+        ) { scrollState ->
+            assertEquals(scrollState.value, 200)
+        }
+    }
+
+    @Test
+    fun edgeswipe_swipe_edge_content_left_then_right_with_scroll() {
+        testBothDirectionScroll(
+            initialTouch = 10,
+            duration = 2000,
+            amplitude = 100,
+            startLeft = true
+        ) { scrollState ->
+            // After scrolling to the left, successful scroll to the right
+            // reduced scrollState
+            assert(scrollState.value < 200)
+        }
+    }
+
+    @Test
+    fun edgeswipe_fling_edge_content_left_then_right_with_scroll() {
+        testBothDirectionScroll(
+            initialTouch = 10,
+            duration = 100,
+            amplitude = 100,
+            startLeft = true
+        ) { scrollState ->
+            // Fling right to the start (0)
+            assertEquals(scrollState.value, 0)
+        }
+    }
+
+    private fun testBothDirectionScroll(
+        initialTouch: Long,
+        duration: Long,
+        amplitude: Long,
+        startLeft: Boolean,
+        testScrollState: (ScrollState) -> Unit
+    ) {
+        val initialScrollState = 200
+        lateinit var horizontalScrollState: ScrollState
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            horizontalScrollState = rememberScrollState(initialScrollState)
+
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                NestedScrollContent(state, horizontalScrollState)
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .performTouchInput {
+                swipeBothDirections(
+                    startLeft = startLeft,
+                    startX = initialTouch,
+                    amplitude = amplitude,
+                    duration = duration
+                )
+            }
+        rule.runOnIdle {
+            testScrollState(horizontalScrollState)
+        }
+    }
+
+    private fun verifySwipe(gesture: TouchInjectionScope.() -> Unit, expectedToDismiss: Boolean) {
+        var dismissed = false
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            LaunchedEffect(state.currentValue) {
+                dismissed =
+                    state.currentValue == SwipeToDismissValue.Dismissed
+            }
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                MessageContent()
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput(gesture)
+
+        rule.runOnIdle {
+            assertEquals(expectedToDismiss, dismissed)
+        }
+    }
+
+    private fun verifyEdgeSwipeWithNestedScroll(
+        gesture: TouchInjectionScope.() -> Unit,
+        expectedToDismiss: Boolean,
+        initialScrollState: Int = 200
+    ) {
+        var dismissed = false
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            val horizontalScrollState = rememberScrollState(initialScrollState)
+
+            LaunchedEffect(state.currentValue) {
+                dismissed =
+                    state.currentValue == SwipeToDismissValue.Dismissed
+            }
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                NestedScrollContent(state, horizontalScrollState)
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput(gesture)
+
+        rule.runOnIdle {
+            assertEquals(expectedToDismiss, dismissed)
+        }
+    }
+
+    private fun verifyPartialSwipe(expectedMessage: String) {
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) { isBackground ->
+                if (isBackground) BasicText(BACKGROUND_MESSAGE) else MessageContent()
+            }
+        }
+
+        // Click down and drag across 1/4 of the screen to start a swipe,
+        // but don't release the finger, so that the screen can be inspected
+        // (note that swipeRight would release the finger and does not pause time midway).
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            down(Offset(x = 0f, y = height / 2f))
+            moveTo(Offset(x = width / 4f, y = height / 2f))
+        }
+
+        rule.onNodeWithText(expectedMessage).assertExists()
+    }
+
+    @Composable
+    private fun MessageContent() {
+        Column(
+            modifier = Modifier.fillMaxSize(),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.Center,
+        ) {
+            BasicText(CONTENT_MESSAGE, color = { Color.Red })
+        }
+    }
+
+    @Composable
+    private fun NestedScrollContent(
+        swipeToDismissState: SwipeToDismissBoxState,
+        horizontalScrollState: ScrollState
+    ) {
+        Box(modifier = Modifier.fillMaxSize()) {
+            BasicText(
+                modifier = Modifier
+                    .align(Alignment.Center)
+                    .edgeSwipeToDismiss(swipeToDismissState)
+                    .horizontalScroll(horizontalScrollState),
+                text = "This text can be scrolled horizontally - to dismiss, swipe " +
+                    "right from the left edge of the screen (called Edge Swiping)",
+            )
+        }
+    }
+
+    private fun TouchInjectionScope.swipeBothDirections(
+        startLeft: Boolean,
+        startX: Long,
+        amplitude: Long,
+        duration: Long = 200
+    ) {
+        val sign = if (startLeft) -1 else 1
+        // By using sin function for range 0.. 3pi/2 , we can achieve 0 -> 1 and 1 -> -1  values
+        swipe(curve = { time ->
+            val x =
+                startX + sign * sin(time.toFloat() / duration.toFloat() * 3 * Math.PI / 2)
+                    .toFloat() * amplitude
+            Offset(
+                x = x,
+                y = centerY
+            )
+        }, durationMillis = duration)
+    }
+}
+
+private const val BACKGROUND_MESSAGE = "The Background"
+private const val CONTENT_MESSAGE = "The Content"
+private const val LONG_SWIPE = 1000L
+private const val TOGGLE_SCREEN = "Toggle"
+private const val COUNTER_SCREEN = "Counter"
+private const val TOGGLE_ON = "On"
+private const val TOGGLE_OFF = "Off"
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CompositionLocals.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CompositionLocals.kt
index f347b14..9b3c2be 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CompositionLocals.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CompositionLocals.kt
@@ -24,7 +24,9 @@
 import android.provider.Settings
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.core.os.HandlerCompat
 import java.util.concurrent.atomic.AtomicReference
@@ -51,6 +53,22 @@
 }
 
 /**
+ * CompositionLocal containing the background scrim color of [SwipeToDismissBox].
+ *
+ * Defaults to [Color.Black] if not explicitly set.
+ */
+public val LocalSwipeToDismissBackgroundScrimColor: ProvidableCompositionLocal<Color> =
+    compositionLocalOf { Color.Black }
+
+/**
+ * CompositionLocal containing the content scrim color of [SwipeToDismissBox].
+ *
+ * Defaults to [Color.Black] if not explicitly set.
+ */
+public val LocalSwipeToDismissContentScrimColor: ProvidableCompositionLocal<Color> =
+    compositionLocalOf { Color.Black }
+
+/**
  * ReduceMotion provides a means for callers to determine whether an app should turn off
  * animations and screen movement.
  */
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeViewModel.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/Resources.kt
similarity index 63%
rename from bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeViewModel.kt
rename to wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/Resources.kt
index 88b1c1c..1ffc795 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/home/HomeViewModel.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/Resources.kt
@@ -14,16 +14,16 @@
  * limitations under the License.
  */
 
-package androidx.bluetooth.integration.testapp.ui.home
+package androidx.wear.compose.foundation
 
-import android.bluetooth.le.ScanResult
-import androidx.lifecycle.ViewModel
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalConfiguration
 
-class HomeViewModel : ViewModel() {
-
-    private companion object {
-        private const val TAG = "HomeViewModel"
+@Composable
+internal fun isRoundDevice(): Boolean {
+    val configuration = LocalConfiguration.current
+    return remember(configuration) {
+        configuration.isScreenRound
     }
-
-    val scanResults = mutableMapOf<String, ScanResult>()
 }
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt
new file mode 100644
index 0000000..b776940
--- /dev/null
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt
@@ -0,0 +1,585 @@
+/*
+ * 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.wear.compose.foundation
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.graphicsLayer
+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.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.changedToUp
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
+
+/**
+ * [SwipeToDismissBox] that handles the swipe-to-dismiss gesture. Takes a single slot for the
+ * background (only displayed during the swipe gesture) and the foreground content.
+ *
+ * Example of a [SwipeToDismissBox] with stateful composables:
+ * @sample androidx.wear.compose.foundation.samples.StatefulSwipeToDismissBox
+ *
+ * Example of using [Modifier.edgeSwipeToDismiss] with [SwipeToDismissBox]
+ * @sample androidx.wear.compose.foundation.samples.EdgeSwipeForSwipeToDismiss
+ *
+ * For more information, see the
+ * [Swipe to dismiss](https://developer.android.com/training/wearables/components/swipe-to-dismiss)
+ * guide.
+ *
+ * To set the custom values of background scrim color and content scrim color, provide the
+ * composition locals - [LocalSwipeToDismissBackgroundScrimColor] and
+ * [LocalSwipeToDismissContentScrimColor].
+ *
+ * @param state [State] containing information about ongoing swipe or animation.
+ * @param modifier [Modifier] for this component.
+ * @param backgroundKey [key] which identifies the content currently composed in
+ * the [content] block when isBackground == true. Provide the backgroundKey if your background
+ * content will be displayed as a foreground after the swipe animation ends
+ * (as is common when [SwipeToDismissBox] is used for the navigation). This allows
+ * remembered state to be correctly moved between background and foreground.
+ * @param contentKey [key] which identifies the content currently composed in the
+ * [content] block when isBackground == false. See [backgroundKey].
+ * @param userSwipeEnabled Whether the swipe gesture is enabled.
+ * (e.g. when there is no background screen, set userSwipeEnabled = false)
+ * @param content Slot for content, with the isBackground parameter enabling content to be
+ * displayed behind the foreground content - the background is normally hidden, is shown behind a
+ * scrim during the swipe gesture, and is shown without scrim once the finger passes the
+ * swipe-to-dismiss threshold.
+ */
+@OptIn(ExperimentalWearFoundationApi::class)
+@Composable
+public fun SwipeToDismissBox(
+    state: SwipeToDismissBoxState,
+    modifier: Modifier = Modifier,
+    backgroundKey: Any = SwipeToDismissKeys.Background,
+    contentKey: Any = SwipeToDismissKeys.Content,
+    userSwipeEnabled: Boolean = true,
+    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
+) {
+    // Will be updated in onSizeChanged, initialise to any value other than zero
+    // so that it is different to the other anchor used for the swipe gesture.
+    var maxWidth by remember { mutableFloatStateOf(1f) }
+    Box(
+        modifier = modifier
+            .fillMaxSize()
+            .onSizeChanged { maxWidth = it.width.toFloat() }
+            .swipeableV2(
+                state = state.swipeableState,
+                orientation = Orientation.Horizontal,
+                enabled = userSwipeEnabled
+            )
+            .swipeAnchors(
+                state = state.swipeableState,
+                possibleValues = anchors().keys
+            ) { value, layoutSize ->
+                val width = layoutSize.width.toFloat()
+                anchors()[value]!! * width
+            }
+    ) {
+        var squeezeMode by remember {
+            mutableStateOf(true)
+        }
+
+        LaunchedEffect(state.isAnimationRunning) {
+            if (state.targetValue == SwipeToDismissValue.Dismissed) {
+                squeezeMode = false
+            }
+        }
+
+        LaunchedEffect(state.targetValue) {
+            if (!squeezeMode && state.targetValue == SwipeToDismissValue.Default) {
+                squeezeMode = true
+            }
+        }
+
+        val isRound = isRoundDevice()
+        val backgroundScrimColor = LocalSwipeToDismissBackgroundScrimColor.current
+        val contentScrimColor = LocalSwipeToDismissContentScrimColor.current
+
+        // Use remember { derivedStateOf{ ... } } idiom to re-use modifiers where possible.
+        // b/280392104: re-calculate modifiers if keys have changed
+        val modifiers by remember(isRound, backgroundScrimColor, backgroundKey, contentKey) {
+            derivedStateOf {
+                val progress = ((state.swipeableState.offset ?: 0f) / maxWidth).coerceIn(0f, 1f)
+                val scale = lerp(SCALE_MAX, SCALE_MIN, progress).coerceIn(SCALE_MIN, SCALE_MAX)
+                val squeezeOffset = max(0f, (1f - scale) * maxWidth / 2f)
+                val slideOffset = lerp(squeezeOffset, maxWidth, max(0f, progress - 0.7f) / 0.3f)
+
+                val translationX = if (squeezeMode) squeezeOffset else slideOffset
+
+                val backgroundAlpha = MAX_BACKGROUND_SCRIM_ALPHA * (1 - progress)
+                val contentScrimAlpha = min(MAX_CONTENT_SCRIM_ALPHA, progress / 2f)
+
+                Modifiers(
+                    contentForeground = Modifier
+                        .fillMaxSize()
+                        .graphicsLayer {
+                            this.translationX = translationX
+                            scaleX = scale
+                            scaleY = scale
+                            clip = isRound && translationX > 0
+                            shape = if (isRound) CircleShape else RectangleShape
+                        }
+                        .background(backgroundScrimColor),
+                    scrimForeground =
+                    Modifier
+                        .background(
+                            contentScrimColor.copy(alpha = contentScrimAlpha)
+                        )
+                        .fillMaxSize(),
+                    scrimBackground =
+                    Modifier
+                        .matchParentSize()
+                        .background(
+                            backgroundScrimColor.copy(alpha = backgroundAlpha)
+                        )
+                )
+            }
+        }
+
+        repeat(2) {
+            val isBackground = it == 0
+            val contentModifier = if (isBackground) {
+                Modifier.fillMaxSize()
+            } else {
+                modifiers.contentForeground
+            }
+
+            val scrimModifier = if (isBackground) {
+                modifiers.scrimBackground
+            } else {
+                modifiers.scrimForeground
+            }
+
+            key(if (isBackground) backgroundKey else contentKey) {
+                if (!isBackground ||
+                    (userSwipeEnabled && (state.swipeableState.offset?.roundToInt() ?: 0) > 0)
+                ) {
+                    Box(contentModifier) {
+                        // We use the repeat loop above and call content at this location
+                        // for both background and foreground so that any persistence
+                        // within the content composable has the same call stack which is used
+                        // as part of the hash identity for saveable state.
+                        content(isBackground)
+                        Box(modifier = scrimModifier)
+                    }
+                }
+            }
+        }
+    }
+}
+
+/**
+ * [SwipeToDismissBox] that handles the swipe-to-dismiss gesture.
+ * This overload takes an [onDismissed] parameter which is used to execute a command when the
+ * swipe to dismiss has completed, such as navigating to another screen.
+ *
+ * Example of a simple SwipeToDismissBox:
+ * @sample androidx.wear.compose.foundation.samples.SimpleSwipeToDismissBox
+ *
+ * Example of using [Modifier.edgeSwipeToDismiss] with [SwipeToDismissBox]
+ * @sample androidx.wear.compose.foundation.samples.EdgeSwipeForSwipeToDismiss
+ *
+ * For more information, see the
+ * [Swipe to dismiss](https://developer.android.com/training/wearables/components/swipe-to-dismiss)
+ * guide.
+ *
+ * To set the custom values of background scrim color and content scrim color, provide the
+ * composition locals - [LocalSwipeToDismissBackgroundScrimColor] and
+ * [LocalSwipeToDismissContentScrimColor].
+ *
+ * @param onDismissed Executes when the swipe to dismiss has completed.
+ * @param modifier [Modifier] for this component.
+ * @param state [State] containing information about ongoing swipe or animation.
+ * @param backgroundKey [key] which identifies the content currently composed in
+ * the [content] block when isBackground == true. Provide the backgroundKey if your background
+ * content will be displayed as a foreground after the swipe animation ends
+ * (as is common when [SwipeToDismissBox] is used for the navigation). This allows
+ * remembered state to be correctly moved between background and foreground.
+ * @param contentKey [key] which identifies the content currently composed in the
+ * [content] block when isBackground == false. See [backgroundKey].
+ * @param userSwipeEnabled Whether the swipe gesture is enabled.
+ * (e.g. when there is no background screen, set userSwipeEnabled = false)
+ * @param content Slot for content, with the isBackground parameter enabling content to be
+ * displayed behind the foreground content - the background is normally hidden, is shown behind a
+ * scrim during the swipe gesture, and is shown without scrim once the finger passes the
+ * swipe-to-dismiss threshold.
+ */
+@Composable
+public fun SwipeToDismissBox(
+    onDismissed: () -> Unit,
+    modifier: Modifier = Modifier,
+    state: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(),
+    backgroundKey: Any = SwipeToDismissKeys.Background,
+    contentKey: Any = SwipeToDismissKeys.Content,
+    userSwipeEnabled: Boolean = true,
+    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
+) {
+    LaunchedEffect(state.currentValue) {
+        if (state.currentValue == SwipeToDismissValue.Dismissed) {
+            state.snapTo(SwipeToDismissValue.Default)
+            onDismissed()
+        }
+    }
+    SwipeToDismissBox(
+        state = state,
+        modifier = modifier,
+        backgroundKey = backgroundKey,
+        contentKey = contentKey,
+        userSwipeEnabled = userSwipeEnabled,
+        content = content
+    )
+}
+
+/**
+ * State for [SwipeToDismissBox].
+ *
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmStateChange callback invoked to confirm or veto a pending state change.
+ */
+@Stable
+@OptIn(ExperimentalWearFoundationApi::class)
+public class SwipeToDismissBoxState(
+    animationSpec: AnimationSpec<Float> = SwipeToDismissBoxDefaults.AnimationSpec,
+    confirmStateChange: (SwipeToDismissValue) -> Boolean = { true },
+) {
+    /**
+     * The current value of the state.
+     *
+     * Before and during a swipe, corresponds to [SwipeToDismissValue.Default], then switches to
+     * [SwipeToDismissValue.Dismissed] if the swipe has been completed.
+     */
+    public val currentValue: SwipeToDismissValue
+        get() = swipeableState.currentValue
+
+    /**
+     * The target value of the state.
+     *
+     * If a swipe is in progress, this is the value that the state would animate to if the
+     * swipe finished. If an animation is running, this is the target value of that animation.
+     * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
+     */
+    public val targetValue: SwipeToDismissValue
+        get() = swipeableState.targetValue
+
+    /**
+     * Whether the state is currently animating.
+     */
+    public val isAnimationRunning: Boolean
+        get() = swipeableState.isAnimationRunning
+
+    internal fun edgeNestedScrollConnection(
+        edgeSwipeState: State<EdgeSwipeState>
+    ): NestedScrollConnection =
+        swipeableState.edgeNestedScrollConnection(edgeSwipeState)
+
+    /**
+     * Set the state without any animation and suspend until it's set
+     *
+     * @param targetValue The new target value to set [currentValue] to.
+     */
+    public suspend fun snapTo(targetValue: SwipeToDismissValue) = swipeableState.snapTo(targetValue)
+
+    private companion object {
+        private fun <T> SwipeableV2State<T>.edgeNestedScrollConnection(
+            edgeSwipeState: State<EdgeSwipeState>
+        ): NestedScrollConnection =
+            object : NestedScrollConnection {
+                override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                    val delta = available.x
+                    // If swipeState = SwipeState.SWIPING_TO_DISMISS - perform swipeToDismiss
+                    // drag and consume everything
+                    return if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss &&
+                        source == NestedScrollSource.Drag
+                    ) {
+                        dispatchRawDelta(delta)
+                        available
+                    } else {
+                        Offset.Zero
+                    }
+                }
+
+                override fun onPostScroll(
+                    consumed: Offset,
+                    available: Offset,
+                    source: NestedScrollSource
+                ): Offset = Offset.Zero
+
+                override suspend fun onPreFling(available: Velocity): Velocity {
+                    val toFling = available.x
+                    // Consumes fling by SwipeToDismiss
+                    return if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss ||
+                        edgeSwipeState.value == EdgeSwipeState.SwipeToDismissInProgress
+                    ) {
+                        settle(velocity = toFling)
+                        available
+                    } else
+                        Velocity.Zero
+                }
+
+                override suspend fun onPostFling(
+                    consumed: Velocity,
+                    available: Velocity
+                ): Velocity {
+                    settle(velocity = available.x)
+                    return available
+                }
+            }
+    }
+
+    internal val swipeableState = SwipeableV2State(
+        initialValue = SwipeToDismissValue.Default,
+        animationSpec = animationSpec,
+        confirmValueChange = confirmStateChange,
+        positionalThreshold = fractionalPositionalThreshold(SWIPE_THRESHOLD)
+    )
+}
+
+/**
+ * Create a [SwipeToDismissBoxState] and remember it.
+ *
+ * @param animationSpec The default animation used to animate to a new state.
+ * @param confirmStateChange callback to confirm or veto a pending state change.
+ */
+@Composable
+public fun rememberSwipeToDismissBoxState(
+    animationSpec: AnimationSpec<Float> = SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC,
+    confirmStateChange: (SwipeToDismissValue) -> Boolean = { true },
+): SwipeToDismissBoxState {
+    return remember(animationSpec, confirmStateChange) {
+        SwipeToDismissBoxState(animationSpec, confirmStateChange)
+    }
+}
+
+/**
+ * Contains defaults for [SwipeToDismissBox].
+ */
+public object SwipeToDismissBoxDefaults {
+    /**
+     * The default animation that will be used to animate to a new state after the swipe gesture.
+     */
+    @OptIn(ExperimentalWearFoundationApi::class)
+    public val AnimationSpec = SwipeableV2Defaults.AnimationSpec
+
+    /**
+     * The default width of the area which might trigger a swipe
+     * with [edgeSwipeToDismiss] modifier
+     */
+    public val EdgeWidth = 30.dp
+}
+
+/**
+ * Keys used to persistent state in [SwipeToDismissBox].
+ */
+public enum class SwipeToDismissKeys {
+    /**
+     * The default background key to identify the content displayed by the content block
+     * when isBackground == true. Specifying a background key instead of using the default
+     * allows remembered state to be correctly moved between background and foreground.
+     */
+    Background,
+
+    /**
+     * The default content key to identify the content displayed by the content block
+     * when isBackground == false. Specifying a background key instead of using the default
+     * allows remembered state to be correctly moved between background and foreground.
+     */
+    Content
+}
+
+/**
+ * States used as targets for the anchor points for swipe-to-dismiss.
+ */
+public enum class SwipeToDismissValue {
+    /**
+     * The state of the SwipeToDismissBox before the swipe started.
+     */
+    Default,
+
+    /**
+     * The state of the SwipeToDismissBox after the swipe passes the swipe-to-dismiss threshold.
+     */
+    Dismissed
+}
+
+/**
+ * Limits swipe to dismiss to be active from the edge of the viewport only. Used when the center
+ * of the screen needs to be able to handle horizontal paging, such as 2-d scrolling a Map
+ * or swiping horizontally between pages. Swipe to the right is intercepted on the left
+ * part of the viewport with width specified by [edgeWidth], with other touch events
+ * ignored - vertical scroll, click, long click, etc.
+ *
+ * Currently Edge swipe, like swipe to dismiss, is only supported on the left part of the viewport
+ * regardless of layout direction as content is swiped away from left to right.
+ *
+ * Requires that the element to which this modifier is applied exists within a
+ * [SwipeToDismissBox] which is using the same [SwipeToDismissBoxState] instance.
+ *
+ * Example of a modifier usage with SwipeToDismiss
+ * @sample androidx.wear.compose.foundation.samples.EdgeSwipeForSwipeToDismiss
+ *
+ * @param swipeToDismissBoxState A state of [SwipeToDismissBox]. Used to trigger swipe gestures
+ * on SwipeToDismissBox
+ * @param edgeWidth A width of edge, where swipe should be recognised
+ */
+public fun Modifier.edgeSwipeToDismiss(
+    swipeToDismissBoxState: SwipeToDismissBoxState,
+    edgeWidth: Dp = SwipeToDismissBoxDefaults.EdgeWidth
+): Modifier =
+    composed(
+        inspectorInfo = debugInspectorInfo {
+            name = "edgeSwipeToDismiss"
+            properties["swipeToDismissBoxState"] = swipeToDismissBoxState
+            properties["edgeWidth"] = edgeWidth
+        }
+    ) {
+        // Tracks the current swipe status
+        val edgeSwipeState = remember { mutableStateOf(EdgeSwipeState.WaitingForTouch) }
+        val nestedScrollConnection =
+            remember(swipeToDismissBoxState) {
+                swipeToDismissBoxState.edgeNestedScrollConnection(edgeSwipeState)
+            }
+
+        val nestedPointerInput: suspend PointerInputScope.() -> Unit = {
+            coroutineScope {
+                awaitPointerEventScope {
+                    while (isActive) {
+                        awaitPointerEvent(PointerEventPass.Initial).changes.forEach { change ->
+                            // By default swipeState is WaitingForTouch.
+                            // If it is in this state and a first touch hit an edge area, we
+                            // set swipeState to EdgeClickedWaitingForDirection.
+                            // After that to track which direction the swipe will go, we check
+                            // the next touch. If it lands to the left of the first, we consider
+                            // it as a swipe left and set the state to SwipingToPage. Otherwise,
+                            // set the state to SwipingToDismiss
+                            when (edgeSwipeState.value) {
+                                EdgeSwipeState.SwipeToDismissInProgress,
+                                EdgeSwipeState.WaitingForTouch -> {
+                                    edgeSwipeState.value =
+                                        if (change.position.x < edgeWidth.toPx())
+                                            EdgeSwipeState.EdgeClickedWaitingForDirection
+                                        else
+                                            EdgeSwipeState.SwipingToPage
+                                }
+
+                                EdgeSwipeState.EdgeClickedWaitingForDirection -> {
+                                    edgeSwipeState.value =
+                                        if (change.position.x < change.previousPosition.x)
+                                            EdgeSwipeState.SwipingToPage
+                                        else
+                                            EdgeSwipeState.SwipingToDismiss
+                                }
+
+                                else -> {} // Do nothing
+                            }
+                            // When finger is up - reset swipeState to WaitingForTouch
+                            // or to SwipeToDismissInProgress if current
+                            // state is SwipingToDismiss
+                            if (change.changedToUp()) {
+                                edgeSwipeState.value =
+                                    if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss)
+                                        EdgeSwipeState.SwipeToDismissInProgress
+                                    else
+                                        EdgeSwipeState.WaitingForTouch
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        pointerInput(edgeWidth, nestedPointerInput)
+            .nestedScroll(nestedScrollConnection)
+    }
+
+/**
+ * An enum which represents a current state of swipe action.
+ */
+internal enum class EdgeSwipeState {
+    // Waiting for touch, edge was not touched before.
+    WaitingForTouch,
+
+    // Edge was touched, now waiting for the second touch
+    // to determine whether we swipe left or right.
+    EdgeClickedWaitingForDirection,
+
+    // Direction was determined, swiping to dismiss.
+    SwipingToDismiss,
+
+    // Direction was determined, all gestures are handled by the page itself.
+    SwipingToPage,
+
+    // Swipe was finished, used to handle fling.
+    SwipeToDismissInProgress
+}
+
+/**
+ * Class to enable calculating group of modifiers in a single, memoised block.
+ */
+private data class Modifiers(
+    val contentForeground: Modifier,
+    val scrimForeground: Modifier,
+    val scrimBackground: Modifier,
+)
+
+// Map states to pixel position - initially, don't know the width in pixels so omit upper bound.
+private fun anchors(): Map<SwipeToDismissValue, Float> =
+    mapOf(
+        SwipeToDismissValue.Default to 0f,
+        SwipeToDismissValue.Dismissed to 1f
+    )
+
+private const val SWIPE_THRESHOLD = 0.5f
+private const val SCALE_MAX = 1f
+private const val SCALE_MIN = 0.7f
+private const val MAX_CONTENT_SCRIM_ALPHA = 0.3f
+private const val MAX_BACKGROUND_SCRIM_ALPHA = 0.5f
+private val SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC =
+    TweenSpec<Float>(200, 0, LinearOutSlowInEasing)
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 78df16c..c1b45d8 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -95,10 +95,12 @@
     method public float getChipHorizontalPadding();
     method public float getChipVerticalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getCompactChipContentPadding();
+    method public float getCompactChipHeight();
     method public float getCompactChipHorizontalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getCompactChipTapTargetPadding();
     method public float getCompactChipVerticalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getHeight();
     method public float getIconSize();
     method public float getLargeIconSize();
     method public float getSmallIconSize();
@@ -111,10 +113,12 @@
     property public final float ChipHorizontalPadding;
     property public final float ChipVerticalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues CompactChipContentPadding;
+    property public final float CompactChipHeight;
     property public final float CompactChipHorizontalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues CompactChipTapTargetPadding;
     property public final float CompactChipVerticalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float Height;
     property public final float IconSize;
     property public final float LargeIconSize;
     property public final float SmallIconSize;
@@ -815,6 +819,7 @@
     method public androidx.compose.ui.graphics.vector.ImageVector checkboxIcon(boolean checked);
     method public androidx.compose.ui.graphics.vector.ImageVector getCheckboxOn();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getHeight();
     method public float getIconSize();
     method public androidx.compose.ui.graphics.vector.ImageVector getRadioOff();
     method public androidx.compose.ui.graphics.vector.ImageVector getRadioOn();
@@ -825,6 +830,7 @@
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material.ToggleChipColors toggleChipColors(optional long checkedStartBackgroundColor, optional long checkedEndBackgroundColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedToggleControlColor, optional long uncheckedStartBackgroundColor, optional long uncheckedEndBackgroundColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedToggleControlColor, optional androidx.compose.ui.unit.LayoutDirection gradientDirection);
     property public final androidx.compose.ui.graphics.vector.ImageVector CheckboxOn;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float Height;
     property public final float IconSize;
     property public final androidx.compose.ui.graphics.vector.ImageVector RadioOff;
     property public final androidx.compose.ui.graphics.vector.ImageVector RadioOn;
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 78df16c..c1b45d8 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -95,10 +95,12 @@
     method public float getChipHorizontalPadding();
     method public float getChipVerticalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getCompactChipContentPadding();
+    method public float getCompactChipHeight();
     method public float getCompactChipHorizontalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getCompactChipTapTargetPadding();
     method public float getCompactChipVerticalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getHeight();
     method public float getIconSize();
     method public float getLargeIconSize();
     method public float getSmallIconSize();
@@ -111,10 +113,12 @@
     property public final float ChipHorizontalPadding;
     property public final float ChipVerticalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues CompactChipContentPadding;
+    property public final float CompactChipHeight;
     property public final float CompactChipHorizontalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues CompactChipTapTargetPadding;
     property public final float CompactChipVerticalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float Height;
     property public final float IconSize;
     property public final float LargeIconSize;
     property public final float SmallIconSize;
@@ -815,6 +819,7 @@
     method public androidx.compose.ui.graphics.vector.ImageVector checkboxIcon(boolean checked);
     method public androidx.compose.ui.graphics.vector.ImageVector getCheckboxOn();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getHeight();
     method public float getIconSize();
     method public androidx.compose.ui.graphics.vector.ImageVector getRadioOff();
     method public androidx.compose.ui.graphics.vector.ImageVector getRadioOn();
@@ -825,6 +830,7 @@
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material.ToggleChipColors toggleChipColors(optional long checkedStartBackgroundColor, optional long checkedEndBackgroundColor, optional long checkedContentColor, optional long checkedSecondaryContentColor, optional long checkedToggleControlColor, optional long uncheckedStartBackgroundColor, optional long uncheckedEndBackgroundColor, optional long uncheckedContentColor, optional long uncheckedSecondaryContentColor, optional long uncheckedToggleControlColor, optional androidx.compose.ui.unit.LayoutDirection gradientDirection);
     property public final androidx.compose.ui.graphics.vector.ImageVector CheckboxOn;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float Height;
     property public final float IconSize;
     property public final androidx.compose.ui.graphics.vector.ImageVector RadioOff;
     property public final androidx.compose.ui.graphics.vector.ImageVector RadioOn;
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/ListHeaderTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/ListHeaderTest.kt
new file mode 100644
index 0000000..92524ec
--- /dev/null
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/ListHeaderTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.wear.compose.material
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.isHeading
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ListHeaderTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun supports_testtag() {
+        rule.setContentWithTheme {
+            ListHeader(
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                Text(text = "List Header")
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun listHeader_has_semantic_heading_property() {
+        rule.setContentWithTheme {
+            ListHeader(
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                Text("Title")
+            }
+        }
+
+        rule.onNode(isHeading())
+    }
+
+    @Test
+    fun gives_listHeader_correct_text_style() {
+        var actualTextStyle = TextStyle.Default
+        var expectedTextStyle = TextStyle.Default
+
+        rule.setContentWithTheme {
+            expectedTextStyle = MaterialTheme.typography.button
+            ListHeader {
+                actualTextStyle = LocalTextStyle.current
+            }
+        }
+        Assert.assertEquals(expectedTextStyle, actualTextStyle)
+    }
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
index 34bcfa9..e200910 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
@@ -1121,7 +1121,7 @@
      * The default height applied for the [Chip].
      * Note that you can override it by applying Modifier.heightIn directly on [Chip].
      */
-    internal val Height = 52.dp
+    public val Height = 52.dp
 
     /**
      * The height applied for the [CompactChip]. This includes a visible chip height of 32.dp and
@@ -1131,7 +1131,7 @@
      * Note that you can override it by adjusting Modifier.height and Modifier.padding directly on
      * [CompactChip].
      */
-    internal val CompactChipHeight = 48.dp
+    public val CompactChipHeight = 48.dp
 
     /**
      * The default padding to be provided around a [CompactChip] in order to ensure that its
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
index de98341..8e6d114 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
@@ -26,6 +26,8 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.heading
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 
 /**
@@ -52,6 +54,7 @@
             .wrapContentSize()
             .background(backgroundColor)
             .padding(horizontal = 14.dp)
+            .semantics { heading() }
     ) {
         CompositionLocalProvider(
             LocalContentColor provides contentColor,
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
index 9ebc6bc..86d8124 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
@@ -25,6 +25,8 @@
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.remember
+import androidx.wear.compose.foundation.LocalSwipeToDismissBackgroundScrimColor
+import androidx.wear.compose.foundation.LocalSwipeToDismissContentScrimColor
 
 // TODO: Provide references to the Wear material design specs.
 /**
@@ -79,7 +81,8 @@
         LocalIndication provides rippleIndication,
         LocalRippleTheme provides MaterialRippleTheme,
         LocalTextSelectionColors provides selectionColors,
-
+        LocalSwipeToDismissBackgroundScrimColor provides rememberedColors.background,
+        LocalSwipeToDismissContentScrimColor provides rememberedColors.background
     ) {
         ProvideTextStyle(value = typography.body1, content = content)
     }
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToDismissBox.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToDismissBox.kt
index 9353e5e..ddbc6dc 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToDismissBox.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/SwipeToDismissBox.kt
@@ -16,50 +16,23 @@
 
 package androidx.wear.compose.material
 
+import androidx.annotation.RestrictTo
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.LinearOutSlowInEasing
 import androidx.compose.animation.core.TweenSpec
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.key
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.graphicsLayer
-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.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.changedToUp
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.lerp
-import kotlin.math.max
-import kotlin.math.min
-import kotlin.math.roundToInt
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.isActive
+import androidx.wear.compose.foundation.LocalSwipeToDismissBackgroundScrimColor
+import androidx.wear.compose.foundation.LocalSwipeToDismissContentScrimColor
+import androidx.wear.compose.foundation.edgeSwipeToDismiss as foundationEdgeSwipeToDismiss
 
 /**
  * Wear Material [SwipeToDismissBox] that handles the swipe-to-dismiss gesture. Takes a single
@@ -95,7 +68,6 @@
  * and is shown without scrim once the finger passes the swipe-to-dismiss threshold.
  */
 @Composable
-@OptIn(ExperimentalWearMaterialApi::class)
 public fun SwipeToDismissBox(
     state: SwipeToDismissBoxState,
     modifier: Modifier = Modifier,
@@ -106,114 +78,18 @@
     hasBackground: Boolean = true,
     content: @Composable BoxScope.(isBackground: Boolean) -> Unit
 ) {
-    // Will be updated in onSizeChanged, initialise to any value other than zero
-    // so that it is different to the other anchor used for the swipe gesture.
-    var maxWidth by remember { mutableFloatStateOf(1f) }
-    Box(
-        modifier = modifier
-            .fillMaxSize()
-            .onSizeChanged { maxWidth = it.width.toFloat() }
-            .swipeable(
-                state = state.swipeableState,
-                enabled = hasBackground,
-                anchors = anchors(maxWidth),
-                thresholds = { _, _ -> FractionalThreshold(SWIPE_THRESHOLD) },
-                resistance = ResistanceConfig(
-                    basis = maxWidth,
-                    factorAtMin = TOTAL_RESISTANCE,
-                    factorAtMax = TOTAL_RESISTANCE,
-                ),
-                orientation = Orientation.Horizontal,
-            )
+    CompositionLocalProvider(
+        LocalSwipeToDismissBackgroundScrimColor provides backgroundScrimColor,
+        LocalSwipeToDismissContentScrimColor provides contentScrimColor
     ) {
-        var squeezeMode by remember {
-            mutableStateOf(true)
-        }
-
-        LaunchedEffect(state.isAnimationRunning) {
-            if (state.targetValue == SwipeToDismissValue.Dismissed) {
-                squeezeMode = false
-            }
-        }
-
-        LaunchedEffect(state.targetValue) {
-            if (!squeezeMode && state.targetValue == SwipeToDismissValue.Default) {
-                squeezeMode = true
-            }
-        }
-
-        val isRound = isRoundDevice()
-
-        // Use remember { derivedStateOf{ ... } } idiom to re-use modifiers where possible.
-        // b/280392104: re-calculate modifiers if keys have changed
-        val modifiers by remember(isRound, backgroundScrimColor, backgroundKey, contentKey) {
-            derivedStateOf {
-                val progress = (state.swipeableState.offset.value / maxWidth).coerceIn(0f, 1f)
-                val scale = lerp(SCALE_MAX, SCALE_MIN, progress).coerceIn(SCALE_MIN, SCALE_MAX)
-                val squeezeOffset = max(0f, (1f - scale) * maxWidth / 2f)
-                val slideOffset = lerp(squeezeOffset, maxWidth, max(0f, progress - 0.7f) / 0.3f)
-
-                val translationX = if (squeezeMode) squeezeOffset else slideOffset
-
-                val backgroundAlpha = MAX_BACKGROUND_SCRIM_ALPHA * (1 - progress)
-                val contentScrimAlpha = min(MAX_CONTENT_SCRIM_ALPHA, progress / 2f)
-
-                Modifiers(
-                    contentForeground = Modifier
-                        .fillMaxSize()
-                        .graphicsLayer {
-                            this.translationX = translationX
-                            scaleX = scale
-                            scaleY = scale
-                            clip = isRound && translationX > 0
-                            shape = if (isRound) CircleShape else RectangleShape
-                        }
-                        .background(backgroundScrimColor),
-                    scrimForeground =
-                    Modifier
-                        .background(
-                            contentScrimColor.copy(alpha = contentScrimAlpha)
-                        )
-                        .fillMaxSize(),
-                    scrimBackground =
-                    Modifier
-                        .matchParentSize()
-                        .background(
-                            backgroundScrimColor.copy(alpha = backgroundAlpha)
-                        )
-                )
-            }
-        }
-
-        repeat(2) {
-            val isBackground = it == 0
-            val contentModifier = if (isBackground) {
-                Modifier.fillMaxSize()
-            } else {
-                modifiers.contentForeground
-            }
-
-            val scrimModifier = if (isBackground) {
-                modifiers.scrimBackground
-            } else {
-                modifiers.scrimForeground
-            }
-
-            key(if (isBackground) backgroundKey else contentKey) {
-                if (!isBackground ||
-                    (hasBackground && state.swipeableState.offset.value.roundToInt() > 0)
-                ) {
-                    Box(contentModifier) {
-                        // We use the repeat loop above and call content at this location
-                        // for both background and foreground so that any persistence
-                        // within the content composable has the same call stack which is used
-                        // as part of the hash identity for saveable state.
-                        content(isBackground)
-                        Box(modifier = scrimModifier)
-                    }
-                }
-            }
-        }
+        androidx.wear.compose.foundation.SwipeToDismissBox(
+            state = state.foundationState,
+            modifier = modifier,
+            backgroundKey = backgroundKey,
+            contentKey = contentKey,
+            userSwipeEnabled = hasBackground,
+            content = content
+        )
     }
 }
 
@@ -264,32 +140,29 @@
     hasBackground: Boolean = true,
     content: @Composable BoxScope.(isBackground: Boolean) -> Unit
 ) {
-    LaunchedEffect(state.currentValue) {
-        if (state.currentValue == SwipeToDismissValue.Dismissed) {
-            state.snapTo(SwipeToDismissValue.Default)
-            onDismissed()
-        }
+    CompositionLocalProvider(
+        LocalSwipeToDismissBackgroundScrimColor provides backgroundScrimColor,
+        LocalSwipeToDismissContentScrimColor provides contentScrimColor
+    ) {
+        androidx.wear.compose.foundation.SwipeToDismissBox(
+            state = state.foundationState,
+            modifier = modifier,
+            onDismissed = onDismissed,
+            backgroundKey = backgroundKey,
+            contentKey = contentKey,
+            userSwipeEnabled = hasBackground,
+            content = content
+        )
     }
-    SwipeToDismissBox(
-        state = state,
-        modifier = modifier,
-        backgroundScrimColor = backgroundScrimColor,
-        contentScrimColor = contentScrimColor,
-        backgroundKey = backgroundKey,
-        contentKey = contentKey,
-        hasBackground = hasBackground,
-        content = content
-    )
 }
 
-@Stable
 /**
  * State for [SwipeToDismissBox].
  *
  * @param animationSpec The default animation that will be used to animate to a new state.
  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
  */
-@OptIn(ExperimentalWearMaterialApi::class)
+@Stable
 public class SwipeToDismissBoxState(
     animationSpec: AnimationSpec<Float> = SwipeToDismissBoxDefaults.AnimationSpec,
     confirmStateChange: (SwipeToDismissValue) -> Boolean = { true },
@@ -301,7 +174,7 @@
      * [SwipeToDismissValue.Dismissed] if the swipe has been completed.
      */
     public val currentValue: SwipeToDismissValue
-        get() = swipeableState.currentValue
+        get() = convertFromFoundationSwipeToDismissValue(foundationState.currentValue)
 
     /**
      * The target value of the state.
@@ -311,78 +184,34 @@
      * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
      */
     public val targetValue: SwipeToDismissValue
-        get() = swipeableState.targetValue
+        get() = convertFromFoundationSwipeToDismissValue(foundationState.targetValue)
 
     /**
      * Whether the state is currently animating.
      */
     public val isAnimationRunning: Boolean
-        get() = swipeableState.isAnimationRunning
-
-    internal fun edgeNestedScrollConnection(
-        edgeSwipeState: State<EdgeSwipeState>
-    ): NestedScrollConnection =
-        swipeableState.edgeNestedScrollConnection(edgeSwipeState)
+        get() = foundationState.isAnimationRunning
 
     /**
      * Set the state without any animation and suspend until it's set
      *
      * @param targetValue The new target value to set [currentValue] to.
      */
-    public suspend fun snapTo(targetValue: SwipeToDismissValue) = swipeableState.snapTo(targetValue)
+    public suspend fun snapTo(targetValue: SwipeToDismissValue) =
+        foundationState.snapTo(convertToFoundationSwipeToDismissValue(targetValue))
 
-    private companion object {
-        private fun <T> SwipeableState<T>.edgeNestedScrollConnection(
-            edgeSwipeState: State<EdgeSwipeState>
-        ): NestedScrollConnection =
-            object : NestedScrollConnection {
-                override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
-                    val delta = available.x
-                    // If swipeState = SwipeState.SWIPING_TO_DISMISS - perform swipeToDismiss
-                    // drag and consume everything
-                    return if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss &&
-                        source == NestedScrollSource.Drag
-                    ) {
-                        performDrag(delta)
-                        available
-                    } else {
-                        Offset.Zero
-                    }
-                }
-
-                override fun onPostScroll(
-                    consumed: Offset,
-                    available: Offset,
-                    source: NestedScrollSource
-                ): Offset = Offset.Zero
-
-                override suspend fun onPreFling(available: Velocity): Velocity {
-                    val toFling = available.x
-                    // Consumes fling by SwipeToDismiss
-                    return if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss ||
-                        edgeSwipeState.value == EdgeSwipeState.SwipeToDismissInProgress
-                    ) {
-                        performFling(velocity = toFling)
-                        available
-                    } else
-                        Velocity.Zero
-                }
-
-                override suspend fun onPostFling(
-                    consumed: Velocity,
-                    available: Velocity
-                ): Velocity {
-                    performFling(velocity = available.x)
-                    return available
-                }
-            }
-    }
-
-    internal val swipeableState = SwipeableState(
-        initialValue = SwipeToDismissValue.Default,
+    /**
+     * Foundation version of the [SwipeToDismissBoxState].
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    val foundationState = androidx.wear.compose.foundation.SwipeToDismissBoxState(
         animationSpec = animationSpec,
-        confirmStateChange = confirmStateChange,
+        confirmStateChange = { value: androidx.wear.compose.foundation.SwipeToDismissValue ->
+            confirmStateChange(convertFromFoundationSwipeToDismissValue(value))
+        }
     )
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        get() = field
 }
 
 /**
@@ -476,111 +305,30 @@
     swipeToDismissBoxState: SwipeToDismissBoxState,
     edgeWidth: Dp = SwipeToDismissBoxDefaults.EdgeWidth
 ): Modifier =
-    composed(
-        inspectorInfo = debugInspectorInfo {
-            name = "edgeSwipeToDismiss"
-            properties["swipeToDismissBoxState"] = swipeToDismissBoxState
-            properties["edgeWidth"] = edgeWidth
-        }
-    ) {
-        // Tracks the current swipe status
-        val edgeSwipeState = remember { mutableStateOf(EdgeSwipeState.WaitingForTouch) }
-        val nestedScrollConnection =
-            remember(swipeToDismissBoxState) {
-                swipeToDismissBoxState.edgeNestedScrollConnection(edgeSwipeState)
-            }
-
-        val nestedPointerInput: suspend PointerInputScope.() -> Unit = {
-            coroutineScope {
-                awaitPointerEventScope {
-                    while (isActive) {
-                        awaitPointerEvent(PointerEventPass.Initial).changes.forEach { change ->
-                            // By default swipeState is WaitingForTouch.
-                            // If it is in this state and a first touch hit an edge area, we
-                            // set swipeState to EdgeClickedWaitingForDirection.
-                            // After that to track which direction the swipe will go, we check
-                            // the next touch. If it lands to the left of the first, we consider
-                            // it as a swipe left and set the state to SwipingToPage. Otherwise,
-                            // set the state to SwipingToDismiss
-                            when (edgeSwipeState.value) {
-                                EdgeSwipeState.SwipeToDismissInProgress,
-                                EdgeSwipeState.WaitingForTouch -> {
-                                    edgeSwipeState.value =
-                                        if (change.position.x < edgeWidth.toPx())
-                                            EdgeSwipeState.EdgeClickedWaitingForDirection
-                                        else
-                                            EdgeSwipeState.SwipingToPage
-                                }
-                                EdgeSwipeState.EdgeClickedWaitingForDirection -> {
-                                    edgeSwipeState.value =
-                                        if (change.position.x < change.previousPosition.x)
-                                            EdgeSwipeState.SwipingToPage
-                                        else
-                                            EdgeSwipeState.SwipingToDismiss
-                                }
-                                else -> {} // Do nothing
-                            }
-                            // When finger is up - reset swipeState to WaitingForTouch
-                            // or to SwipeToDismissInProgress if current
-                            // state is SwipingToDismiss
-                            if (change.changedToUp()) {
-                                edgeSwipeState.value =
-                                    if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss)
-                                        EdgeSwipeState.SwipeToDismissInProgress
-                                    else
-                                        EdgeSwipeState.WaitingForTouch
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        pointerInput(edgeWidth, nestedPointerInput)
-            .nestedScroll(nestedScrollConnection)
-    }
-
-/**
- * An enum which represents a current state of swipe action.
- */
-internal enum class EdgeSwipeState {
-    // Waiting for touch, edge was not touched before.
-    WaitingForTouch,
-
-    // Edge was touched, now waiting for the second touch
-    // to determine whether we swipe left or right.
-    EdgeClickedWaitingForDirection,
-
-    // Direction was determined, swiping to dismiss.
-    SwipingToDismiss,
-
-    // Direction was determined, all gestures are handled by the page itself.
-    SwipingToPage,
-
-    // Swipe was finished, used to handle fling.
-    SwipeToDismissInProgress
-}
-
-/**
- * Class to enable calculating group of modifiers in a single, memoised block.
- */
-private data class Modifiers(
-    val contentForeground: Modifier,
-    val scrimForeground: Modifier,
-    val scrimBackground: Modifier,
-)
-
-// Map pixel position to states - initially, don't know the width in pixels so omit upper bound.
-private fun anchors(maxWidth: Float): Map<Float, SwipeToDismissValue> =
-    mapOf(
-        0f to SwipeToDismissValue.Default,
-        maxWidth to SwipeToDismissValue.Dismissed
+    foundationEdgeSwipeToDismiss(
+        swipeToDismissBoxState = swipeToDismissBoxState.foundationState,
+        edgeWidth = edgeWidth
     )
 
-private const val SWIPE_THRESHOLD = 0.5f
-private const val TOTAL_RESISTANCE = 1000f
-private const val SCALE_MAX = 1f
-private const val SCALE_MIN = 0.7f
-private const val MAX_CONTENT_SCRIM_ALPHA = 0.3f
-private const val MAX_BACKGROUND_SCRIM_ALPHA = 0.5f
+private fun convertToFoundationSwipeToDismissValue(
+    value: SwipeToDismissValue
+) = when (value) {
+    SwipeToDismissValue.Default ->
+        androidx.wear.compose.foundation.SwipeToDismissValue.Default
+
+    SwipeToDismissValue.Dismissed ->
+        androidx.wear.compose.foundation.SwipeToDismissValue.Dismissed
+}
+
+private fun convertFromFoundationSwipeToDismissValue(
+    value: androidx.wear.compose.foundation.SwipeToDismissValue
+) = when (value) {
+    androidx.wear.compose.foundation.SwipeToDismissValue.Default ->
+        SwipeToDismissValue.Default
+
+    androidx.wear.compose.foundation.SwipeToDismissValue.Dismissed ->
+        SwipeToDismissValue.Dismissed
+}
+
 private val SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC =
     TweenSpec<Float>(200, 0, LinearOutSlowInEasing)
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ToggleChip.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ToggleChip.kt
index 6627d5d..d6b7da3 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ToggleChip.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ToggleChip.kt
@@ -781,7 +781,7 @@
      * Note that you can override it by applying Modifier.heightIn directly on [ToggleChip] or
      * [SplitToggleChip].
      */
-    internal val Height = 52.dp
+    public val Height = 52.dp
 
     /**
      * The default size of app icons or toggle controls when used inside a [ToggleChip] or
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index d2f7e98..0657516 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -30,6 +30,7 @@
     method public float getButtonHorizontalPadding();
     method public float getButtonVerticalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getHeight();
     method public float getIconSize();
     method public float getLargeIconSize();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors imageBackgroundButtonColors(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush, optional long contentColor, optional long secondaryContentColor, optional long iconColor);
@@ -38,6 +39,7 @@
     property public final float ButtonHorizontalPadding;
     property public final float ButtonVerticalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float Height;
     property public final float IconSize;
     property public final float LargeIconSize;
     field public static final androidx.wear.compose.material3.ButtonDefaults INSTANCE;
@@ -269,6 +271,19 @@
     property @SuppressCompatibility @androidx.wear.compose.material3.ExperimentalWearMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
   }
 
+  public final class ListHeaderDefaults {
+    method public androidx.compose.foundation.layout.PaddingValues getHeaderContentPadding();
+    method public androidx.compose.foundation.layout.PaddingValues getSubheaderContentPadding();
+    property public final androidx.compose.foundation.layout.PaddingValues HeaderContentPadding;
+    property public final androidx.compose.foundation.layout.PaddingValues SubheaderContentPadding;
+    field public static final androidx.wear.compose.material3.ListHeaderDefaults INSTANCE;
+  }
+
+  public final class ListHeaderKt {
+    method @androidx.compose.runtime.Composable public static void ListHeader(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void ListSubheader(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+  }
+
   public final class MaterialTheme {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.wear.compose.material3.ColorScheme getColorScheme();
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.wear.compose.material3.Shapes getShapes();
@@ -342,6 +357,11 @@
     method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
   }
 
+  public final class SwipeToDismissBoxKt {
+    method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional androidx.compose.ui.Modifier modifier, optional long backgroundScrimColor, optional long contentScrimColor, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissed, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional long backgroundScrimColor, optional long contentScrimColor, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
+  }
+
   @androidx.compose.runtime.Immutable public final class TextButtonColors {
     ctor public TextButtonColors(long containerColor, long contentColor, long disabledContainerColor, long disabledContentColor);
     method public long getContainerColor();
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index d2f7e98..0657516 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -30,6 +30,7 @@
     method public float getButtonHorizontalPadding();
     method public float getButtonVerticalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    method public float getHeight();
     method public float getIconSize();
     method public float getLargeIconSize();
     method @androidx.compose.runtime.Composable public androidx.wear.compose.material3.ButtonColors imageBackgroundButtonColors(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush, optional long contentColor, optional long secondaryContentColor, optional long iconColor);
@@ -38,6 +39,7 @@
     property public final float ButtonHorizontalPadding;
     property public final float ButtonVerticalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+    property public final float Height;
     property public final float IconSize;
     property public final float LargeIconSize;
     field public static final androidx.wear.compose.material3.ButtonDefaults INSTANCE;
@@ -269,6 +271,19 @@
     property @SuppressCompatibility @androidx.wear.compose.material3.ExperimentalWearMaterial3Api public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
   }
 
+  public final class ListHeaderDefaults {
+    method public androidx.compose.foundation.layout.PaddingValues getHeaderContentPadding();
+    method public androidx.compose.foundation.layout.PaddingValues getSubheaderContentPadding();
+    property public final androidx.compose.foundation.layout.PaddingValues HeaderContentPadding;
+    property public final androidx.compose.foundation.layout.PaddingValues SubheaderContentPadding;
+    field public static final androidx.wear.compose.material3.ListHeaderDefaults INSTANCE;
+  }
+
+  public final class ListHeaderKt {
+    method @androidx.compose.runtime.Composable public static void ListHeader(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void ListSubheader(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? icon, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> label);
+  }
+
   public final class MaterialTheme {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.wear.compose.material3.ColorScheme getColorScheme();
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.wear.compose.material3.Shapes getShapes();
@@ -342,6 +357,11 @@
     method @androidx.compose.runtime.Composable public static void Stepper(int value, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> onValueChange, kotlin.ranges.IntProgression valueProgression, kotlin.jvm.functions.Function0<kotlin.Unit> decreaseIcon, kotlin.jvm.functions.Function0<kotlin.Unit> increaseIcon, optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional long iconColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
   }
 
+  public final class SwipeToDismissBoxKt {
+    method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional androidx.compose.ui.Modifier modifier, optional long backgroundScrimColor, optional long contentScrimColor, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void SwipeToDismissBox(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissed, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.foundation.SwipeToDismissBoxState state, optional long backgroundScrimColor, optional long contentScrimColor, optional Object backgroundKey, optional Object contentKey, optional boolean userSwipeEnabled, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.BoxScope,? super java.lang.Boolean,kotlin.Unit> content);
+  }
+
   @androidx.compose.runtime.Immutable public final class TextButtonColors {
     ctor public TextButtonColors(long containerColor, long contentColor, long disabledContainerColor, long disabledContentColor);
     method public long getContainerColor();
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
index bad3230..d546a2e 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ButtonDemo.kt
@@ -16,21 +16,24 @@
 
 package androidx.wear.compose.material3.demos
 
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.semantics.heading
-import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextOverflow
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.Button
 import androidx.wear.compose.material3.ButtonDefaults
 import androidx.wear.compose.material3.ChildButton
 import androidx.wear.compose.material3.FilledTonalButton
 import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.OutlinedButton
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.samples.ButtonSample
@@ -49,10 +52,9 @@
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
-            Text(
-                text = "1 slot button",
-                modifier = Modifier.semantics { heading() }
-            )
+            ListHeader {
+                Text("1 slot button")
+            }
         }
         item {
             SimpleButtonSample()
@@ -65,10 +67,9 @@
             )
         }
         item {
-            Text(
-                text = "3 slot button",
-                modifier = Modifier.semantics { heading() }
-            )
+            ListHeader {
+                Text("3 slot button")
+            }
         }
         item {
             ButtonSample()
@@ -98,10 +99,9 @@
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
-            Text(
-                text = "1 slot button",
-                modifier = Modifier.semantics { heading() }
-            )
+            ListHeader {
+                Text("1 slot button")
+            }
         }
         item {
             SimpleFilledTonalButtonSample()
@@ -114,10 +114,9 @@
             )
         }
         item {
-            Text(
-                text = "3 slot button",
-                modifier = Modifier.semantics { heading() }
-            )
+            ListHeader {
+                Text("3 slot button")
+            }
         }
         item {
             FilledTonalButtonSample()
@@ -147,10 +146,9 @@
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
-            Text(
-                text = "1 slot button",
-                modifier = Modifier.semantics { heading() }
-            )
+            ListHeader {
+                Text("1 slot button")
+            }
         }
         item {
             SimpleOutlinedButtonSample()
@@ -163,10 +161,9 @@
             )
         }
         item {
-            Text(
-                text = "3 slot button",
-                modifier = Modifier.semantics { heading() }
-            )
+            ListHeader {
+                Text("3 slot button")
+            }
         }
         item {
             OutlinedButtonSample()
@@ -196,10 +193,9 @@
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
-            Text(
-                text = "1 slot button",
-                modifier = Modifier.semantics { heading() }
-            )
+            ListHeader {
+                Text("1 slot button")
+            }
         }
         item {
             SimpleChildButtonSample()
@@ -212,10 +208,9 @@
             )
         }
         item {
-            Text(
-                text = "3 slot button",
-                modifier = Modifier.semantics { heading() }
-            )
+            ListHeader {
+                Text("3 slot button")
+            }
         }
         item {
             ChildButtonSample()
@@ -236,4 +231,159 @@
             )
         }
     }
+}
+
+@Composable
+fun MultilineButtonDemo() {
+    ScalingLazyColumn(
+        modifier = Modifier.fillMaxSize(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        item {
+            ListHeader {
+                Text("3 line label")
+            }
+        }
+        item {
+            MultilineButton(enabled = true)
+        }
+        item {
+            MultilineButton(enabled = false)
+        }
+        item {
+            MultilineButton(enabled = true, icon = { StandardIcon() })
+        }
+        item {
+            MultilineButton(enabled = false, icon = { StandardIcon() })
+        }
+        item {
+            ListHeader {
+                Text("5 line button")
+            }
+        }
+        item {
+            Multiline3SlotButton(enabled = true)
+        }
+        item {
+            Multiline3SlotButton(enabled = false)
+        }
+        item {
+            Multiline3SlotButton(enabled = true, icon = { StandardIcon() })
+        }
+        item {
+            Multiline3SlotButton(enabled = false, icon = { StandardIcon() })
+        }
+    }
+}
+
+@Composable
+fun AvatarButtonDemo() {
+    ScalingLazyColumn(
+        modifier = Modifier.fillMaxSize(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        item {
+            ListHeader {
+                Text("Label + Avatar")
+            }
+        }
+        item {
+            AvatarButton(enabled = true)
+        }
+        item {
+            AvatarButton(enabled = false)
+        }
+        item {
+            ListHeader {
+                Text("Primary/Secondary + Avatar")
+            }
+        }
+        item {
+            Avatar3SlotButton(enabled = true)
+        }
+        item {
+            Avatar3SlotButton(enabled = false)
+        }
+    }
+}
+
+@Composable
+private fun AvatarButton(enabled: Boolean) =
+    MultilineButton(
+        enabled = enabled, icon = { AvatarIcon() }, label = { Text("Primary text") }
+    )
+
+@Composable
+private fun Avatar3SlotButton(enabled: Boolean) =
+    Multiline3SlotButton(
+        enabled = enabled,
+        icon = { AvatarIcon() },
+        label = { Text("Primary text") },
+        secondaryLabel = { Text("Secondary label") }
+    )
+
+@Composable
+private fun MultilineButton(
+    enabled: Boolean,
+    icon: (@Composable BoxScope.() -> Unit)? = null,
+    label: @Composable RowScope.() -> Unit = {
+        Text(
+            text = "Multiline label that include a lot of text and stretches to third line",
+            maxLines = 3,
+            overflow = TextOverflow.Ellipsis,
+        )
+    },
+) {
+    Button(
+        onClick = { /* Do something */ },
+        icon = icon,
+        label = label,
+        enabled = enabled
+    )
+}
+
+@Composable
+private fun Multiline3SlotButton(
+    enabled: Boolean,
+    icon: (@Composable BoxScope.() -> Unit)? = null,
+    label: @Composable RowScope.() -> Unit = {
+        Text(
+            text = "Multiline label that include a lot of text and stretches to third line",
+            maxLines = 3,
+            overflow = TextOverflow.Ellipsis,
+        )
+    },
+    secondaryLabel: @Composable RowScope.() -> Unit = {
+        Text(
+            text = "Secondary label over two lines",
+            maxLines = 2,
+            overflow = TextOverflow.Ellipsis,
+        )
+    },
+) {
+    Button(
+        onClick = { /* Do something */ },
+        icon = icon,
+        label = label,
+        secondaryLabel = secondaryLabel,
+        enabled = enabled
+    )
+}
+
+@Composable
+private fun StandardIcon() {
+    Icon(
+        Icons.Filled.Favorite,
+        contentDescription = "Favorite icon",
+        modifier = Modifier.size(ButtonDefaults.IconSize)
+    )
+}
+
+@Composable
+private fun AvatarIcon() {
+    Icon(
+        Icons.Filled.AccountCircle,
+        contentDescription = "Account",
+        modifier = Modifier.size(ButtonDefaults.LargeIconSize)
+    )
 }
\ No newline at end of file
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
index b647153..dbd1fff 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
@@ -31,6 +31,7 @@
 import androidx.wear.compose.material3.FilledTonalIconButton
 import androidx.wear.compose.material3.Icon
 import androidx.wear.compose.material3.IconButton
+import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.OutlinedIconButton
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.samples.FilledIconButtonSample
@@ -45,7 +46,9 @@
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
-            Text("Icon Button")
+            ListHeader {
+                Text("Icon button")
+            }
         }
         item {
             Row {
@@ -63,7 +66,9 @@
             }
         }
         item {
-            Text("FilledTonalIconButton")
+            ListHeader {
+                Text("FilledTonalIconButton")
+            }
         }
         item {
             Row {
@@ -81,7 +86,9 @@
             }
         }
         item {
-            Text("FilledIconButton")
+            ListHeader {
+                Text("FilledIconButton")
+            }
         }
         item {
             Row {
@@ -99,7 +106,9 @@
             }
         }
         item {
-            Text("OutlinedIconButton")
+            ListHeader {
+                Text("OutlinedIconButton")
+            }
         }
         item {
             Row {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt
new file mode 100644
index 0000000..0b88f4a
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/ListHeaderDemo.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.wear.compose.material3.demos
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.material3.samples.ListHeaderSample
+import androidx.wear.compose.material3.samples.ListSubheaderSample
+import androidx.wear.compose.material3.samples.ListSubheaderWithIconSample
+
+@Composable
+fun ListHeaderDemo() {
+    ScalingLazyColumn(
+        modifier = Modifier.fillMaxWidth()
+    ) {
+        item {
+            ListHeaderSample()
+        }
+        item {
+            ListSubheaderSample()
+        }
+        item {
+            ListSubheaderWithIconSample()
+        }
+    }
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
index cd29d8f..4ad6082 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/SliderDemo.kt
@@ -42,6 +42,7 @@
 import androidx.wear.compose.material3.InlineSlider
 import androidx.wear.compose.material3.InlineSliderColors
 import androidx.wear.compose.material3.InlineSliderDefaults
+import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.samples.InlineSliderSample
 import androidx.wear.compose.material3.samples.InlineSliderSegmentedSample
@@ -97,7 +98,11 @@
         modifier = Modifier.fillMaxSize(),
         autoCentering = AutoCenteringParams(itemIndex = 0)
     ) {
-        item { Text("Enabled Slider, value = $enabledValue") }
+        item {
+            ListHeader {
+                Text("Enabled Slider, value = $enabledValue")
+            }
+        }
         item {
             DefaultInlineSlider(
                 value = enabledValue,
@@ -108,7 +113,11 @@
                 onValueChange = { enabledValue = it }
             )
         }
-        item { Text("Disabled Slider, value = $disabledValue") }
+        item {
+            ListHeader {
+                Text("Disabled Slider, value = $disabledValue")
+            }
+        }
         item {
             DefaultInlineSlider(
                 value = disabledValue,
@@ -136,7 +145,11 @@
         modifier = Modifier.fillMaxSize(),
         autoCentering = AutoCenteringParams(itemIndex = 0)
     ) {
-        item { Text("No segments, value = $valueWithoutSegments") }
+        item {
+            ListHeader {
+                Text("No segments, value = $valueWithoutSegments")
+            }
+        }
         item {
             DefaultInlineSlider(
                 value = valueWithoutSegments,
@@ -144,7 +157,11 @@
                 segmented = false,
                 onValueChange = { valueWithoutSegments = it })
         }
-        item { Text("With segments, value = $valueWithSegments") }
+        item {
+            ListHeader {
+                Text("With segments, value = $valueWithSegments")
+            }
+        }
         item {
             DefaultInlineSlider(
                 value = valueWithSegments,
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
index 382812b..3ba251d 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.ListHeader
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.TextButton
 import androidx.wear.compose.material3.TextButtonDefaults
@@ -41,7 +42,9 @@
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         item {
-            Text("Text Button")
+            ListHeader {
+                Text("Text Button")
+            }
         }
         item {
             Row {
@@ -53,7 +56,9 @@
             }
         }
         item {
-            Text("FilledTonalTextButton")
+            ListHeader {
+                Text("FilledTonalTextButton")
+            }
         }
         item {
             Row {
@@ -69,7 +74,9 @@
             }
         }
         item {
-            Text("FilledTextButton")
+            ListHeader {
+                Text("FilledTextButton")
+            }
         }
         item {
             Row {
@@ -85,7 +92,9 @@
             }
         }
         item {
-            Text("OutlinedTextButton")
+            ListHeader {
+                Text("OutlinedTextButton")
+            }
         }
         item {
             Row {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 66c6a63..d4df8fc 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -52,7 +52,13 @@
                 },
                 ComposableDemo("Child Button") {
                     ChildButtonDemo()
-                }
+                },
+                ComposableDemo("Multiline Button") {
+                    MultilineButtonDemo()
+                },
+                ComposableDemo("Avatar Button") {
+                    AvatarButtonDemo()
+                },
             )
         ),
         DemoCategory(
@@ -104,6 +110,11 @@
             "Slider",
             SliderDemos
         ),
+        ComposableDemo("List Headers") {
+            Centralize {
+                ListHeaderDemo()
+            }
+        },
         ComposableDemo(
             title = "Fixed Font Size"
         ) {
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ListHeaderSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ListHeaderSample.kt
new file mode 100644
index 0000000..ef263e8
--- /dev/null
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ListHeaderSample.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Home
+import androidx.compose.runtime.Composable
+import androidx.wear.compose.material3.Icon
+import androidx.wear.compose.material3.ListHeader
+import androidx.wear.compose.material3.ListSubheader
+import androidx.wear.compose.material3.Text
+
+@Sampled
+@Composable
+fun ListHeaderSample() {
+    ListHeader {
+        Text("List Header")
+    }
+}
+
+@Sampled
+@Composable
+fun ListSubheaderSample() {
+    ListSubheader {
+        Text("List Subheader")
+    }
+}
+
+@Sampled
+@Composable
+fun ListSubheaderWithIconSample() {
+    ListSubheader(
+        label = { Text(text = "List Subheader") },
+        icon = { Icon(imageVector = Icons.Outlined.Home, "home") }
+    )
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ListHeaderTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ListHeaderTest.kt
new file mode 100644
index 0000000..9ebc106
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ListHeaderTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.wear.compose.material3
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.isHeading
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.TextStyle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ListHeaderTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun supports_testtag() {
+        rule.setContentWithTheme {
+            ListHeader(
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                Text("Header")
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun listHeader_has_semantic_heading_property() {
+        rule.setContentWithTheme {
+            ListHeader(
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                Text("Header")
+            }
+        }
+
+        rule.onNode(isHeading())
+    }
+
+    @Test
+    fun listSubheader_has_semantic_heading_property() {
+        rule.setContentWithTheme {
+            ListSubheader(
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                Text("Subheader")
+            }
+        }
+
+        rule.onNode(isHeading())
+    }
+
+    @Test
+    fun gives_listHeader_correct_text_style() {
+        var actualTextStyle = TextStyle.Default
+        var expectedTextStyle = TextStyle.Default
+
+        rule.setContentWithTheme {
+            expectedTextStyle = MaterialTheme.typography.buttonMedium
+            ListHeader {
+                actualTextStyle = LocalTextStyle.current
+            }
+        }
+
+        Assert.assertEquals(expectedTextStyle, actualTextStyle)
+    }
+
+    @Test
+    fun gives_listSubheader_correct_text_style() {
+        var actualTextStyle = TextStyle.Default
+        var expectedTextStyle = TextStyle.Default
+
+        rule.setContentWithTheme {
+            expectedTextStyle = MaterialTheme.typography.captionLarge
+            ListSubheader {
+                actualTextStyle = LocalTextStyle.current
+            }
+        }
+
+        Assert.assertEquals(expectedTextStyle, actualTextStyle)
+    }
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 1f5b63b..6c8de9eb59 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -922,7 +922,7 @@
      * The default height applied for the [Button].
      * Note that you can override it by applying Modifier.heightIn directly on [Button].
      */
-    internal val Height = 52.dp
+    val Height = 52.dp
 
     /**
      * The default size of the spacing between an icon and a text when they are used inside a
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ListHeader.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ListHeader.kt
new file mode 100644
index 0000000..f680518
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ListHeader.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.wear.compose.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.heading
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+
+/**
+ * A slot based composable for creating a list header item. [ListHeader]s are typically expected
+ * to be a few words of text on a single line.
+ * The contents will be start and end padded.
+ *
+ * TODO(b/261838497) Add Material3 UX guidance links
+ *
+ * Example of a [ListHeader]:
+ * @sample androidx.wear.compose.material3.samples.ListHeaderSample
+ *
+ * @param modifier The modifier for the [ListHeader].
+ * @param backgroundColor The background color to apply - typically Color.Transparent
+ * @param contentColor The color to apply to content.
+ * @param contentPadding The spacing values to apply internally between the container
+ * and the content.
+ * @param content Slot for [ListHeader] content, expected to be a single line of text.
+ */
+@Composable
+public fun ListHeader(
+    modifier: Modifier = Modifier,
+    backgroundColor: Color = Color.Transparent,
+    contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+    contentPadding: PaddingValues = ListHeaderDefaults.HeaderContentPadding,
+    content: @Composable RowScope.() -> Unit
+) {
+    Row(
+        modifier = modifier
+            .height(48.dp)
+            .wrapContentSize()
+            .background(backgroundColor)
+            .padding(contentPadding)
+            .semantics { heading() }
+    ) {
+        CompositionLocalProvider(
+            LocalContentColor provides contentColor,
+            LocalTextStyle provides MaterialTheme.typography.buttonMedium,
+        ) {
+            content()
+        }
+    }
+}
+
+/**
+ * A two slot based composable for creating a list subheader item.
+ * [ListSubheader]s offer slots for an icon and for a text label.
+ * The contents will be start and end padded.
+ *
+ * TODO(b/261838497) Add Material3 UX guidance links
+ *
+ * Example of a [ListSubheader]:
+ * @sample androidx.wear.compose.material3.samples.ListSubheaderSample
+ *
+ * Example of a [ListSubheader] with an icon:
+ * @sample androidx.wear.compose.material3.samples.ListSubheaderWithIconSample
+ *
+ * @param modifier The modifier for the [ListSubheader].
+ * @param backgroundColor The background color to apply - typically Color.Transparent
+ * @param contentColor The color to apply to content.
+ * @param contentPadding The spacing values to apply internally between the container
+ * and the content.
+ * @param icon A slot for providing icon to the [ListSubheader].
+ * @param label A slot for providing label to the [ListSubheader].
+ */
+@Composable
+public fun ListSubheader(
+    modifier: Modifier = Modifier,
+    backgroundColor: Color = Color.Transparent,
+    contentColor: Color = MaterialTheme.colorScheme.onBackground,
+    contentPadding: PaddingValues = ListHeaderDefaults.SubheaderContentPadding,
+    icon: (@Composable BoxScope.() -> Unit)? = null,
+    label: @Composable RowScope.() -> Unit,
+) {
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        modifier = modifier
+            .height(48.dp)
+            .wrapContentSize()
+            .background(backgroundColor)
+            .padding(contentPadding)
+            .semantics { heading() }
+    ) {
+        CompositionLocalProvider(
+            LocalContentColor provides contentColor,
+            LocalTextStyle provides MaterialTheme.typography.captionLarge,
+        ) {
+            if (icon != null) {
+                Box(
+                    modifier = Modifier
+                        .wrapContentSize(align = Alignment.CenterStart),
+                    content = icon
+                )
+                Spacer(modifier = Modifier.width(6.dp))
+            }
+            label()
+        }
+    }
+}
+
+object ListHeaderDefaults {
+    private val TOP_PADDING = 16.dp
+    private val SUBHEADER_BOTTOM_PADDING = 8.dp
+    private val HEADER_BOTTOM_PADDING = 12.dp
+    private val HORIZONTAL_PADDING = 14.dp
+
+    val HeaderContentPadding = PaddingValues(
+        HORIZONTAL_PADDING,
+        TOP_PADDING,
+        HORIZONTAL_PADDING,
+        HEADER_BOTTOM_PADDING
+    )
+    val SubheaderContentPadding = PaddingValues(
+        HORIZONTAL_PADDING,
+        TOP_PADDING,
+        HORIZONTAL_PADDING,
+        SUBHEADER_BOTTOM_PADDING
+    )
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
index 9c15d6f2..4a3bdb1 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
@@ -25,6 +25,8 @@
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.remember
+import androidx.wear.compose.foundation.LocalSwipeToDismissBackgroundScrimColor
+import androidx.wear.compose.foundation.LocalSwipeToDismissContentScrimColor
 
 /**
  * MaterialTheme defines the styling principles from the Wear Material3 design specification
@@ -74,7 +76,8 @@
         LocalIndication provides rippleIndication,
         LocalRippleTheme provides MaterialRippleTheme,
         LocalTextSelectionColors provides selectionColors,
-
+        LocalSwipeToDismissBackgroundScrimColor provides rememberedColors.background,
+        LocalSwipeToDismissContentScrimColor provides rememberedColors.background
         ) {
         ProvideTextStyle(value = typography.bodyLarge, content = content)
     }
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToDismissBox.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToDismissBox.kt
new file mode 100644
index 0000000..03a8069
--- /dev/null
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwipeToDismissBox.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.wear.compose.material3
+
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.key
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.wear.compose.foundation.LocalSwipeToDismissBackgroundScrimColor
+import androidx.wear.compose.foundation.LocalSwipeToDismissContentScrimColor
+import androidx.wear.compose.foundation.SwipeToDismissBoxState
+import androidx.wear.compose.foundation.SwipeToDismissKeys
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
+
+/**
+ * Wear Material 3 [SwipeToDismissBox] that handles the swipe-to-dismiss gesture. Takes a single
+ * slot for the background (only displayed during the swipe gesture) and the foreground content.
+ *
+ * For more information, see the
+ * [Swipe to dismiss](https://developer.android.com/training/wearables/components/swipe-to-dismiss)
+ * guide.
+ *
+ * @param state State containing information about ongoing swipe or animation.
+ * @param modifier [Modifier] for this component.
+ * @param backgroundScrimColor [Color] for background scrim.
+ * @param contentScrimColor [Color] used for the scrim over the content composable during
+ * the swipe gesture.
+ * @param backgroundKey [key] which identifies the content currently composed in
+ * the [content] block when isBackground == true. Provide the backgroundKey if your background
+ * content will be displayed as a foreground after the swipe animation ends
+ * (as is common when [SwipeToDismissBox] is used for the navigation). This allows
+ * remembered state to be correctly moved between background and foreground.
+ * @param contentKey [key] which identifies the content currently composed in the
+ * [content] block when isBackground == false. See [backgroundKey].
+ * @param userSwipeEnabled Whether the swipe gesture is enabled.
+ * (e.g. when there is no background screen, set userSwipeEnabled = false)
+ * @param content Slot for content, with the isBackground parameter enabling content to be
+ * displayed behind the foreground content - the background is normally hidden, is shown behind a
+ * scrim during the swipe gesture, and is shown without scrim once the finger passes the
+ * swipe-to-dismiss threshold.
+ */
+@Composable
+public fun SwipeToDismissBox(
+    state: SwipeToDismissBoxState,
+    modifier: Modifier = Modifier,
+    backgroundScrimColor: Color = MaterialTheme.colorScheme.background,
+    contentScrimColor: Color = MaterialTheme.colorScheme.background,
+    backgroundKey: Any = SwipeToDismissKeys.Background,
+    contentKey: Any = SwipeToDismissKeys.Content,
+    userSwipeEnabled: Boolean = true,
+    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
+) {
+    CompositionLocalProvider(
+        LocalSwipeToDismissBackgroundScrimColor provides backgroundScrimColor,
+        LocalSwipeToDismissContentScrimColor provides contentScrimColor
+    ) {
+        androidx.wear.compose.foundation.SwipeToDismissBox(
+            state = state,
+            modifier = modifier,
+            backgroundKey = backgroundKey,
+            contentKey = contentKey,
+            userSwipeEnabled = userSwipeEnabled,
+            content = content
+        )
+    }
+}
+
+/**
+ * Wear Material 3 [SwipeToDismissBox] that handles the swipe-to-dismiss gesture.
+ * This overload takes an [onDismissed] parameter which is used to execute a command when the
+ * swipe to dismiss has completed, such as navigating to another screen.
+ *
+ * For more information, see the
+ * [Swipe to dismiss](https://developer.android.com/training/wearables/components/swipe-to-dismiss)
+ * guide.
+ *
+ * @param onDismissed Executes when the swipe to dismiss has completed.
+ * @param modifier [Modifier] for this component.
+ * @param state State containing information about ongoing swipe or animation.
+ * @param backgroundScrimColor [Color] for background scrim.
+ * @param contentScrimColor [Color] used for the scrim over the content composable during
+ * the swipe gesture.
+ * @param backgroundKey [key] which identifies the content currently composed in
+ * the [content] block when isBackground == true. Provide the backgroundKey if your background
+ * content will be displayed as a foreground after the swipe animation ends
+ * (as is common when [SwipeToDismissBox] is used for the navigation). This allows
+ * remembered state to be correctly moved between background and foreground.
+ * @param contentKey [key] which identifies the content currently composed in the
+ * [content] block when isBackground == false. See [backgroundKey].
+ * @param userSwipeEnabled Whether the swipe gesture is enabled.
+ * (e.g. when there is no background screen, set userSwipeEnabled = false)
+ * @param content Slot for content, with the isBackground parameter enabling content to be
+ * displayed behind the foreground content - the background is normally hidden, is shown behind a
+ * scrim during the swipe gesture, and is shown without scrim once the finger passes the
+ * swipe-to-dismiss threshold.
+ */
+@Composable
+public fun SwipeToDismissBox(
+    onDismissed: () -> Unit,
+    modifier: Modifier = Modifier,
+    state: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(),
+    backgroundScrimColor: Color = MaterialTheme.colorScheme.background,
+    contentScrimColor: Color = MaterialTheme.colorScheme.background,
+    backgroundKey: Any = SwipeToDismissKeys.Background,
+    contentKey: Any = SwipeToDismissKeys.Content,
+    userSwipeEnabled: Boolean = true,
+    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
+) {
+    CompositionLocalProvider(
+        LocalSwipeToDismissBackgroundScrimColor provides backgroundScrimColor,
+        LocalSwipeToDismissContentScrimColor provides contentScrimColor
+    ) {
+        androidx.wear.compose.foundation.SwipeToDismissBox(
+            state = state,
+            modifier = modifier,
+            onDismissed = onDismissed,
+            backgroundKey = backgroundKey,
+            contentKey = contentKey,
+            userSwipeEnabled = userSwipeEnabled,
+            content = content
+        )
+    }
+}
diff --git a/wear/compose/compose-navigation/api/current.txt b/wear/compose/compose-navigation/api/current.txt
index 96ffadf..e27dd9d 100644
--- a/wear/compose/compose-navigation/api/current.txt
+++ b/wear/compose/compose-navigation/api/current.txt
@@ -11,13 +11,17 @@
   }
 
   public final class SwipeDismissableNavHostKt {
-    method @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, androidx.navigation.NavGraph graph, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state);
-    method @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, String startDestination, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state, optional String? route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.navigation.SwipeDismissableNavHostState rememberSwipeDismissableNavHostState(optional androidx.wear.compose.material.SwipeToDismissBoxState swipeToDismissBoxState);
+    method @Deprecated @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, androidx.navigation.NavGraph graph, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state);
+    method @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, androidx.navigation.NavGraph graph, optional androidx.compose.ui.Modifier modifier, optional boolean userSwipeEnabled, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state);
+    method @Deprecated @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, String startDestination, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state, optional String? route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
+    method @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, String startDestination, optional androidx.compose.ui.Modifier modifier, optional boolean userSwipeEnabled, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state, optional String? route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.navigation.SwipeDismissableNavHostState rememberSwipeDismissableNavHostState(optional androidx.wear.compose.foundation.SwipeToDismissBoxState swipeToDismissBoxState);
+    method @Deprecated @androidx.compose.runtime.Composable public static androidx.wear.compose.navigation.SwipeDismissableNavHostState rememberSwipeDismissableNavHostState(optional androidx.wear.compose.material.SwipeToDismissBoxState swipeToDismissBoxState);
   }
 
   public final class SwipeDismissableNavHostState {
-    ctor public SwipeDismissableNavHostState(androidx.wear.compose.material.SwipeToDismissBoxState swipeToDismissBoxState);
+    ctor public SwipeDismissableNavHostState(androidx.wear.compose.foundation.SwipeToDismissBoxState swipeToDismissBoxState);
+    ctor @Deprecated public SwipeDismissableNavHostState(androidx.wear.compose.material.SwipeToDismissBoxState swipeToDismissBoxState);
   }
 
   @androidx.navigation.Navigator.Name("wear-navigator") public final class WearNavigator extends androidx.navigation.Navigator<androidx.wear.compose.navigation.WearNavigator.Destination> {
diff --git a/wear/compose/compose-navigation/api/restricted_current.txt b/wear/compose/compose-navigation/api/restricted_current.txt
index 96ffadf..e27dd9d 100644
--- a/wear/compose/compose-navigation/api/restricted_current.txt
+++ b/wear/compose/compose-navigation/api/restricted_current.txt
@@ -11,13 +11,17 @@
   }
 
   public final class SwipeDismissableNavHostKt {
-    method @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, androidx.navigation.NavGraph graph, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state);
-    method @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, String startDestination, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state, optional String? route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
-    method @androidx.compose.runtime.Composable public static androidx.wear.compose.navigation.SwipeDismissableNavHostState rememberSwipeDismissableNavHostState(optional androidx.wear.compose.material.SwipeToDismissBoxState swipeToDismissBoxState);
+    method @Deprecated @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, androidx.navigation.NavGraph graph, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state);
+    method @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, androidx.navigation.NavGraph graph, optional androidx.compose.ui.Modifier modifier, optional boolean userSwipeEnabled, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state);
+    method @Deprecated @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, String startDestination, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state, optional String? route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
+    method @androidx.compose.runtime.Composable public static void SwipeDismissableNavHost(androidx.navigation.NavHostController navController, String startDestination, optional androidx.compose.ui.Modifier modifier, optional boolean userSwipeEnabled, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState state, optional String? route, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
+    method @androidx.compose.runtime.Composable public static androidx.wear.compose.navigation.SwipeDismissableNavHostState rememberSwipeDismissableNavHostState(optional androidx.wear.compose.foundation.SwipeToDismissBoxState swipeToDismissBoxState);
+    method @Deprecated @androidx.compose.runtime.Composable public static androidx.wear.compose.navigation.SwipeDismissableNavHostState rememberSwipeDismissableNavHostState(optional androidx.wear.compose.material.SwipeToDismissBoxState swipeToDismissBoxState);
   }
 
   public final class SwipeDismissableNavHostState {
-    ctor public SwipeDismissableNavHostState(androidx.wear.compose.material.SwipeToDismissBoxState swipeToDismissBoxState);
+    ctor public SwipeDismissableNavHostState(androidx.wear.compose.foundation.SwipeToDismissBoxState swipeToDismissBoxState);
+    ctor @Deprecated public SwipeDismissableNavHostState(androidx.wear.compose.material.SwipeToDismissBoxState swipeToDismissBoxState);
   }
 
   @androidx.navigation.Navigator.Name("wear-navigator") public final class WearNavigator extends androidx.navigation.Navigator<androidx.wear.compose.navigation.WearNavigator.Destination> {
diff --git a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
index 655e7bc..360df14 100644
--- a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
+++ b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
@@ -101,6 +101,21 @@
     }
 
     @Test
+    fun does_not_navigate_back_to_previous_level_when_swipe_disabled() {
+        rule.setContentWithTheme {
+            SwipeDismissWithNavigation(userSwipeEnabled = false)
+        }
+
+        // Click to move to next destination then swipe to dismiss.
+        rule.onNodeWithText(START).performClick()
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { swipeRight() }
+
+        // Should still display "next".
+        rule.onNodeWithText(NEXT).assertExists()
+        rule.onNodeWithText(START).assertDoesNotExist()
+    }
+
+    @Test
     fun navigates_back_to_previous_level_with_back_button() {
         val onBackPressedDispatcher = OnBackPressedDispatcher()
         val dispatcherOwner =
@@ -423,12 +438,14 @@
 
     @Composable
     fun SwipeDismissWithNavigation(
-        navController: NavHostController = rememberSwipeDismissableNavController()
+        navController: NavHostController = rememberSwipeDismissableNavController(),
+        userSwipeEnabled: Boolean = true
     ) {
         SwipeDismissableNavHost(
             navController = navController,
             startDestination = START,
             modifier = Modifier.testTag(TEST_TAG),
+            userSwipeEnabled = userSwipeEnabled
         ) {
             composable(START) {
                 CompactChip(
diff --git a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
index 95f6fbe..377b170 100644
--- a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
+++ b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
@@ -43,12 +43,14 @@
 import androidx.navigation.compose.LocalOwnersProvider
 import androidx.navigation.createGraph
 import androidx.navigation.get
-import androidx.wear.compose.material.SwipeToDismissBox
-import androidx.wear.compose.material.SwipeToDismissBoxState
-import androidx.wear.compose.material.SwipeToDismissKeys
-import androidx.wear.compose.material.SwipeToDismissValue
-import androidx.wear.compose.material.edgeSwipeToDismiss
-import androidx.wear.compose.material.rememberSwipeToDismissBoxState
+import androidx.wear.compose.foundation.LocalSwipeToDismissBackgroundScrimColor
+import androidx.wear.compose.foundation.LocalSwipeToDismissContentScrimColor
+import androidx.wear.compose.foundation.SwipeToDismissBox
+import androidx.wear.compose.foundation.SwipeToDismissBoxState
+import androidx.wear.compose.foundation.SwipeToDismissKeys
+import androidx.wear.compose.foundation.SwipeToDismissValue
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 
 /**
  * Provides a place in the Compose hierarchy for self-contained navigation to occur,
@@ -62,7 +64,8 @@
  *
  * Content is displayed within a [SwipeToDismissBox], showing the current navigation level.
  * During a swipe-to-dismiss gesture, the previous navigation level (if any) is shown in
- * the background.
+ * the background. BackgroundScrimColor and ContentScrimColor of it are taken from
+ * [LocalSwipeToDismissBackgroundScrimColor] and [LocalSwipeToDismissContentScrimColor].
  *
  * Example of a [SwipeDismissableNavHost] alternating between 2 screens:
  * @sample androidx.wear.compose.navigation.samples.SimpleNavHost
@@ -73,6 +76,7 @@
  * @param navController The navController for this host
  * @param startDestination The route for the start destination
  * @param modifier The modifier to be applied to the layout
+ * @param userSwipeEnabled [Boolean] Whether swipe-to-dismiss gesture is enabled.
  * @param state State containing information about ongoing swipe and animation.
  * @param route The route for the graph
  * @param builder The builder used to construct the graph
@@ -82,6 +86,7 @@
     navController: NavHostController,
     startDestination: String,
     modifier: Modifier = Modifier,
+    userSwipeEnabled: Boolean = true,
     state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(),
     route: String? = null,
     builder: NavGraphBuilder.() -> Unit
@@ -92,6 +97,7 @@
             navController.createGraph(startDestination, route, builder)
         },
         modifier,
+        userSwipeEnabled,
         state = state,
     )
 
@@ -107,7 +113,8 @@
  *
  * Content is displayed within a [SwipeToDismissBox], showing the current navigation level.
  * During a swipe-to-dismiss gesture, the previous navigation level (if any) is shown in
- * the background.
+ * the background. BackgroundScrimColor and ContentScrimColor of it are taken from
+ * [LocalSwipeToDismissBackgroundScrimColor] and [LocalSwipeToDismissContentScrimColor].
  *
  * Example of a [SwipeDismissableNavHost] alternating between 2 screens:
  * @sample androidx.wear.compose.navigation.samples.SimpleNavHost
@@ -118,6 +125,7 @@
  * @param navController [NavHostController] for this host
  * @param graph Graph for this host
  * @param modifier [Modifier] to be applied to the layout
+ * @param userSwipeEnabled [Boolean] Whether swipe-to-dismiss gesture is enabled.
  * @param state State containing information about ongoing swipe and animation.
  *
  * @throws IllegalArgumentException if no WearNavigation.Destination is on the navigation backstack.
@@ -127,6 +135,7 @@
     navController: NavHostController,
     graph: NavGraph,
     modifier: Modifier = Modifier,
+    userSwipeEnabled: Boolean = true,
     state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(),
 ) {
     val lifecycleOwner = LocalLifecycleOwner.current
@@ -213,7 +222,7 @@
     SwipeToDismissBox(
         state = swipeState,
         modifier = Modifier,
-        hasBackground = previous != null,
+        userSwipeEnabled = userSwipeEnabled && previous != null,
         backgroundKey = previous?.id ?: SwipeToDismissKeys.Background,
         contentKey = current?.id ?: SwipeToDismissKeys.Content,
         content = { isBackground ->
@@ -239,6 +248,104 @@
 }
 
 /**
+ * Provides a place in the Compose hierarchy for self-contained navigation to occur,
+ * with backwards navigation provided by a swipe gesture.
+ *
+ * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
+ * the provided [navController].
+ *
+ * The builder passed into this method is [remember]ed. This means that for this NavHost, the
+ * contents of the builder cannot be changed.
+ *
+ * Content is displayed within a [SwipeToDismissBox], showing the current navigation level.
+ * During a swipe-to-dismiss gesture, the previous navigation level (if any) is shown in
+ * the background. BackgroundScrimColor and ContentScrimColor of it are taken from
+ * [LocalSwipeToDismissBackgroundScrimColor] and [LocalSwipeToDismissContentScrimColor].
+ *
+ * Example of a [SwipeDismissableNavHost] alternating between 2 screens:
+ * @sample androidx.wear.compose.navigation.samples.SimpleNavHost
+ *
+ * Example of a [SwipeDismissableNavHost] for which a destination has a named argument:
+ * @sample androidx.wear.compose.navigation.samples.NavHostWithNamedArgument
+ *
+ * @param navController The navController for this host
+ * @param startDestination The route for the start destination
+ * @param modifier The modifier to be applied to the layout
+ * @param state State containing information about ongoing swipe and animation.
+ * @param route The route for the graph
+ * @param builder The builder used to construct the graph
+ */
+@Deprecated(
+    "This overload is provided for backwards compatibility. " +
+        "A newer overload is available with an additional userSwipeEnabled param.",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun SwipeDismissableNavHost(
+    navController: NavHostController,
+    startDestination: String,
+    modifier: Modifier = Modifier,
+    state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(),
+    route: String? = null,
+    builder: NavGraphBuilder.() -> Unit
+) = SwipeDismissableNavHost(
+    navController = navController,
+    startDestination = startDestination,
+    modifier = modifier,
+    userSwipeEnabled = true,
+    state = state,
+    route = route,
+    builder = builder
+)
+
+/**
+ * Provides a place in the Compose hierarchy for self-contained navigation to occur,
+ * with backwards navigation provided by a swipe gesture.
+ *
+ * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
+ * the provided [navController].
+ *
+ * The builder passed into this method is [remember]ed. This means that for this NavHost, the
+ * contents of the builder cannot be changed.
+ *
+ * Content is displayed within a [SwipeToDismissBox], showing the current navigation level.
+ * During a swipe-to-dismiss gesture, the previous navigation level (if any) is shown in
+ * the background. BackgroundScrimColor and ContentScrimColor of it are taken from
+ * [LocalSwipeToDismissBackgroundScrimColor] and [LocalSwipeToDismissContentScrimColor].
+ *
+ * Example of a [SwipeDismissableNavHost] alternating between 2 screens:
+ * @sample androidx.wear.compose.navigation.samples.SimpleNavHost
+ *
+ * Example of a [SwipeDismissableNavHost] for which a destination has a named argument:
+ * @sample androidx.wear.compose.navigation.samples.NavHostWithNamedArgument
+ *
+ * @param navController [NavHostController] for this host
+ * @param graph Graph for this host
+ * @param modifier [Modifier] to be applied to the layout
+ * @param state State containing information about ongoing swipe and animation.
+ *
+ * @throws IllegalArgumentException if no WearNavigation.Destination is on the navigation backstack.
+ */
+@Deprecated(
+    "This overload is provided for backwards compatibility. " +
+        "A newer overload is available with an additional userSwipeEnabled param.",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun SwipeDismissableNavHost(
+    navController: NavHostController,
+    graph: NavGraph,
+    modifier: Modifier = Modifier,
+    state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(),
+) = SwipeDismissableNavHost(
+    navController = navController,
+    graph = graph,
+    modifier = modifier,
+    userSwipeEnabled = true,
+    state = state
+)
+
+/**
  * State for [SwipeDismissableNavHost]
  *
  * @param swipeToDismissBoxState State for [SwipeToDismissBox], which is used to support the
@@ -247,7 +354,16 @@
  */
 public class SwipeDismissableNavHostState(
     internal val swipeToDismissBoxState: SwipeToDismissBoxState
-)
+) {
+
+    @Deprecated(
+        "This overload is provided for backward compatibility. " +
+            "A newer overload is available which uses SwipeToDismissBoxState " +
+            "from androidx.wear.compose.foundation package."
+    )
+    constructor(swipeToDismissBoxState: androidx.wear.compose.material.SwipeToDismissBoxState) :
+        this(swipeToDismissBoxState.foundationState)
+}
 
 /**
  * Create a [SwipeToDismissBoxState] and remember it.
@@ -258,7 +374,23 @@
  */
 @Composable
 public fun rememberSwipeDismissableNavHostState(
-    swipeToDismissBoxState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(),
+    swipeToDismissBoxState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState()
+): SwipeDismissableNavHostState {
+    return remember(swipeToDismissBoxState) {
+        SwipeDismissableNavHostState(swipeToDismissBoxState)
+    }
+}
+
+@Suppress("DEPRECATION")
+@Deprecated(
+    "This overload is provided for backward compatibility. A newer overload is available " +
+        "which uses SwipeToDismissBoxState from androidx.wear.compose.foundation package.",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+public fun rememberSwipeDismissableNavHostState(
+    swipeToDismissBoxState: androidx.wear.compose.material.SwipeToDismissBoxState =
+        androidx.wear.compose.material.rememberSwipeToDismissBoxState()
 ): SwipeDismissableNavHostState {
     return remember(swipeToDismissBoxState) {
         SwipeDismissableNavHostState(swipeToDismissBoxState)
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index fa0c223..36e7808d 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
         applicationId "androidx.wear.compose.integration.demos"
         minSdk 25
         targetSdk 30
-        versionCode 15
-        versionName "1.15"
+        versionCode 16
+        versionName "1.16"
         // Change the APK name to match the *testapp regex we use to pick up APKs for testing as
         // part of CI.
         archivesBaseName = "wear-compose-demos-testapp"
diff --git a/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Demo.kt b/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Demo.kt
index 56fa2c7..137125f 100644
--- a/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Demo.kt
+++ b/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Demo.kt
@@ -25,7 +25,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.wear.compose.material.SwipeToDismissBoxState
+import androidx.wear.compose.foundation.SwipeToDismissBoxState
 import kotlin.reflect.KClass
 
 /**
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index 38cfda1..02fcfac 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -43,6 +43,10 @@
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.SwipeToDismissBox
+import androidx.wear.compose.foundation.SwipeToDismissBoxState
+import androidx.wear.compose.foundation.SwipeToDismissKeys
+import androidx.wear.compose.foundation.SwipeToDismissValue
 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
@@ -50,6 +54,7 @@
 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
 import androidx.wear.compose.foundation.lazy.ScalingParams
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.integration.demos.common.ActivityDemo
 import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.integration.demos.common.Demo
@@ -60,12 +65,7 @@
 import androidx.wear.compose.material.ListHeader
 import androidx.wear.compose.material.LocalTextStyle
 import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.SwipeToDismissBox
-import androidx.wear.compose.material.SwipeToDismissBoxState
-import androidx.wear.compose.material.SwipeToDismissKeys
-import androidx.wear.compose.material.SwipeToDismissValue
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.rememberSwipeToDismissBoxState
 import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.delay
@@ -93,7 +93,7 @@
 ) {
     SwipeToDismissBox(
         state = state,
-        hasBackground = parentDemo != null,
+        userSwipeEnabled = parentDemo != null,
         backgroundKey = parentDemo?.title ?: SwipeToDismissKeys.Background,
         contentKey = currentDemo.title,
     ) { isBackground ->
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 730a272..5aff765 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
@@ -16,6 +16,8 @@
 
 package androidx.wear.compose.integration.demos
 
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.wear.compose.foundation.samples.CurvedAndNormalText
 import androidx.wear.compose.foundation.samples.CurvedBackground
 import androidx.wear.compose.foundation.samples.CurvedBottomLayout
@@ -24,6 +26,7 @@
 import androidx.wear.compose.foundation.samples.CurvedFonts
 import androidx.wear.compose.foundation.samples.CurvedRowAndColumn
 import androidx.wear.compose.foundation.samples.CurvedWeight
+import androidx.wear.compose.foundation.samples.EdgeSwipeForSwipeToDismiss
 import androidx.wear.compose.foundation.samples.ExpandableTextSample
 import androidx.wear.compose.foundation.samples.ExpandableWithItemsSample
 import androidx.wear.compose.foundation.samples.HierarchicalFocusCoordinatorSample
@@ -33,10 +36,51 @@
 import androidx.wear.compose.foundation.samples.SimpleScalingLazyColumn
 import androidx.wear.compose.foundation.samples.SimpleScalingLazyColumnWithContentPadding
 import androidx.wear.compose.foundation.samples.SimpleScalingLazyColumnWithSnap
+import androidx.wear.compose.foundation.samples.SimpleSwipeToDismissBox
+import androidx.wear.compose.foundation.samples.StatefulSwipeToDismissBox
 import androidx.wear.compose.foundation.samples.SwipeToRevealWithExpandables
 import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.integration.demos.common.DemoCategory
 
+// Declare the swipe to dismiss demos so that we can use this variable as the background composable
+// for the SwipeToDismissDemo itself.
+internal val SwipeToDismissDemos =
+    DemoCategory(
+        "Swipe to Dismiss",
+        listOf(
+            DemoCategory(
+                "Samples",
+                listOf(
+                    ComposableDemo("Simple") { params ->
+                        SimpleSwipeToDismissBox(params.navigateBack)
+                    },
+                    ComposableDemo("Stateful") { StatefulSwipeToDismissBox() },
+                    ComposableDemo("Edge swipe") { params ->
+                        EdgeSwipeForSwipeToDismiss(params.navigateBack)
+                    },
+                )
+            ),
+            DemoCategory(
+                "Demos",
+                listOf(
+                    ComposableDemo("Demo") { params ->
+                        val state = remember { mutableStateOf(SwipeDismissDemoState.List) }
+                        SwipeToDismissDemo(navigateBack = params.navigateBack, demoState = state)
+                    },
+                    ComposableDemo("Stateful Demo") { params ->
+                        SwipeToDismissBoxWithState(params.navigateBack)
+                    },
+                    ComposableDemo("EdgeSwipeToDismiss modifier") { params ->
+                        EdgeSwipeDemo(params.swipeToDismissBoxState)
+                    },
+                    ComposableDemo("Nested SwipeToDismissBox") {
+                        NestedSwipeToDismissDemo()
+                    }
+                )
+            )
+        )
+    )
+
 val WearFoundationDemos = DemoCategory(
     "Foundation",
     listOf(
@@ -101,6 +145,7 @@
                 },
             ),
         ),
+        SwipeToDismissDemos,
         DemoCategory(
             "Swipe To Reveal",
             listOf(
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index 9ae9fcc..5562839 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -52,7 +52,6 @@
 import androidx.wear.compose.material.samples.ConfirmationWithAnimation
 import androidx.wear.compose.material.samples.CurvedTextDemo
 import androidx.wear.compose.material.samples.CurvedTextProviderDemo
-import androidx.wear.compose.material.samples.EdgeSwipeForSwipeToDismiss
 import androidx.wear.compose.material.samples.FixedFontSize
 import androidx.wear.compose.material.samples.HorizontalPageIndicatorSample
 import androidx.wear.compose.material.samples.IndeterminateCircularProgressIndicator
@@ -71,9 +70,7 @@
 import androidx.wear.compose.material.samples.SimpleScalingLazyColumn
 import androidx.wear.compose.material.samples.SimpleScalingLazyColumnWithContentPadding
 import androidx.wear.compose.material.samples.SimpleScalingLazyColumnWithSnap
-import androidx.wear.compose.material.samples.SimpleSwipeToDismissBox
 import androidx.wear.compose.material.samples.SplitToggleChipWithCheckbox
-import androidx.wear.compose.material.samples.StatefulSwipeToDismissBox
 import androidx.wear.compose.material.samples.StepperSample
 import androidx.wear.compose.material.samples.StepperWithCustomSemanticsSample
 import androidx.wear.compose.material.samples.StepperWithIntegerSample
@@ -90,45 +87,6 @@
 import java.time.LocalDate
 import java.time.LocalTime
 
-// Declare the swipe to dismiss demos so that we can use this variable as the background composable
-// for the SwipeToDismissDemo itself.
-internal val SwipeToDismissDemos =
-    DemoCategory(
-        "Swipe to Dismiss",
-        listOf(
-            DemoCategory(
-                "Samples",
-                listOf(
-                    ComposableDemo("Simple") { params ->
-                        SimpleSwipeToDismissBox(params.navigateBack)
-                    },
-                    ComposableDemo("Stateful") { StatefulSwipeToDismissBox() },
-                    ComposableDemo("Edge swipe") { params ->
-                        EdgeSwipeForSwipeToDismiss(params.navigateBack)
-                    },
-                )
-            ),
-            DemoCategory(
-                "Demos",
-                listOf(
-                    ComposableDemo("Demo") { params ->
-                        val state = remember { mutableStateOf(SwipeDismissDemoState.List) }
-                        SwipeToDismissDemo(navigateBack = params.navigateBack, demoState = state)
-                    },
-                    ComposableDemo("Stateful Demo") { params ->
-                        SwipeToDismissBoxWithState(params.navigateBack)
-                    },
-                    ComposableDemo("EdgeSwipeToDismiss modifier") { params ->
-                        EdgeSwipeDemo(params.swipeToDismissBoxState)
-                    },
-                    ComposableDemo("Nested SwipeToDismissBox") {
-                        NestedSwipeToDismissDemo()
-                    }
-                )
-            )
-        )
-    )
-
 @SuppressLint("ClassVerificationFailure")
 val WearMaterialDemos = DemoCategory(
     "Material",
@@ -598,7 +556,6 @@
                 )
             )
         ),
-        SwipeToDismissDemos,
         DemoCategory(
             "List (Scaling Lazy Column)",
             listOf(
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemo.kt
index 0dd106a..60e878f 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemo.kt
@@ -39,15 +39,15 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.SwipeToDismissBox
+import androidx.wear.compose.foundation.SwipeToDismissBoxState
+import androidx.wear.compose.foundation.SwipeToDismissKeys
+import androidx.wear.compose.foundation.SwipeToDismissValue
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
-import androidx.wear.compose.material.SwipeToDismissBox
-import androidx.wear.compose.material.SwipeToDismissBoxState
-import androidx.wear.compose.material.SwipeToDismissKeys
-import androidx.wear.compose.material.SwipeToDismissValue
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.edgeSwipeToDismiss
-import androidx.wear.compose.material.rememberSwipeToDismissBoxState
 
 /**
  * SwipeToDismiss demo - manages its own navigation between a List screen and a Detail screen,
@@ -143,7 +143,7 @@
         state = state,
         backgroundKey = previous ?: SwipeToDismissKeys.Background,
         contentKey = current,
-        hasBackground = previous != null,
+        userSwipeEnabled = previous != null,
         onDismissed = { items.removeLastOrNull() }
     ) { isBackground ->
         val item = if (isBackground) {
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemoWithState.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemoWithState.kt
index 2e61b3f..9118854 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemoWithState.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemoWithState.kt
@@ -31,13 +31,13 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.wear.compose.foundation.SwipeToDismissBox
+import androidx.wear.compose.foundation.SwipeToDismissValue
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.SwipeToDismissBox
-import androidx.wear.compose.material.SwipeToDismissValue
 import androidx.wear.compose.material.Text
 import androidx.wear.compose.material.ToggleButton
-import androidx.wear.compose.material.rememberSwipeToDismissBoxState
 
 @Composable
 fun SwipeToDismissBoxWithState(
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/SwipeActivity.kt b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/SwipeActivity.kt
index 26990fc..4282144 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/SwipeActivity.kt
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/SwipeActivity.kt
@@ -29,10 +29,10 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
-import androidx.wear.compose.material.SwipeToDismissBox
-import androidx.wear.compose.material.SwipeToDismissValue
+import androidx.wear.compose.foundation.SwipeToDismissBox
+import androidx.wear.compose.foundation.SwipeToDismissValue
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.rememberSwipeToDismissBoxState
 
 class SwipeActivity : ComponentActivity() {
 
diff --git a/wear/compose/integration-tests/navigation/src/main/java/androidx/wear/compose/integration/navigation/MainActivity.kt b/wear/compose/integration-tests/navigation/src/main/java/androidx/wear/compose/integration/navigation/MainActivity.kt
index c250fe8..d8c4163 100644
--- a/wear/compose/integration-tests/navigation/src/main/java/androidx/wear/compose/integration/navigation/MainActivity.kt
+++ b/wear/compose/integration-tests/navigation/src/main/java/androidx/wear/compose/integration/navigation/MainActivity.kt
@@ -31,11 +31,11 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.edgeSwipeToDismiss
+import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.material.CompactChip
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.Text
-import androidx.wear.compose.material.edgeSwipeToDismiss
-import androidx.wear.compose.material.rememberSwipeToDismissBoxState
 import androidx.wear.compose.navigation.SwipeDismissableNavHost
 import androidx.wear.compose.navigation.composable
 import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
diff --git a/wear/watchface/watchface-complications-data-source-samples/build.gradle b/wear/watchface/watchface-complications-data-source-samples/build.gradle
index 0ff1cd5..e374bb3 100644
--- a/wear/watchface/watchface-complications-data-source-samples/build.gradle
+++ b/wear/watchface/watchface-complications-data-source-samples/build.gradle
@@ -21,13 +21,11 @@
 }
 
 dependencies {
-    implementation("androidx.core:core:1.1.0")
     api(project(":wear:watchface:watchface-complications-data-source"))
     api(libs.guavaAndroid)
     api(libs.kotlinStdlib)
-    // TODO(b/267170061): Use released protolayout-expression, remove protolayout-proto.
-    implementation(project(":wear:protolayout:protolayout-expression"))
-    implementation(project(":wear:protolayout:protolayout-proto"))
+    implementation("androidx.core:core:1.1.0")
+    implementation("androidx.wear.protolayout:protolayout-expression:1.0.0-beta01")
 }
 
 android {
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index f4d254b..b8293ff 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -28,17 +28,14 @@
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
     api("androidx.versionedparcelable:versionedparcelable:1.1.0")
+    api("androidx.wear.protolayout:protolayout-expression:1.0.0-beta01")
     api(libs.kotlinStdlib)
     api(libs.kotlinCoroutinesAndroid)
 
-    // TODO(b/267170061): Use released protolayout-expression, remove protolayout-proto.
-    api(project(":wear:protolayout:protolayout-expression"))
-    api(project(":wear:protolayout:protolayout-expression-pipeline"))
-    implementation(project(":wear:protolayout:protolayout-proto"))
-
+    implementation("androidx.annotation:annotation:1.2.0")
     implementation("androidx.core:core:1.1.0")
     implementation("androidx.preference:preference:1.1.0")
-    implementation("androidx.annotation:annotation:1.2.0")
+    implementation("androidx.wear.protolayout:protolayout-expression-pipeline:1.0.0-beta01")
     testImplementation(libs.testCore)
     testImplementation(libs.testRunner)
     testImplementation(libs.testRules)