Merge "Explicitly release ImageWriter resources when CaptureSessionState is shutdown" into androidx-main
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
index a8e1bee..169e92a2 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
@@ -38,6 +38,7 @@
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import kotlin.coroutines.cancellation.CancellationException
 import kotlinx.coroutines.delay
@@ -173,12 +174,14 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 34) // Below API 34 startGestureBack triggers back
     fun testPredictiveBackHandlerDisabledBeforeStart() {
         val result = mutableListOf<String>()
         var count by mutableStateOf(2)
         lateinit var dispatcherOwner: TestOnBackPressedDispatcherOwner
         lateinit var dispatcher: OnBackPressedDispatcher
         var started = false
+        var cancelled = false
 
         rule.setContent {
             dispatcherOwner =
@@ -188,8 +191,12 @@
                     if (count <= 1) {
                         started = true
                     }
-                    progress.collect()
-                    result += "onBack"
+                    try {
+                        progress.collect()
+                        result += "onBack"
+                    } catch (e: CancellationException) {
+                        cancelled = true
+                    }
                 }
                 dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
             }
@@ -200,9 +207,12 @@
         count = 1
         dispatcher.startGestureBack()
 
+        // In a test, we don't get the launched effect fast enough to prevent starting
+        // but since we idle here, we can cancel the callback channel and keep from completing
         rule.runOnIdle { assertThat(started).isTrue() }
         dispatcher.api34Complete()
-        rule.runOnIdle { assertThat(result).isEqualTo(listOf("onBack")) }
+        rule.runOnIdle { assertThat(result).isEmpty() }
+        rule.runOnIdle { assertThat(cancelled).isTrue() }
     }
 
     fun testPredictiveBackHandlerDisabledAfterStart() {
diff --git a/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt b/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt
index 3bdbb96..b35c8bf 100644
--- a/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt
+++ b/activity/activity-compose/src/main/java/androidx/activity/compose/PredictiveBackHandler.kt
@@ -76,63 +76,19 @@
     // ensure we don't re-register callbacks when onBack changes
     val currentOnBack by rememberUpdatedState(onBack)
     val onBackScope = rememberCoroutineScope()
-    var onBackInstance: OnBackInstance? = null
 
     val backCallBack = remember {
-        object : OnBackPressedCallback(enabled) {
-
-            override fun handleOnBackStarted(backEvent: BackEventCompat) {
-                super.handleOnBackStarted(backEvent)
-                // in case the previous onBackInstance was started by a normal back gesture
-                // we want to make sure it's still cancelled before we start a predictive
-                // back gesture
-                onBackInstance?.cancel()
-                onBackInstance = OnBackInstance(onBackScope, true, currentOnBack)
-            }
-
-            override fun handleOnBackProgressed(backEvent: BackEventCompat) {
-                super.handleOnBackProgressed(backEvent)
-                onBackInstance?.send(backEvent)
-            }
-
-            override fun handleOnBackPressed() {
-                // handleOnBackPressed could be called by regular back to restart
-                // a new back instance. If this is the case (where current back instance
-                // was NOT started by handleOnBackStarted) then we need to reset the previous
-                // regular back.
-                onBackInstance?.apply {
-                    if (!isPredictiveBack) {
-                        cancel()
-                        onBackInstance = null
-                    }
-                }
-                if (onBackInstance == null) {
-                    onBackInstance = OnBackInstance(onBackScope, false, currentOnBack)
-                }
-
-                // finally, we close the channel to ensure no more events can be sent
-                // but let the job complete normally
-                onBackInstance?.close()
-                onBackInstance?.isPredictiveBack = false
-            }
-
-            override fun handleOnBackCancelled() {
-                super.handleOnBackCancelled()
-                // cancel will purge the channel of any sent events that are yet to be received
-                onBackInstance?.cancel()
-                onBackInstance?.isPredictiveBack = false
-            }
-        }
+        PredictiveBackHandlerCallback(enabled, onBackScope, currentOnBack)
     }
 
-    LaunchedEffect(enabled) {
-        backCallBack.isEnabled = enabled
-        if (!enabled) {
-            onBackInstance?.close()
-            onBackInstance = null
-        }
+    // we want to use the same callback, but ensure we adjust the variable on recomposition
+    remember(currentOnBack, onBackScope) {
+        backCallBack.currentOnBack = currentOnBack
+        backCallBack.onBackScope = onBackScope
     }
 
+    LaunchedEffect(enabled) { backCallBack.setIsEnabled(enabled) }
+
     val backDispatcher =
         checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
                 "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
@@ -153,13 +109,16 @@
     scope: CoroutineScope,
     var isPredictiveBack: Boolean,
     onBack: suspend (progress: Flow<BackEventCompat>) -> Unit,
+    callback: OnBackPressedCallback
 ) {
     val channel = Channel<BackEventCompat>(capacity = BUFFERED, onBufferOverflow = SUSPEND)
     val job =
         scope.launch {
-            var completed = false
-            onBack(channel.consumeAsFlow().onCompletion { completed = true })
-            check(completed) { "You must collect the progress flow" }
+            if (callback.isEnabled) {
+                var completed = false
+                onBack(channel.consumeAsFlow().onCompletion { completed = true })
+                check(completed) { "You must collect the progress flow" }
+            }
         }
 
     fun send(backEvent: BackEventCompat) = channel.trySend(backEvent)
@@ -172,3 +131,63 @@
         job.cancel()
     }
 }
+
+private class PredictiveBackHandlerCallback(
+    enabled: Boolean,
+    var onBackScope: CoroutineScope,
+    var currentOnBack: suspend (progress: Flow<BackEventCompat>) -> Unit,
+) : OnBackPressedCallback(enabled) {
+    private var onBackInstance: OnBackInstance? = null
+
+    fun setIsEnabled(enabled: Boolean) {
+        // We are disabling a callback that was enabled.
+        if (!enabled && isEnabled) {
+            onBackInstance?.cancel()
+        }
+        isEnabled = enabled
+    }
+
+    override fun handleOnBackStarted(backEvent: BackEventCompat) {
+        super.handleOnBackStarted(backEvent)
+        // in case the previous onBackInstance was started by a normal back gesture
+        // we want to make sure it's still cancelled before we start a predictive
+        // back gesture
+        onBackInstance?.cancel()
+        if (isEnabled) {
+            onBackInstance = OnBackInstance(onBackScope, true, currentOnBack, this)
+        }
+    }
+
+    override fun handleOnBackProgressed(backEvent: BackEventCompat) {
+        super.handleOnBackProgressed(backEvent)
+        onBackInstance?.send(backEvent)
+    }
+
+    override fun handleOnBackPressed() {
+        // handleOnBackPressed could be called by regular back to restart
+        // a new back instance. If this is the case (where current back instance
+        // was NOT started by handleOnBackStarted) then we need to reset the previous
+        // regular back.
+        onBackInstance?.apply {
+            if (!isPredictiveBack) {
+                cancel()
+                onBackInstance = null
+            }
+        }
+        if (onBackInstance == null) {
+            onBackInstance = OnBackInstance(onBackScope, false, currentOnBack, this)
+        }
+
+        // finally, we close the channel to ensure no more events can be sent
+        // but let the job complete normally
+        onBackInstance?.close()
+        onBackInstance?.isPredictiveBack = false
+    }
+
+    override fun handleOnBackCancelled() {
+        super.handleOnBackCancelled()
+        // cancel will purge the channel of any sent events that are yet to be received
+        onBackInstance?.cancel()
+        onBackInstance?.isPredictiveBack = false
+    }
+}
diff --git a/benchmark/benchmark-common/src/androidTest/assets/macro-legacy-trivialscrollbench.json b/benchmark/benchmark-common/src/androidTest/assets/macro-legacy-trivialscrollbench.json
index 9f828f4..fd4ad97 100644
--- a/benchmark/benchmark-common/src/androidTest/assets/macro-legacy-trivialscrollbench.json
+++ b/benchmark/benchmark-common/src/androidTest/assets/macro-legacy-trivialscrollbench.json
@@ -36,6 +36,7 @@
                     "minimum": 65.0,
                     "maximum": 65.0,
                     "median": 65.0,
+                    "coefficientOfVariation": 0.0,
                     "runs": [
                         65.0,
                         65.0
@@ -45,6 +46,7 @@
                     "minimum": 474.123432,
                     "maximum": 481.377279,
                     "median": 477.75035549999996,
+                    "coefficientOfVariation": 0.01073624403,
                     "runs": [
                         474.123432,
                         481.377279
@@ -69,6 +71,7 @@
                     "minimum": 78.0,
                     "maximum": 79.0,
                     "median": 78.5,
+                    "coefficientOfVariation": 0.009007729697,
                     "runs": [
                         78.0,
                         79.0
@@ -78,6 +81,7 @@
                     "minimum": 490.891433,
                     "maximum": 497.585203,
                     "median": 494.238318,
+                    "coefficientOfVariation": 0.009576777005,
                     "runs": [
                         490.891433,
                         497.585203
diff --git a/benchmark/benchmark-common/src/androidTest/assets/micro-legacy-trivialbench.json b/benchmark/benchmark-common/src/androidTest/assets/micro-legacy-trivialbench.json
index db96d0f..2ffda20 100644
--- a/benchmark/benchmark-common/src/androidTest/assets/micro-legacy-trivialbench.json
+++ b/benchmark/benchmark-common/src/androidTest/assets/micro-legacy-trivialbench.json
@@ -32,6 +32,7 @@
                     "minimum": 5.52001617001617,
                     "maximum": 5.658262878262878,
                     "median": 5.542584892584893,
+                    "coefficientOfVariation": 0.006590340481,
                     "runs": [
                         5.563728343728344,
                         5.546317856317856,
@@ -89,6 +90,7 @@
                     "minimum": 0.0,
                     "maximum": 0.0,
                     "median": 0.0,
+                    "coefficientOfVariation": 0.0,
                     "runs": [
                         0.0,
                         0.0,
@@ -113,6 +115,7 @@
                     "minimum": 5.52031108031108,
                     "maximum": 5.632142912142912,
                     "median": 5.548982828982829,
+                    "coefficientOfVariation": 0.0,
                     "runs": [
                         5.553719873719873,
                         5.520551320551321,
@@ -170,6 +173,7 @@
                     "minimum": 0.0,
                     "maximum": 0.0,
                     "median": 0.0,
+                    "coefficientOfVariation": 0.0,
                     "runs": [
                         0.0,
                         0.0,
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
index bbfcf86..be93faa 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
@@ -35,12 +35,27 @@
     }
 
     @Test
+    fun zeros() {
+        val metricResult = MetricResult("test", listOf(0.0, 0.0))
+        assertEquals(0.0, metricResult.min, 0.0)
+        assertEquals(0.0, metricResult.max, 0.0)
+        assertEquals(0.0, metricResult.median, 0.0)
+        assertEquals(0.0, metricResult.standardDeviation, 0.0)
+        assertEquals(0.0, metricResult.coefficientOfVariation, 0.0)
+
+        assertEquals(0, metricResult.minIndex)
+        assertEquals(0, metricResult.maxIndex)
+        assertEquals(1, metricResult.medianIndex)
+    }
+
+    @Test
     fun repeat() {
         val metricResult = MetricResult("test", listOf(10.0, 10.0, 10.0, 10.0))
         assertEquals(10.0, metricResult.min, 0.0)
         assertEquals(10.0, metricResult.max, 0.0)
         assertEquals(10.0, metricResult.median, 0.0)
         assertEquals(0.0, metricResult.standardDeviation, 0.0)
+        assertEquals(0.0, metricResult.coefficientOfVariation, 0.0)
 
         assertEquals(0, metricResult.minIndex)
         assertEquals(0, metricResult.maxIndex)
@@ -54,6 +69,7 @@
         assertEquals(10.0, metricResult.max, 0.0)
         assertEquals(10.0, metricResult.median, 0.0)
         assertEquals(0.0, metricResult.standardDeviation, 0.0)
+        assertEquals(0.0, metricResult.coefficientOfVariation, 0.0)
 
         assertEquals(0, metricResult.minIndex)
         assertEquals(0, metricResult.maxIndex)
@@ -67,6 +83,7 @@
         assertEquals(100.0, metricResult.max, 0.0)
         assertEquals(0.0, metricResult.min, 0.0)
         assertEquals(29.3, metricResult.standardDeviation, 0.05)
+        assertEquals(0.586, metricResult.coefficientOfVariation, 0.0005)
 
         assertEquals(0, metricResult.minIndex)
         assertEquals(100, metricResult.maxIndex)
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
index 638a21c..0f05072 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
@@ -127,6 +127,7 @@
                                 "minimum": 100.0,
                                 "maximum": 102.0,
                                 "median": 101.0,
+                                "coefficientOfVariation": 0.009900990099009901,
                                 "runs": [
                                     100.0,
                                     101.0,
@@ -149,6 +150,7 @@
                                 "minimum": 100.0,
                                 "maximum": 102.0,
                                 "median": 101.0,
+                                "coefficientOfVariation": 0.009900990099009901,
                                 "runs": [
                                     100.0,
                                     101.0,
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
index 3fd1e36..d4b026d 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
@@ -38,6 +38,7 @@
     val max: Double
     val maxIndex: Int
     val standardDeviation: Double
+    val coefficientOfVariation: Double
 
     val p50: Double
     val p90: Double
@@ -70,6 +71,12 @@
                 val sum = values.map { (it - mean).pow(2) }.sum()
                 sqrt(sum / (size - 1).toDouble())
             }
+        coefficientOfVariation =
+            if (mean == 0.0) {
+                0.0
+            } else {
+                standardDeviation / mean
+            }
     }
 
     internal fun getSummary(): String {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/json/BenchmarkData.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/json/BenchmarkData.kt
index 2d280cce..99dea1b 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/json/BenchmarkData.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/json/BenchmarkData.kt
@@ -225,6 +225,7 @@
             val minimum: Double,
             val maximum: Double,
             val median: Double,
+            val coefficientOfVariation: Double,
             val runs: List<Double>
         ) : MetricResult() {
             constructor(
@@ -233,6 +234,7 @@
                 minimum = metricResult.min,
                 maximum = metricResult.max,
                 median = metricResult.median,
+                coefficientOfVariation = metricResult.coefficientOfVariation,
                 runs = metricResult.data
             )
         }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
index f3f9ffa..736ae72 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
@@ -32,7 +32,7 @@
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.config.Camera2ControllerScope
 import androidx.camera.camera2.pipe.core.Log
-import androidx.camera.camera2.pipe.core.Threading.runBlockingWithTimeoutOrNull
+import androidx.camera.camera2.pipe.core.Threading.runBlockingCheckedOrNull
 import androidx.camera.camera2.pipe.core.Threads
 import androidx.camera.camera2.pipe.core.TimeSource
 import androidx.camera.camera2.pipe.graph.GraphListener
@@ -320,7 +320,7 @@
             // getting blocked for too long.
             //
             // [1] b/307594946 - [ANR] at Camera2CameraController.disconnectSessionAndCamera
-            runBlockingWithTimeoutOrNull(threads.backgroundDispatcher, DISCONNECT_TIMEOUT_MS) {
+            runBlockingCheckedOrNull(threads.backgroundDispatcher, DISCONNECT_TIMEOUT_MS) {
                 deferred.await()
             }
                 ?: run {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
index 54f4524..a6081aa 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
@@ -87,6 +87,7 @@
         closeUnderError: Boolean,
         androidCameraState: AndroidCameraState,
     ) {
+        Log.debug { "$this#closeCameraDevice($cameraDevice)" }
         val cameraId = CameraId.fromCamera2Id(cameraDevice.id)
         if (camera2Quirks.shouldCreateCaptureSessionBeforeClosing(cameraId) && !closeUnderError) {
             Debug.trace("Camera2DeviceCloserImpl#createCaptureSession") {
@@ -95,7 +96,7 @@
                 Log.debug { "Empty capture session quirk completed" }
             }
         }
-        Threading.runBlockingWithTimeout(threads.backgroundDispatcher, 5000L) {
+        Threading.runBlockingChecked(threads.backgroundDispatcher, 5000L) {
             cameraDevice.closeWithTrace()
         }
         if (camera2Quirks.shouldWaitForCameraDeviceOnClosed(cameraId)) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Threading.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Threading.kt
index 1496985..d25b18a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Threading.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Threading.kt
@@ -21,6 +21,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.async
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withTimeout
@@ -31,20 +32,26 @@
         CoroutineScope(CoroutineName("GlobalThreadingScope") + SupervisorJob())
 
     /**
-     * runBlockingWithTime runs the specified [block] on a timeout of [timeoutMs] using the given
+     * runBlockingChecked runs the specified [block] on a timeout of [timeoutMs] using the given
      * [dispatcher]. The function runs the given block asynchronously on a supervised scope,
      * allowing it to return after the timeout completes, even if the calling thread is blocked.
-     * Throws [kotlinx.coroutines.TimeoutCancellationException] when the execution of the [block]
-     * times out.
+     * Throws [IllegalStateException] when the execution of the [block] times out.
      */
-    fun <T> runBlockingWithTimeout(
+    fun <T> runBlockingChecked(
         dispatcher: CoroutineDispatcher,
         timeoutMs: Long,
         block: suspend () -> T
-    ): T? {
+    ): T {
         return runBlocking {
             val result = runAsyncSupervised(dispatcher, block)
-            withTimeout(timeoutMs) { result.await() }
+            try {
+                withTimeout(timeoutMs) { result.await() }
+            } catch (e: TimeoutCancellationException) {
+                Log.error { "Timed out after ${timeoutMs}ms!" }
+                // For some reason, if TimeoutCancellationException is thrown, runBlocking can
+                // suspend indefinitely. Catch it and rethrow IllegalStateException.
+                throw IllegalStateException("Timed out after ${timeoutMs}ms!")
+            }
         }
     }
 
@@ -54,7 +61,7 @@
      * allowing it to return after the timeout completes, even if the calling thread is blocked.
      * Returns null when the execution of the [block] times out.
      */
-    fun <T> runBlockingWithTimeoutOrNull(
+    fun <T> runBlockingCheckedOrNull(
         dispatcher: CoroutineDispatcher,
         timeoutMs: Long,
         block: suspend () -> T
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/ThreadingTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/ThreadingTest.kt
index 3fefe2f..9eb385b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/ThreadingTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/core/ThreadingTest.kt
@@ -23,7 +23,6 @@
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 
@@ -31,10 +30,10 @@
 @SdkSuppress(minSdkVersion = 21)
 class ThreadingTest {
     @Test
-    fun runBlockingWithTimeoutThrowsOnTimeout() = runTest {
+    fun runBlockingCheckedThrowsOnTimeout() = runTest {
         val latch = CountDownLatch(1)
-        assertThrows<TimeoutCancellationException> {
-            Threading.runBlockingWithTimeout(Dispatchers.IO, 500L) {
+        assertThrows<IllegalStateException> {
+            Threading.runBlockingChecked(Dispatchers.IO, 500L) {
                 // Simulate a long call that should time out.
                 latch.await(10, TimeUnit.SECONDS)
             }
@@ -42,13 +41,32 @@
     }
 
     @Test
-    fun runBlockingWithTimeoutOrNullReturnsNullOnTimeout() = runTest {
+    fun runBlockingCheckedDoesNotThrowWhenNotTimedOut() = runTest {
+        val latch = CountDownLatch(1)
+        Threading.runBlockingChecked(Dispatchers.IO, 10_000L) {
+            latch.await(500, TimeUnit.MILLISECONDS)
+        }
+    }
+
+    @Test
+    fun runBlockingCheckedOrNullReturnsNullOnTimeout() = runTest {
         val latch = CountDownLatch(1)
         val result =
-            Threading.runBlockingWithTimeoutOrNull(Dispatchers.IO, 500L) {
+            Threading.runBlockingCheckedOrNull(Dispatchers.IO, 500L) {
                 // Simulate a long call that should time out.
                 latch.await(10, TimeUnit.SECONDS)
             }
         assertThat(result).isNull()
     }
+
+    @Test
+    fun runBlockingCheckedOrNullReturnsNonNullWhenNotTimeout() = runTest {
+        val latch = CountDownLatch(1)
+        val result =
+            Threading.runBlockingCheckedOrNull(Dispatchers.IO, 10_000L) {
+                // Simulate a long call that should time out.
+                latch.await(500, TimeUnit.MILLISECONDS)
+            }
+        assertThat(result).isNotNull()
+    }
 }
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 475569e..ce51760 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -339,6 +339,7 @@
     method public void setFlashMode(int);
     method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
     method public void setTargetRotation(int);
+    method public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
     method public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
     method public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
     field public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0; // 0x0
@@ -355,6 +356,8 @@
     field public static final int FLASH_MODE_SCREEN = 3; // 0x3
     field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG = 0; // 0x0
     field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1; // 0x1
+    field public static final int OUTPUT_FORMAT_RAW = 2; // 0x2
+    field public static final int OUTPUT_FORMAT_RAW_JPEG = 3; // 0x3
   }
 
   public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
@@ -414,6 +417,7 @@
   }
 
   public static class ImageCapture.OutputFileResults {
+    method public int getImageFormat();
     method public android.net.Uri? getSavedUri();
   }
 
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 475569e..ce51760 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -339,6 +339,7 @@
     method public void setFlashMode(int);
     method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
     method public void setTargetRotation(int);
+    method public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
     method public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
     method public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
     field public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0; // 0x0
@@ -355,6 +356,8 @@
     field public static final int FLASH_MODE_SCREEN = 3; // 0x3
     field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG = 0; // 0x0
     field @SuppressCompatibility @androidx.camera.core.ExperimentalImageCaptureOutputFormat public static final int OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1; // 0x1
+    field public static final int OUTPUT_FORMAT_RAW = 2; // 0x2
+    field public static final int OUTPUT_FORMAT_RAW_JPEG = 3; // 0x3
   }
 
   public static final class ImageCapture.Builder implements androidx.camera.core.ExtendableBuilder<androidx.camera.core.ImageCapture!> {
@@ -414,6 +417,7 @@
   }
 
   public static class ImageCapture.OutputFileResults {
+    method public int getImageFormat();
     method public android.net.Uri? getSavedUri();
   }
 
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 3fac0e4..97b578e 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
@@ -316,14 +316,12 @@
     /**
      * Captures raw images in the {@link ImageFormat#RAW_SENSOR} image format.
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
     public static final int OUTPUT_FORMAT_RAW = 2;
 
     /**
      * Captures raw images in the {@link ImageFormat#RAW_SENSOR} and {@link ImageFormat#JPEG}
      * image formats.
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
     public static final int OUTPUT_FORMAT_RAW_JPEG = 3;
 
     /**
@@ -905,8 +903,8 @@
      *
      * <p>For simultaneous image capture with {@link #OUTPUT_FORMAT_RAW_JPEG}, the
      * {@link OnImageCapturedCallback#onCaptureSuccess(ImageProxy)} will be triggered twice, one for
-     * {@link ImageFormat#RAW_SENSOR} and the other for {@link ImageFormat#JPEG}. The order in which
-     * image format is triggered first is not guaranteed.
+     * {@link ImageFormat#RAW_SENSOR} and the other for {@link ImageFormat#JPEG}. The order of the
+     * callbacks for which image format is triggered first is not guaranteed.
      *
      * @param executor The executor in which the callback methods will be run.
      * @param callback Callback to be invoked for the newly captured image.
@@ -937,7 +935,8 @@
      * @param imageSavedCallback Callback to be called for the newly captured image.
      *
      * @throws IllegalArgumentException If {@link ImageCapture#FLASH_MODE_SCREEN} is used without a
-     *                                  a non-null {@code ScreenFlash} instance set.
+     *                                  a non-null {@code ScreenFlash} instance set. Also if
+     *                                  {@link ImageCapture#OUTPUT_FORMAT_RAW_JPEG} is used.
      * @see ViewPort
      */
     public void takePicture(
@@ -961,7 +960,9 @@
      * <p>Currently only {@link #OUTPUT_FORMAT_RAW_JPEG} is supporting simultaneous image capture.
      * It needs two {@link OutputFileOptions}, the first one is used for
      * {@link ImageFormat#RAW_SENSOR} image and the second one is for {@link ImageFormat#JPEG}. The
-     * order in which image format is triggered first is not guaranteed.
+     * order of the callbacks for which image format is triggered first is not guaranteed. Check
+     * with {@link OutputFileResults#getImageFormat()} in
+     * {@link OnImageSavedCallback#onImageSaved(OutputFileResults)} for the image format.
      *
      * @param rawOutputFileOptions  Options to store the newly captured raw image.
      * @param jpegOutputFileOptions Options to store the newly captured jpeg image.
@@ -969,9 +970,10 @@
      * @param imageSavedCallback Callback to be called for the newly captured image.
      *
      * @throws IllegalArgumentException If {@link ImageCapture#FLASH_MODE_SCREEN} is used without a
-     *                                  a non-null {@code ScreenFlash} instance set.
+     *                                  a non-null {@code ScreenFlash} instance set. Also if
+     *                                  non-{@link ImageCapture#OUTPUT_FORMAT_RAW_JPEG} format is
+     *                                  used.
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
     public void takePicture(
             final @NonNull OutputFileOptions rawOutputFileOptions,
             final @NonNull OutputFileOptions jpegOutputFileOptions,
@@ -1476,6 +1478,16 @@
             sendInvalidCameraError(executor, inMemoryCallback, onDiskCallback);
             return;
         }
+        boolean isSimultaneousCapture = getCurrentConfig()
+                .getSecondaryInputFormat() != ImageFormat.UNKNOWN;
+        if (isSimultaneousCapture && secondaryOutputFileOptions == null) {
+            throw new IllegalArgumentException(
+                    "Simultaneous capture RAW and JPEG needs two output file options");
+        }
+        if (!isSimultaneousCapture && secondaryOutputFileOptions != null) {
+            throw new IllegalArgumentException(
+                    "Non simultaneous capture cannot have two output file options");
+        }
         requireNonNull(mTakePictureManager).offerRequest(TakePictureRequest.of(
                 executor,
                 inMemoryCallback,
@@ -1487,7 +1499,7 @@
                 getRelativeRotation(camera),
                 getJpegQualityInternal(),
                 getCaptureMode(),
-                getCurrentConfig().getSecondaryInputFormat() != ImageFormat.UNKNOWN,
+                isSimultaneousCapture,
                 mSessionConfigBuilder.getSingleCameraCaptureCallbacks()));
     }
 
@@ -2227,7 +2239,6 @@
         /**
          * Returns the {@link ImageFormat} of the saved file.
          */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         public int getImageFormat() {
             return mImageFormat;
         }
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 8609e1b..69e5e0e 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -976,7 +976,6 @@
         }
     }
 
-    @SuppressLint("RestrictedApiAndroidX")
     private void setUpTakePictureButton() {
         mTakePicture.setOnClickListener(
                 new View.OnClickListener() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
index a3b1549..e02b5cf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
@@ -45,6 +45,8 @@
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.clipRect
 import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
 import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.LayoutCoordinates
@@ -55,11 +57,13 @@
 import androidx.compose.ui.node.LayoutModifierNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.requireDensity
+import androidx.compose.ui.node.requireGraphicsContext
 import androidx.compose.ui.node.requireLayoutDirection
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
@@ -208,6 +212,7 @@
     private var containerWidth by mutableIntStateOf(0)
     private var hasFocus by mutableStateOf(false)
     private var animationJob: Job? = null
+    private var marqueeLayer: GraphicsLayer? = null
     var spacing: MarqueeSpacing by mutableStateOf(spacing)
     var animationMode: MarqueeAnimationMode by mutableStateOf(animationMode)
 
@@ -225,12 +230,27 @@
     }
 
     override fun onAttach() {
+        val layer = marqueeLayer
+        val graphicsContext = requireGraphicsContext()
+        // Shouldn't happen as detach should be called in between in onAttach call but
+        // just in case
+        if (layer != null) {
+            graphicsContext.releaseGraphicsLayer(layer)
+        }
+
+        marqueeLayer = graphicsContext.createGraphicsLayer()
         restartAnimation()
     }
 
     override fun onDetach() {
         animationJob?.cancel()
         animationJob = null
+
+        val layer = marqueeLayer
+        if (layer != null) {
+            requireGraphicsContext().releaseGraphicsLayer(layer)
+            marqueeLayer = null
+        }
     }
 
     fun update(
@@ -320,17 +340,31 @@
                 else -> -contentWidth - spacingPx
             }.toFloat()
 
-        clipRect(left = clipOffset, right = clipOffset + containerWidth) {
-            // TODO(b/262284225) When both copies are visible, we call drawContent twice. This is
-            //  generally a bad practice, however currently the only alternative is to compose the
-            //  content twice, which can't be done with a modifier. In the future we might get the
-            //  ability to create intrinsic layers in draw scopes, which we should use here to avoid
-            //  invalidating the contents' draw scopes.
-            if (firstCopyVisible) {
+        val drawHeight = size.height
+        marqueeLayer?.let { layer ->
+            layer.record(size = IntSize(contentWidth, drawHeight.roundToInt())) {
                 [email protected]()
             }
-            if (secondCopyVisible) {
-                translate(left = secondCopyOffset) { [email protected]() }
+        }
+        clipRect(left = clipOffset, right = clipOffset + containerWidth) {
+            val layer = marqueeLayer
+            // Unless there are circumstances where the Modifier's draw call can be invoked without
+            // an attach call, the else case here is optional. However we can be safe and make sure
+            // that we definitely draw even when the layer could not be initialized for any reason.
+            if (layer != null) {
+                if (firstCopyVisible) {
+                    drawLayer(layer)
+                }
+                if (secondCopyVisible) {
+                    translate(left = secondCopyOffset) { drawLayer(layer) }
+                }
+            } else {
+                if (firstCopyVisible) {
+                    [email protected]()
+                }
+                if (secondCopyVisible) {
+                    translate(left = secondCopyOffset) { [email protected]() }
+                }
             }
         }
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index 23a9c30..9a578ca 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -119,7 +119,9 @@
             cachedBucket.clear()
         }
 
-        checkPrecondition(currentLine <= lineIndex) { "currentLine > lineIndex" }
+        checkPrecondition(currentLine <= lineIndex) {
+            "currentLine ($currentLine) > lineIndex ($lineIndex)"
+        }
 
         while (currentLine < lineIndex && currentItemIndex < totalSize) {
             if (cacheThisBucket) {
diff --git a/compose/integration-tests/hero/OWNERS b/compose/integration-tests/hero/OWNERS
index ee8658e..76f9fee 100644
--- a/compose/integration-tests/hero/OWNERS
+++ b/compose/integration-tests/hero/OWNERS
@@ -1,4 +1,5 @@
 # Bug component: 343210
 [email protected]
 [email protected]
[email protected]
\ No newline at end of file
[email protected]
[email protected]
\ No newline at end of file
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/SplitButtonBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/SplitButtonBenchmark.kt
index 9c1865c..7986432 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/SplitButtonBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/SplitButtonBenchmark.kt
@@ -29,7 +29,9 @@
 import androidx.compose.material3.SplitButtonLayout
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkFirstCompose
 import androidx.compose.testutils.benchmark.benchmarkFirstDraw
@@ -87,8 +89,11 @@
     }
 }
 
-internal class SplitButtonTestCase(private val type: SplitButtonType) : LayeredComposeTestCase() {
-    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+internal class SplitButtonTestCase(private val type: SplitButtonType) :
+    LayeredComposeTestCase(), ToggleableTestCase {
+    private var trailingButtonChecked = mutableStateOf(false)
+
     @Composable
     override fun MeasuredContent() {
         when (type) {
@@ -103,7 +108,7 @@
                     },
                     trailingButton = {
                         SplitButtonDefaults.TrailingButton(
-                            checked = false,
+                            checked = trailingButtonChecked.value,
                             onCheckedChange = { /* Do Nothing */ },
                         ) {
                             trailingContent()
@@ -121,7 +126,7 @@
                     },
                     trailingButton = {
                         SplitButtonDefaults.TonalTrailingButton(
-                            checked = false,
+                            checked = trailingButtonChecked.value,
                             onCheckedChange = { /* Do Nothing */ },
                         ) {
                             trailingContent()
@@ -139,7 +144,7 @@
                     },
                     trailingButton = {
                         SplitButtonDefaults.ElevatedTrailingButton(
-                            checked = false,
+                            checked = trailingButtonChecked.value,
                             onCheckedChange = { /* Do Nothing */ },
                         ) {
                             trailingContent()
@@ -157,7 +162,7 @@
                     },
                     trailingButton = {
                         SplitButtonDefaults.OutlinedTrailingButton(
-                            checked = false,
+                            checked = trailingButtonChecked.value,
                             onCheckedChange = { /* Do Nothing */ },
                         ) {
                             trailingContent()
@@ -171,6 +176,10 @@
     override fun ContentWrappers(content: @Composable () -> Unit) {
         MaterialTheme { content() }
     }
+
+    override fun toggleState() {
+        trailingButtonChecked.value = !trailingButtonChecked.value
+    }
 }
 
 @Composable
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
index 0a9014c..e11e8ad 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonTest.kt
@@ -20,6 +20,10 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Edit
 import androidx.compose.material.icons.outlined.KeyboardArrowDown
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
@@ -30,10 +34,14 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.isToggleable
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -85,6 +93,46 @@
     }
 
     @Test
+    fun filledSplitButton_trailingButtonChecked() {
+        rule.setMaterialContent(lightColorScheme()) {
+            var trailingButtonChecked by remember { mutableStateOf(false) }
+
+            SplitButtonLayout(
+                leadingButton = {
+                    SplitButtonDefaults.LeadingButton(
+                        onClick = { /* Do Nothing */ },
+                        modifier = Modifier.testTag("leadingButton")
+                    ) {
+                        Icon(
+                            Icons.Outlined.Edit,
+                            contentDescription = "Leading Icon",
+                        )
+                        Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+                        Text("My Button")
+                    }
+                },
+                trailingButton = {
+                    SplitButtonDefaults.TrailingButton(
+                        modifier = Modifier.size(34.dp).testTag("trailingButton"),
+                        checked = trailingButtonChecked,
+                        onCheckedChange = { trailingButtonChecked = !trailingButtonChecked },
+                    ) {
+                        Icon(Icons.Outlined.KeyboardArrowDown, contentDescription = "Trailing Icon")
+                    }
+                }
+            )
+        }
+
+        rule
+            .onNode(isToggleable())
+            .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Checkbox))
+            .assertIsEnabled()
+            .assertIsOff()
+            .performClick()
+            .assertIsOn()
+    }
+
+    @Test
     fun filledSplitButton_defaultSemantics() {
         rule.setMaterialContent(lightColorScheme()) {
             SplitButtonLayout(
diff --git a/compose/runtime/runtime/proguard-rules.pro b/compose/runtime/runtime/proguard-rules.pro
index a0d0c23..6e78193 100644
--- a/compose/runtime/runtime/proguard-rules.pro
+++ b/compose/runtime/runtime/proguard-rules.pro
@@ -24,6 +24,10 @@
     static java.lang.Void throw*Exception(...);
     static java.lang.Void throw*ExceptionForNullCheck(...);
 
+    # For functions generating error messages
+    static java.lang.String exceptionMessage*(...);
+    java.lang.String exceptionMessage*(...);
+
     static void compose*RuntimeError(...);
     static java.lang.Void compose*RuntimeError(...);
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
index 5ccd71e..c7f6dcb 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operations.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
 package androidx.compose.runtime.changelist
 
 import androidx.compose.runtime.Applier
@@ -101,8 +103,12 @@
 
         // Resize arrays if needed
         if (opCodesSize == opCodes.size) {
+            // Note: manual allocation + copy of the array produces much better code on Android
+            // than calling Array.copyOf()
             val resizeAmount = opCodesSize.coerceAtMost(MaxResizeAmount)
-            opCodes = opCodes.copyOf(opCodesSize + resizeAmount)
+            val newOpCodes = arrayOfNulls<Operation>(opCodesSize + resizeAmount)
+            opCodes.copyInto(newOpCodes, 0, 0, opCodesSize)
+            opCodes = newOpCodes
         }
         ensureIntArgsSizeAtLeast(intArgsSize + operation.ints)
         ensureObjectArgsSizeAtLeast(objectArgsSize + operation.objects)
@@ -121,14 +127,22 @@
     private fun ensureIntArgsSizeAtLeast(requiredSize: Int) {
         val currentSize = intArgs.size
         if (requiredSize > currentSize) {
-            intArgs = intArgs.copyOf(determineNewSize(currentSize, requiredSize))
+            // Note: manual allocation + copy of the array produces much better code on Android
+            // than calling Array.copyOf()
+            val newIntArgs = IntArray(determineNewSize(currentSize, requiredSize))
+            intArgs.copyInto(newIntArgs, 0, 0, currentSize)
+            intArgs = newIntArgs
         }
     }
 
     private fun ensureObjectArgsSizeAtLeast(requiredSize: Int) {
         val currentSize = objectArgs.size
         if (requiredSize > currentSize) {
-            objectArgs = objectArgs.copyOf(determineNewSize(currentSize, requiredSize))
+            // Note: manual allocation + copy of the array produces much better code on Android
+            // than calling Array.copyOf()
+            val newObjectArgs = arrayOfNulls<Any>(determineNewSize(currentSize, requiredSize))
+            objectArgs.copyInto(newObjectArgs, 0, 0, currentSize)
+            objectArgs = newObjectArgs
         }
     }
 
@@ -140,13 +154,16 @@
      * any arguments.
      */
     fun push(operation: Operation) {
-        requirePrecondition(operation.ints == 0 && operation.objects == 0) {
-            "Cannot push $operation without arguments because it expects " +
-                "${operation.ints} ints and ${operation.objects} objects."
+        requirePrecondition((operation.ints and operation.objects) == 0) {
+            exceptionMessageForOperationPushNoScope(operation)
         }
         @OptIn(InternalComposeApi::class) pushOp(operation)
     }
 
+    private fun exceptionMessageForOperationPushNoScope(operation: Operation) =
+        "Cannot push $operation without arguments because it expects " +
+            "${operation.ints} ints and ${operation.objects} objects."
+
     /**
      * Adds an [operation] to the stack with arguments. To set arguments on the operation, call
      * [WriteScope.setObject] and [WriteScope.setInt] inside of the [args] lambda.
@@ -169,42 +186,47 @@
             pushedIntMask == createExpectedArgMask(operation.ints) &&
                 pushedObjectMask == createExpectedArgMask(operation.objects)
         ) {
-            var missingIntCount = 0
-            val missingInts = buildString {
-                repeat(operation.ints) { arg ->
-                    if ((0b1 shl arg) and pushedIntMask != 0b0) {
-                        if (missingIntCount > 0) append(", ")
-                        append(operation.intParamName(IntParameter(arg)))
-                        missingIntCount++
-                    }
-                }
-            }
-
-            var missingObjectCount = 0
-            val missingObjects = buildString {
-                repeat(operation.objects) { arg ->
-                    if ((0b1 shl arg) and pushedObjectMask != 0b0) {
-                        if (missingIntCount > 0) append(", ")
-                        append(operation.objectParamName(ObjectParameter<Nothing>(arg)))
-                        missingObjectCount++
-                    }
-                }
-            }
-
-            "Error while pushing $operation. Not all arguments were provided. " +
-                "Missing $missingIntCount int arguments ($missingInts) " +
-                "and $missingObjectCount object arguments ($missingObjects)."
+            exceptionMessageForOperationPushWithScope(operation)
         }
     }
 
+    private fun exceptionMessageForOperationPushWithScope(operation: Operation): String {
+        var missingIntCount = 0
+        val missingInts = buildString {
+            repeat(operation.ints) { arg ->
+                if ((0b1 shl arg) and pushedIntMask != 0b0) {
+                    if (missingIntCount > 0) append(", ")
+                    append(operation.intParamName(IntParameter(arg)))
+                    missingIntCount++
+                }
+            }
+        }
+
+        var missingObjectCount = 0
+        val missingObjects = buildString {
+            repeat(operation.objects) { arg ->
+                if ((0b1 shl arg) and pushedObjectMask != 0b0) {
+                    if (missingIntCount > 0) append(", ")
+                    append(operation.objectParamName(ObjectParameter<Nothing>(arg)))
+                    missingObjectCount++
+                }
+            }
+        }
+
+        return "Error while pushing $operation. Not all arguments were provided. " +
+            "Missing $missingIntCount int arguments ($missingInts) " +
+            "and $missingObjectCount object arguments ($missingObjects)."
+    }
+
     /**
      * Returns a bitmask int where the bottommost [paramCount] bits are 1's, and the rest of the
      * bits are 0's. This corresponds to what [pushedIntMask] and [pushedObjectMask] will equal if
      * all [paramCount] arguments are set for the most recently pushed operation.
      */
-    private fun createExpectedArgMask(paramCount: Int): Int {
+    private inline fun createExpectedArgMask(paramCount: Int): Int {
         // Calling ushr(32) no-ops instead of returning 0, so add a special case if paramCount is 0
-        return if (paramCount == 0) 0 else 0b0.inv().ushr(Int.SIZE_BITS - paramCount)
+        // Keep the if/else in the parenthesis so we generate a single csetm on aarch64
+        return (if (paramCount == 0) 0 else 0b0.inv()) ushr (Int.SIZE_BITS - paramCount)
     }
 
     /**
@@ -212,15 +234,16 @@
      * references.
      */
     fun pop() {
-        if (isEmpty()) {
-            throw NoSuchElementException("Cannot pop(), because the stack is empty.")
-        }
+        // We could check for isEmpty(), instead we'll just let the array access throw an index out
+        // of bounds exception
         val op = opCodes[--opCodesSize]!!
         opCodes[opCodesSize] = null
 
         repeat(op.objects) { objectArgs[--objectArgsSize] = null }
 
-        repeat(op.ints) { intArgs[--intArgsSize] = 0 }
+        // We can just skip this work and leave the content of the array as is
+        // repeat(op.ints) { intArgs[--intArgsSize] = 0 }
+        intArgsSize -= op.ints
     }
 
     /**
@@ -229,34 +252,40 @@
      */
     @OptIn(InternalComposeApi::class)
     fun popInto(other: Operations) {
-        if (isEmpty()) {
-            throw NoSuchElementException("Cannot pop(), because the stack is empty.")
-        }
+        // We could check for isEmpty(), instead we'll just let the array access throw an index out
+        // of bounds exception
+        val opCodes = opCodes
         val op = opCodes[--opCodesSize]!!
         opCodes[opCodesSize] = null
 
         other.pushOp(op)
 
         var thisObjIdx = objectArgsSize
+        val objectArgs = objectArgs
         var otherObjIdx = other.objectArgsSize
+        val otherObjectArs = other.objectArgs
+
+        objectArgsSize -= op.objects
         repeat(op.objects) {
             otherObjIdx--
             thisObjIdx--
-            other.objectArgs[otherObjIdx] = objectArgs[thisObjIdx]
+            otherObjectArs[otherObjIdx] = objectArgs[thisObjIdx]
             objectArgs[thisObjIdx] = null
         }
 
         var thisIntIdx = intArgsSize
+        val intArgs = intArgs
         var otherIntIdx = other.intArgsSize
+        val otherIntArgs = other.intArgs
+
+        intArgsSize -= op.ints
         repeat(op.ints) {
             otherIntIdx--
             thisIntIdx--
-            other.intArgs[otherIntIdx] = intArgs[thisIntIdx]
-            intArgs[thisIntIdx] = 0
+            otherIntArgs[otherIntIdx] = intArgs[thisIntIdx]
+            // We don't need to zero out the ints
+            // intArgs[thisIntIdx] = 0
         }
-
-        objectArgsSize -= op.objects
-        intArgsSize -= op.ints
     }
 
     /**
@@ -405,7 +434,7 @@
                 var isFirstParam = true
                 val argLinePrefix = linePrefix.indent()
                 repeat(operation.ints) { offset ->
-                    val param = Operation.IntParameter(offset)
+                    val param = IntParameter(offset)
                     val name = operation.intParamName(param)
                     if (!isFirstParam) append(", ") else isFirstParam = false
                     appendLine()
@@ -415,7 +444,7 @@
                     append(getInt(param))
                 }
                 repeat(operation.objects) { offset ->
-                    val param = Operation.ObjectParameter<Any?>(offset)
+                    val param = ObjectParameter<Any?>(offset)
                     val name = operation.objectParamName(param)
                     if (!isFirstParam) append(", ") else isFirstParam = false
                     appendLine()
diff --git a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/changelist/OperationsTest.kt b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/changelist/OperationsTest.kt
index 99e72a1..e3f9c87 100644
--- a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/changelist/OperationsTest.kt
+++ b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/changelist/OperationsTest.kt
@@ -293,7 +293,7 @@
         }
     }
 
-    @Test(expected = NoSuchElementException::class)
+    @Test(expected = IndexOutOfBoundsException::class)
     fun testPop_throwsIfStackIsEmpty() {
         stack.pop()
     }
@@ -336,7 +336,7 @@
         )
     }
 
-    @Test(expected = NoSuchElementException::class)
+    @Test(expected = IndexOutOfBoundsException::class)
     fun testPopInto_throwsIfStackIsEmpty() {
         stack.pop()
     }
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index 8e574c8..86f8a51 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -153,4 +153,7 @@
     namespace "androidx.compose.ui.text"
     // TODO(b/328001575)
     experimentalProperties["android.lint.useK2Uast"] = false
+    buildTypes.configureEach {
+        consumerProguardFiles("proguard-rules.pro")
+    }
 }
diff --git a/compose/ui/ui-text/proguard-rules.pro b/compose/ui/ui-text/proguard-rules.pro
new file mode 100644
index 0000000..67d118b
--- /dev/null
+++ b/compose/ui/ui-text/proguard-rules.pro
@@ -0,0 +1,24 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Keep all the functions created to throw an exception. We don't want these functions to be
+# inlined in any way, which R8 will do by default. The whole point of these functions is to
+# reduce the amount of code generated at the call site.
+-keep,allowshrinking,allowobfuscation class androidx.compose.**.* {
+    static void throw*Exception(...);
+    static void throw*ExceptionForNullCheck(...);
+    # For methods returning Nothing
+    static java.lang.Void throw*Exception(...);
+    static java.lang.Void throw*ExceptionForNullCheck(...);
+}
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
index 7425e87..f3b7e2a 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
@@ -72,6 +72,7 @@
 import androidx.compose.ui.text.android.style.IndentationFixSpan
 import androidx.compose.ui.text.android.style.PlaceholderSpan
 import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.internal.requirePrecondition
 import androidx.compose.ui.text.platform.AndroidParagraphIntrinsics
 import androidx.compose.ui.text.platform.AndroidTextPaint
 import androidx.compose.ui.text.platform.extensions.setSpan
@@ -134,11 +135,11 @@
     @VisibleForTesting internal val charSequence: CharSequence
 
     init {
-        require(constraints.minHeight == 0 && constraints.minWidth == 0) {
+        requirePrecondition(constraints.minHeight == 0 && constraints.minWidth == 0) {
             "Setting Constraints.minWidth and Constraints.minHeight is not supported, " +
                 "these should be the default zero values instead."
         }
-        require(maxLines >= 1) { "maxLines should be greater than 0" }
+        requirePrecondition(maxLines >= 1) { "maxLines should be greater than 0" }
 
         val style = paragraphIntrinsics.style
 
@@ -384,7 +385,7 @@
      * the top, bottom, left and right of a character.
      */
     override fun getBoundingBox(offset: Int): Rect {
-        require(offset in charSequence.indices) {
+        requirePrecondition(offset in charSequence.indices) {
             "offset($offset) is out of bounds [0,${charSequence.length})"
         }
         val rectF = layout.getBoundingBox(offset)
@@ -425,7 +426,7 @@
     }
 
     override fun getPathForRange(start: Int, end: Int): Path {
-        require(start in 0..end && end <= charSequence.length) {
+        requirePrecondition(start in 0..end && end <= charSequence.length) {
             "start($start) or end($end) is out of range [0..${charSequence.length}]," +
                 " or start > end!"
         }
@@ -435,7 +436,7 @@
     }
 
     override fun getCursorRect(offset: Int): Rect {
-        require(offset in 0..charSequence.length) {
+        requirePrecondition(offset in 0..charSequence.length) {
             "offset($offset) is out of bounds [0,${charSequence.length}]"
         }
         val horizontal = layout.getPrimaryHorizontal(offset)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/BoringLayoutFactory.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/BoringLayoutFactory.android.kt
index 287c6c9..133332e 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/BoringLayoutFactory.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/BoringLayoutFactory.android.kt
@@ -24,6 +24,7 @@
 import android.text.TextPaint
 import android.text.TextUtils.TruncateAt
 import androidx.annotation.RequiresApi
+import androidx.compose.ui.text.internal.requirePrecondition
 
 /** Factory Class for BoringLayout */
 @OptIn(InternalPlatformTextApi::class)
@@ -74,8 +75,8 @@
         ellipsize: TruncateAt? = null,
         ellipsizedWidth: Int = width,
     ): BoringLayout {
-        require(width >= 0) { "negative width" }
-        require(ellipsizedWidth >= 0) { "negative ellipsized width" }
+        requirePrecondition(width >= 0) { "negative width" }
+        requirePrecondition(ellipsizedWidth >= 0) { "negative ellipsized width" }
 
         return if (Build.VERSION.SDK_INT >= 33) {
             BoringLayoutFactory33.create(
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/ListUtils.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/ListUtils.android.kt
index 781a651..5871d7f 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/ListUtils.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/ListUtils.android.kt
@@ -70,7 +70,7 @@
 @OptIn(ExperimentalContracts::class)
 internal inline fun <T, R> List<T>.fastZipWithNext(transform: (T, T) -> R): List<R> {
     contract { callsInPlace(transform) }
-    if (size == 0 || size == 1) return emptyList()
+    if (size <= 1) return emptyList()
     val result = mutableListOf<R>()
     var current = get(0)
     // `until` as we don't want to invoke this for the last element, since that won't have a `next`
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt
index 256e746..01604ab 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/StaticLayoutFactory.android.kt
@@ -32,6 +32,7 @@
 import androidx.compose.ui.text.android.LayoutCompat.JustificationMode
 import androidx.compose.ui.text.android.LayoutCompat.LineBreakStyle
 import androidx.compose.ui.text.android.LayoutCompat.LineBreakWordStyle
+import androidx.compose.ui.text.internal.requirePrecondition
 import java.lang.reflect.Constructor
 import java.lang.reflect.InvocationTargetException
 
@@ -139,12 +140,12 @@
     val rightIndents: IntArray?
 ) {
     init {
-        require(start in 0..end) { "invalid start value" }
-        require(end in 0..text.length) { "invalid end value" }
-        require(maxLines >= 0) { "invalid maxLines value" }
-        require(width >= 0) { "invalid width value" }
-        require(ellipsizedWidth >= 0) { "invalid ellipsizedWidth value" }
-        require(lineSpacingMultiplier >= 0f) { "invalid lineSpacingMultiplier value" }
+        requirePrecondition(start in 0..end) { "invalid start value" }
+        requirePrecondition(end in 0..text.length) { "invalid end value" }
+        requirePrecondition(maxLines >= 0) { "invalid maxLines value" }
+        requirePrecondition(width >= 0) { "invalid width value" }
+        requirePrecondition(ellipsizedWidth >= 0) { "invalid ellipsizedWidth value" }
+        requirePrecondition(lineSpacingMultiplier >= 0f) { "invalid lineSpacingMultiplier value" }
     }
 }
 
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
index 81d7f19..5f5fd9a 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/TextLayout.android.kt
@@ -72,6 +72,7 @@
 import androidx.compose.ui.text.android.style.LineHeightStyleSpan
 import androidx.compose.ui.text.android.style.getEllipsizedLeftPadding
 import androidx.compose.ui.text.android.style.getEllipsizedRightPadding
+import androidx.compose.ui.text.internal.requirePrecondition
 import kotlin.math.abs
 import kotlin.math.ceil
 import kotlin.math.max
@@ -603,7 +604,7 @@
         val range = lineEndOffset - lineStartOffset
         val minArraySize = range * 2
 
-        require(array.size >= minArraySize) {
+        requirePrecondition(array.size >= minArraySize) {
             "array.size - arrayStart must be greater or equal than (endOffset - startOffset) * 2"
         }
 
@@ -670,15 +671,21 @@
      */
     fun fillBoundingBoxes(startOffset: Int, endOffset: Int, array: FloatArray, arrayStart: Int) {
         val textLength = text.length
-        require(startOffset >= 0) { "startOffset must be > 0" }
-        require(startOffset < textLength) { "startOffset must be less than text length" }
-        require(endOffset > startOffset) { "endOffset must be greater than startOffset" }
-        require(endOffset <= textLength) { "endOffset must be smaller or equal to text length" }
+        requirePrecondition(startOffset >= 0) { "startOffset must be > 0" }
+        requirePrecondition(startOffset < textLength) {
+            "startOffset must be less than text length"
+        }
+        requirePrecondition(endOffset > startOffset) {
+            "endOffset must be greater than startOffset"
+        }
+        requirePrecondition(endOffset <= textLength) {
+            "endOffset must be smaller or equal to text length"
+        }
 
         val range = endOffset - startOffset
         val minArraySize = range * 4
 
-        require((array.size - arrayStart) >= minArraySize) {
+        requirePrecondition((array.size - arrayStart) >= minArraySize) {
             "array.size - arrayStart must be greater or equal than (endOffset - startOffset) * 4"
         }
 
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/selection/WordIterator.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/selection/WordIterator.android.kt
index 0d43401..588382f 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/selection/WordIterator.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/selection/WordIterator.android.kt
@@ -16,6 +16,7 @@
 package androidx.compose.ui.text.android.selection
 
 import androidx.compose.ui.text.android.CharSequenceCharacterIterator
+import androidx.compose.ui.text.internal.requirePrecondition
 import androidx.emoji2.text.EmojiCompat
 import java.text.BreakIterator
 import java.util.Locale
@@ -40,8 +41,12 @@
     private val iterator: BreakIterator
 
     init {
-        require(start in 0..charSequence.length) { "input start index is outside the CharSequence" }
-        require(end in 0..charSequence.length) { "input end index is outside the CharSequence" }
+        requirePrecondition(start in 0..charSequence.length) {
+            "input start index is outside the CharSequence"
+        }
+        requirePrecondition(end in 0..charSequence.length) {
+            "input end index is outside the CharSequence"
+        }
         iterator = BreakIterator.getWordInstance(locale)
         this.start = max(0, start - WINDOW_WIDTH)
         this.end = min(charSequence.length, end + WINDOW_WIDTH)
@@ -315,8 +320,8 @@
 
     /** Check if the given offset is in the given range. */
     private fun checkOffsetIsValid(offset: Int) {
-        require(offset in start..end) {
-            ("Invalid offset: $offset. Valid range is [$start , $end]")
+        requirePrecondition(offset in start..end) {
+            "Invalid offset: $offset. Valid range is [$start , $end]"
         }
     }
 
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
index e0d6cf0..1114446 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/LineHeightStyleSpan.android.kt
@@ -17,6 +17,7 @@
 
 import android.graphics.Paint.FontMetricsInt
 import androidx.annotation.FloatRange
+import androidx.compose.ui.text.internal.checkPrecondition
 import kotlin.math.abs
 import kotlin.math.ceil
 
@@ -64,7 +65,9 @@
         private set
 
     init {
-        check(topRatio in 0f..1f || topRatio == -1f) { "topRatio should be in [0..1] range or -1" }
+        checkPrecondition(topRatio in 0f..1f || topRatio == -1f) {
+            "topRatio should be in [0..1] range or -1"
+        }
     }
 
     override fun chooseHeight(
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/PlaceholderSpan.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/PlaceholderSpan.android.kt
index 4f665e8..8eb5c28 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/PlaceholderSpan.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/android/style/PlaceholderSpan.android.kt
@@ -21,6 +21,10 @@
 import android.graphics.Paint
 import android.text.style.ReplacementSpan
 import androidx.annotation.IntDef
+import androidx.compose.ui.text.internal.checkPrecondition
+import androidx.compose.ui.text.internal.requirePrecondition
+import androidx.compose.ui.text.internal.throwIllegalArgumentException
+import androidx.compose.ui.text.internal.throwIllegalArgumentExceptionForNullCheck
 import kotlin.math.ceil
 import kotlin.math.max
 import kotlin.math.min
@@ -85,7 +89,7 @@
     var widthPx: Int = 0
         private set
         get() {
-            check(isLaidOut) { "PlaceholderSpan is not laid out yet." }
+            checkPrecondition(isLaidOut) { "PlaceholderSpan is not laid out yet." }
             return field
         }
 
@@ -93,7 +97,7 @@
     var heightPx: Int = 0
         private set
         get() {
-            check(isLaidOut) { "PlaceholderSpan is not laid out yet." }
+            checkPrecondition(isLaidOut) { "PlaceholderSpan is not laid out yet." }
             return field
         }
 
@@ -111,7 +115,7 @@
         isLaidOut = true
         val fontSize = paint.textSize
         fontMetrics = paint.fontMetricsInt
-        require(fontMetrics.descent > fontMetrics.ascent) {
+        requirePrecondition(fontMetrics.descent > fontMetrics.ascent) {
             "Invalid fontMetrics: line height can not be negative."
         }
 
@@ -119,14 +123,14 @@
             when (widthUnit) {
                 UNIT_SP -> width * pxPerSp
                 UNIT_EM -> width * fontSize
-                else -> throw IllegalArgumentException("Unsupported unit.")
+                else -> throwIllegalArgumentExceptionForNullCheck("Unsupported unit.")
             }.ceilToInt()
 
         heightPx =
             when (heightUnit) {
                 UNIT_SP -> (height * pxPerSp).ceilToInt()
                 UNIT_EM -> (height * fontSize).ceilToInt()
-                else -> throw IllegalArgumentException("Unsupported unit.")
+                else -> throwIllegalArgumentExceptionForNullCheck("Unsupported unit.")
             }
 
         fm?.apply {
@@ -159,7 +163,7 @@
                     if (ascent > -heightPx) {
                         ascent = -heightPx
                     }
-                else -> throw IllegalArgumentException("Unknown verticalAlign.")
+                else -> throwIllegalArgumentException("Unknown verticalAlign.")
             }
             // make top/bottom at least same as ascent/descent.
             top = min(fontMetrics.top, ascent)
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFont.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFont.android.kt
index 21c5b32..073ed59 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFont.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/font/DeviceFontFamilyNameFont.android.kt
@@ -20,6 +20,7 @@
 
 import android.content.Context
 import android.graphics.Typeface
+import androidx.compose.ui.text.internal.requirePrecondition
 
 /**
  * Describes a system-installed font that may be present on some Android devices.
@@ -74,7 +75,7 @@
 @JvmInline
 value class DeviceFontFamilyName(val name: String) {
     init {
-        require(name.isNotEmpty()) { "name may not be empty" }
+        requirePrecondition(name.isNotEmpty()) { "name may not be empty" }
     }
 }
 
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidFontListTypeface.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidFontListTypeface.android.kt
index aaa9578..8e6a073 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidFontListTypeface.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidFontListTypeface.android.kt
@@ -34,6 +34,9 @@
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.ResourceFont
 import androidx.compose.ui.text.font.synthesizeTypeface
+import androidx.compose.ui.text.internal.checkPrecondition
+import androidx.compose.ui.text.internal.checkPreconditionNotNull
+import androidx.compose.ui.text.internal.throwIllegalStateException
 import androidx.compose.ui.util.fastDistinctBy
 import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastFilterNotNull
@@ -69,14 +72,14 @@
                 ?.fastFilterNotNull()
                 ?.fastDistinctBy { it }
         val targetFonts = matchedFonts ?: blockingFonts
-        check(targetFonts.isNotEmpty()) { "Could not match font" }
+        checkPrecondition(targetFonts.isNotEmpty()) { "Could not match font" }
 
         val typefaces = mutableMapOf<Font, Typeface>()
         targetFonts.fastForEach {
             try {
                 typefaces[it] = AndroidTypefaceCache.getOrCreate(context, it)
             } catch (e: Exception) {
-                throw IllegalStateException("Cannot create Typeface from $it")
+                throwIllegalStateException("Cannot create Typeface from $it")
             }
         }
 
@@ -94,10 +97,10 @@
             fontMatcher
                 .matchFont(ArrayList(loadedTypefaces.keys), fontWeight, fontStyle)
                 .firstOrNull()
-        checkNotNull(font) { "Could not load font" }
+        checkPreconditionNotNull(font) { "Could not load font" }
 
         val typeface = loadedTypefaces[font]
-        checkNotNull(typeface) { "Could not load typeface" }
+        checkPreconditionNotNull(typeface) { "Could not load typeface" }
 
         return synthesis.synthesizeTypeface(typeface, font, fontWeight, fontStyle) as Typeface
     }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
index 380f9d4..d407002 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
@@ -22,14 +22,16 @@
 import androidx.compose.ui.text.AnnotatedString.Annotation
 import androidx.compose.ui.text.AnnotatedString.Builder
 import androidx.compose.ui.text.AnnotatedString.Range
+import androidx.compose.ui.text.internal.checkPrecondition
+import androidx.compose.ui.text.internal.requirePrecondition
 import androidx.compose.ui.text.intl.LocaleList
 import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastCoerceIn
 import androidx.compose.ui.util.fastFilter
+import androidx.compose.ui.util.fastFilteredMap
 import androidx.compose.ui.util.fastFlatMap
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.contract
 import kotlin.jvm.JvmName
 
 /**
@@ -44,12 +46,12 @@
     internal val spanStylesOrNull: List<Range<SpanStyle>>?
     /** All [SpanStyle] that have been applied to a range of this String */
     val spanStyles: List<Range<SpanStyle>>
-        get() = spanStylesOrNull ?: emptyList()
+        get() = spanStylesOrNull ?: listOf()
 
     internal val paragraphStylesOrNull: List<Range<ParagraphStyle>>?
     /** All [ParagraphStyle] that have been applied to a range of this String */
     val paragraphStyles: List<Range<ParagraphStyle>>
-        get() = paragraphStylesOrNull ?: emptyList()
+        get() = paragraphStylesOrNull ?: listOf()
 
     /**
      * The basic data structure of text with multiple styles. To construct an [AnnotatedString] you
@@ -61,7 +63,7 @@
      * @param spanStyles a list of [Range]s that specifies [SpanStyle]s on certain portion of the
      *   text. These styles will be applied in the order of the list. And the [SpanStyle]s applied
      *   later can override the former styles. Notice that [SpanStyle] attributes which are null or
-     *   [Unspecified] won't change the current ones.
+     *   unspecified won't change the current ones.
      * @param paragraphStyles a list of [Range]s that specifies [ParagraphStyle]s on certain portion
      *   of the text. Each [ParagraphStyle] with a [Range] defines a paragraph of text. It's
      *   required that [Range]s of paragraphs don't overlap with each other. If there are gaps
@@ -99,7 +101,7 @@
      */
     constructor(
         text: String,
-        annotations: List<Range<out Annotation>> = emptyList()
+        annotations: List<Range<out Annotation>> = listOf()
     ) : this(annotations.ifEmpty { null }, text)
 
     init {
@@ -127,10 +129,10 @@
         paragraphStylesOrNull
             ?.sortedBy { it.start }
             ?.fastForEach { paragraphStyle ->
-                require(paragraphStyle.start >= lastStyleEnd) {
+                requirePrecondition(paragraphStyle.start >= lastStyleEnd) {
                     "ParagraphStyle should not overlap"
                 }
-                require(paragraphStyle.end <= text.length) {
+                requirePrecondition(paragraphStyle.end <= text.length) {
                     "ParagraphStyle range [${paragraphStyle.start}, ${paragraphStyle.end})" +
                         " is out of boundary"
                 }
@@ -151,7 +153,7 @@
      * @param endIndex the exclusive end offset of the range
      */
     override fun subSequence(startIndex: Int, endIndex: Int): AnnotatedString {
-        require(startIndex <= endIndex) {
+        requirePrecondition(startIndex <= endIndex) {
             "start ($startIndex) should be less or equal to end ($endIndex)"
         }
         if (startIndex == 0 && endIndex == text.length) return this
@@ -193,13 +195,13 @@
      *   with the range [start, end) will be returned. When [start] is bigger than [end], an empty
      *   list will be returned.
      */
-    @Suppress("UNCHECKED_CAST")
+    @Suppress("UNCHECKED_CAST", "KotlinRedundantDiagnosticSuppress")
     fun getStringAnnotations(tag: String, start: Int, end: Int): List<Range<String>> =
-        (annotations?.fastFilterMap({
+        (annotations?.fastFilteredMap({
             it.item is StringAnnotation && tag == it.tag && intersect(start, end, it.start, it.end)
         }) {
             it.unbox()
-        } ?: emptyList())
+        } ?: listOf())
 
     /**
      * Returns true if [getStringAnnotations] with the same parameters would return a non-empty list
@@ -218,13 +220,13 @@
      *   with the range [start, end) will be returned. When [start] is bigger than [end], an empty
      *   list will be returned.
      */
-    @Suppress("UNCHECKED_CAST")
+    @Suppress("UNCHECKED_CAST", "KotlinRedundantDiagnosticSuppress")
     fun getStringAnnotations(start: Int, end: Int): List<Range<String>> =
-        annotations?.fastFilterMap({
+        annotations?.fastFilteredMap({
             it.item is StringAnnotation && intersect(start, end, it.start, it.end)
         }) {
             it.unbox()
-        } ?: emptyList()
+        } ?: listOf()
 
     /**
      * Query all of the [TtsAnnotation]s attached on this [AnnotatedString].
@@ -239,7 +241,7 @@
     fun getTtsAnnotations(start: Int, end: Int): List<Range<TtsAnnotation>> =
         ((annotations?.fastFilter {
             it.item is TtsAnnotation && intersect(start, end, it.start, it.end)
-        } ?: emptyList())
+        } ?: listOf())
             as List<Range<TtsAnnotation>>)
 
     /**
@@ -257,7 +259,7 @@
     fun getUrlAnnotations(start: Int, end: Int): List<Range<UrlAnnotation>> =
         ((annotations?.fastFilter {
             it.item is UrlAnnotation && intersect(start, end, it.start, it.end)
-        } ?: emptyList())
+        } ?: listOf())
             as List<Range<UrlAnnotation>>)
 
     /**
@@ -273,7 +275,7 @@
     fun getLinkAnnotations(start: Int, end: Int): List<Range<LinkAnnotation>> =
         ((annotations?.fastFilter {
             it.item is LinkAnnotation && intersect(start, end, it.start, it.end)
-        } ?: emptyList())
+        } ?: listOf())
             as List<Range<LinkAnnotation>>)
 
     /**
@@ -360,7 +362,7 @@
         constructor(item: T, start: Int, end: Int) : this(item, start, end, "")
 
         init {
-            require(start <= end) { "Reversed range is not supported" }
+            requirePrecondition(start <= end) { "Reversed range is not supported" }
         }
     }
 
@@ -391,7 +393,7 @@
              */
             fun toRange(defaultEnd: Int = Int.MIN_VALUE): Range<T> {
                 val end = if (end == Int.MIN_VALUE) defaultEnd else end
-                check(end != Int.MIN_VALUE) { "Item.end should be set first" }
+                checkPrecondition(end != Int.MIN_VALUE) { "Item.end should be set first" }
                 return Range(item = item, start = start, end = end, tag = tag)
             }
 
@@ -402,12 +404,12 @@
              */
             fun <R> toRange(transform: (T) -> R, defaultEnd: Int = Int.MIN_VALUE): Range<R> {
                 val end = if (end == Int.MIN_VALUE) defaultEnd else end
-                check(end != Int.MIN_VALUE) { "Item.end should be set first" }
+                checkPrecondition(end != Int.MIN_VALUE) { "Item.end should be set first" }
                 return Range(item = transform(item), start = start, end = end, tag = tag)
             }
 
             companion object {
-                fun <T> fromRange(range: AnnotatedString.Range<T>) =
+                fun <T> fromRange(range: Range<T>) =
                     MutableRange(range.item, range.start, range.end, range.tag)
             }
         }
@@ -545,7 +547,7 @@
         }
 
         /**
-         * Set a [SpanStyle] for the given [range].
+         * Set a [SpanStyle] for the given range defined by [start] and [end].
          *
          * @param style [SpanStyle] to be applied
          * @param start the inclusive starting offset of the range
@@ -556,8 +558,9 @@
         }
 
         /**
-         * Set a [ParagraphStyle] for the given [range]. When a [ParagraphStyle] is applied to the
-         * [AnnotatedString], it will be rendered as a separate paragraph.
+         * Set a [ParagraphStyle] for the given range defined by [start] and [end]. When a
+         * [ParagraphStyle] is applied to the [AnnotatedString], it will be rendered as a separate
+         * paragraph.
          *
          * @param style [ParagraphStyle] to be applied
          * @param start the inclusive starting offset of the range
@@ -568,7 +571,7 @@
         }
 
         /**
-         * Set an Annotation for the given [range].
+         * Set an Annotation for the given range defined by [start] and [end].
          *
          * @param tag the tag used to distinguish annotations
          * @param annotation the string annotation that is attached
@@ -589,7 +592,7 @@
         }
 
         /**
-         * Set a [TtsAnnotation] for the given [range].
+         * Set a [TtsAnnotation] for the given range defined by [start] and [end].
          *
          * @param ttsAnnotation an object that stores text to speech metadata that intended for the
          *   TTS engine.
@@ -605,9 +608,9 @@
         }
 
         /**
-         * Set a [UrlAnnotation] for the given [range]. URLs may be treated specially by screen
-         * readers, including being identified while reading text with an audio icon or being
-         * summarized in a links menu.
+         * Set a [UrlAnnotation] for the given range defined by [start] and [end]. URLs may be
+         * treated specially by screen readers, including being identified while reading text with
+         * an audio icon or being summarized in a links menu.
          *
          * @param urlAnnotation A [UrlAnnotation] object that stores the URL being linked to.
          * @param start the inclusive starting offset of the range
@@ -626,10 +629,10 @@
         }
 
         /**
-         * Set a [LinkAnnotation.Url] for the given [range].
+         * Set a [LinkAnnotation.Url] for the given range defined by [start] and [end].
          *
-         * When clicking on the text in [range], the corresponding URL from the [url] annotation
-         * will be opened using [androidx.compose.ui.platform.UriHandler].
+         * When clicking on the text in range, the corresponding URL from the [url] annotation will
+         * be opened using [androidx.compose.ui.platform.UriHandler].
          *
          * URLs may be treated specially by screen readers, including being identified while reading
          * text with an audio icon or being summarized in a links menu.
@@ -645,9 +648,9 @@
         }
 
         /**
-         * Set a [LinkAnnotation.Clickable] for the given [range].
+         * Set a [LinkAnnotation.Clickable] for the given range defined by [start] and [end].
          *
-         * When clicking on the text in [range], a [LinkInteractionListener] will be triggered with
+         * When clicking on the text in range, a [LinkInteractionListener] will be triggered with
          * the [clickable] object.
          *
          * Clickable link may be treated specially by screen readers, including being identified
@@ -775,7 +778,7 @@
          * @see pushStringAnnotation
          */
         fun pop() {
-            check(styleStack.isNotEmpty()) { "Nothing to pop." }
+            checkPrecondition(styleStack.isNotEmpty()) { "Nothing to pop." }
             // pop the last element
             val item = styleStack.removeAt(styleStack.size - 1)
             item.end = text.length
@@ -792,7 +795,9 @@
          * @see pushStringAnnotation
          */
         fun pop(index: Int) {
-            check(index < styleStack.size) { "$index should be less than ${styleStack.size}" }
+            checkPrecondition(index < styleStack.size) {
+                "$index should be less than ${styleStack.size}"
+            }
             while ((styleStack.size - 1) >= index) {
                 pop()
             }
@@ -842,7 +847,7 @@
     sealed interface Annotation
 
     // Unused private subclass of the marker interface to avoid exhaustive "when" statement
-    private class ExhaustiveAnnotation : Annotation
+    @Suppress("unused") private class ExhaustiveAnnotation : Annotation
 
     companion object {
         /**
@@ -891,7 +896,7 @@
     defaultParagraphStyle: ParagraphStyle
 ): List<Range<ParagraphStyle>> {
     val length = text.length
-    val paragraphStyles = paragraphStylesOrNull ?: emptyList()
+    val paragraphStyles = paragraphStylesOrNull ?: listOf()
 
     var lastOffset = 0
     val result = mutableListOf<Range<ParagraphStyle>>()
@@ -928,8 +933,12 @@
     if (start == 0 && end >= this.text.length) {
         return spanStyles
     }
-    return spanStyles.fastFilterMap({ intersect(start, end, it.start, it.end) }) {
-        Range(it.item, it.start.coerceIn(start, end) - start, it.end.coerceIn(start, end) - start)
+    return spanStyles.fastFilteredMap({ intersect(start, end, it.start, it.end) }) {
+        Range(
+            it.item,
+            it.start.fastCoerceIn(start, end) - start,
+            it.end.fastCoerceIn(start, end) - start
+        )
     }
 }
 
@@ -950,8 +959,12 @@
     if (start == 0 && end >= this.text.length) {
         return paragraphStyles
     }
-    return paragraphStyles.fastFilterMap({ intersect(start, end, it.start, it.end) }) {
-        Range(it.item, it.start.coerceIn(start, end) - start, it.end.coerceIn(start, end) - start)
+    return paragraphStyles.fastFilteredMap({ intersect(start, end, it.start, it.end) }) {
+        Range(
+            it.item,
+            it.start.fastCoerceIn(start, end) - start,
+            it.end.fastCoerceIn(start, end) - start
+        )
     }
 }
 
@@ -965,14 +978,14 @@
 private fun AnnotatedString.getLocalAnnotations(
     start: Int,
     end: Int
-): List<Range<out AnnotatedString.Annotation>>? {
+): List<Range<out Annotation>>? {
     if (start == end) return null
     val annotations = annotations ?: return null
     // If the given range covers the whole AnnotatedString, return it without conversion.
     if (start == 0 && end >= this.text.length) {
         return annotations
     }
-    return annotations.fastFilterMap({ intersect(start, end, it.start, it.end) }) {
+    return annotations.fastFilteredMap({ intersect(start, end, it.start, it.end) }) {
         Range(
             tag = it.tag,
             item = it.item,
@@ -994,7 +1007,7 @@
 private fun AnnotatedString.substringWithoutParagraphStyles(start: Int, end: Int): AnnotatedString {
     return AnnotatedString(
         text = if (start != end) text.substring(start, end) else "",
-        annotations = getLocalSpanStyles(start, end) ?: emptyList()
+        annotations = getLocalSpanStyles(start, end) ?: listOf()
     )
 }
 
@@ -1249,11 +1262,13 @@
  * @param end the exclusive end offset of the text range
  */
 private fun <T> filterRanges(ranges: List<Range<out T>>?, start: Int, end: Int): List<Range<T>>? {
-    require(start <= end) { "start ($start) should be less than or equal to end ($end)" }
+    requirePrecondition(start <= end) {
+        "start ($start) should be less than or equal to end ($end)"
+    }
     val nonNullRange = ranges ?: return null
 
     return nonNullRange
-        .fastFilterMap({ intersect(start, end, it.start, it.end) }) {
+        .fastFilteredMap({ intersect(start, end, it.start, it.end) }) {
             Range(
                 item = it.item,
                 start = maxOf(start, it.start) - start,
@@ -1300,43 +1315,30 @@
     Builder().apply(builder).toAnnotatedString()
 
 /**
- * Helper function that checks if the range [baseStart, baseEnd) contains the range [targetStart,
- * targetEnd).
- *
- * @return true if
- *   [baseStart, baseEnd) contains [targetStart, targetEnd), vice versa. When [baseStart]==[baseEnd]
- *   it return true iff [targetStart]==[targetEnd]==[baseStart].
- */
-internal fun contains(baseStart: Int, baseEnd: Int, targetStart: Int, targetEnd: Int) =
-    (baseStart <= targetStart && targetEnd <= baseEnd) &&
-        (baseEnd != targetEnd || (targetStart == targetEnd) == (baseStart == baseEnd))
-
-/**
  * Helper function that checks if the range [lStart, lEnd) intersects with the range [rStart, rEnd).
  *
  * @return [lStart, lEnd) intersects with range [rStart, rEnd), vice versa.
  */
-internal fun intersect(lStart: Int, lEnd: Int, rStart: Int, rEnd: Int) =
-    maxOf(lStart, rStart) < minOf(lEnd, rEnd) ||
-        contains(lStart, lEnd, rStart, rEnd) ||
-        contains(rStart, rEnd, lStart, lEnd)
+internal fun intersect(lStart: Int, lEnd: Int, rStart: Int, rEnd: Int): Boolean {
+    // We can check if two ranges intersect just by performing the following operation:
+    //
+    //     lStart < rEnd && rStart < lEnd
+    //
+    // This operation handles all cases, including when one of the ranges is fully included in the
+    // other ranges. This is however not enough in this particular case because our ranges are open
+    // at the end, but closed at the start.
+    //
+    // This means the test above would fail cases like: [1, 4) intersect [1, 1)
+    // To address this we check if either one of the ranges is a "point" (empty selection). If
+    // that's the case and both ranges share the same start point, then they intersect.
+    //
+    // In addition, we use bitwise operators (or, and) instead of boolean operators (||, &&) to
+    // generate branchless code.
+    return ((lStart == lEnd) or (rStart == rEnd) and (lStart == rStart)) or
+        ((lStart < rEnd) and (rStart < lEnd))
+}
 
 private val EmptyAnnotatedString: AnnotatedString = AnnotatedString("")
 
 /** Returns an AnnotatedString with empty text and no annotations. */
 internal fun emptyAnnotatedString() = EmptyAnnotatedString
-
-@OptIn(ExperimentalContracts::class)
-@Suppress("BanInlineOptIn")
-private inline fun <T, R> List<T>.fastFilterMap(
-    predicate: (T) -> Boolean,
-    transform: (T) -> R
-): List<R> {
-    contract {
-        callsInPlace(predicate)
-        callsInPlace(transform)
-    }
-    val target = ArrayList<R>(size)
-    fastForEach { if (predicate(it)) target += transform(it) }
-    return target
-}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
index fff373a..b6d6baf 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraph.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.internal.requirePrecondition
 import androidx.compose.ui.text.platform.drawMultiParagraph
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.text.style.TextDecoration
@@ -378,7 +379,7 @@
     internal val paragraphInfoList: List<ParagraphInfo>
 
     init {
-        require(constraints.minWidth == 0 && constraints.minHeight == 0) {
+        requirePrecondition(constraints.minWidth == 0 && constraints.minHeight == 0) {
             "Setting Constraints.minWidth and Constraints.minHeight is not supported, " +
                 "these should be the default zero values instead."
         }
@@ -511,7 +512,7 @@
 
     /** Returns path that enclose the given text range. */
     fun getPathForRange(start: Int, end: Int): Path {
-        require(start in 0..end && end <= annotatedString.text.length) {
+        requirePrecondition(start in 0..end && end <= annotatedString.text.length) {
             "Start($start) or End($end) is out of range [0..${annotatedString.text.length})," +
                 " or start > end!"
         }
@@ -977,19 +978,19 @@
     }
 
     private fun requireIndexInRange(offset: Int) {
-        require(offset in annotatedString.text.indices) {
+        requirePrecondition(offset in annotatedString.text.indices) {
             "offset($offset) is out of bounds [0, ${annotatedString.length})"
         }
     }
 
     private fun requireIndexInRangeInclusiveEnd(offset: Int) {
-        require(offset in 0..annotatedString.text.length) {
+        requirePrecondition(offset in 0..annotatedString.text.length) {
             "offset($offset) is out of bounds [0, ${annotatedString.length}]"
         }
     }
 
     private fun requireLineIndexInRange(lineIndex: Int) {
-        require(lineIndex in 0 until lineCount) {
+        requirePrecondition(lineIndex in 0 until lineCount) {
             "lineIndex($lineIndex) is out of bounds [0, $lineCount)"
         }
     }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
index 2c35bcb..252c853 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
@@ -19,11 +19,11 @@
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.internal.requirePrecondition
 import androidx.compose.ui.text.style.TextDirection
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastFilter
-import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.util.fastFilteredMap
 import androidx.compose.ui.util.fastMaxBy
 
 /**
@@ -142,13 +142,12 @@
 }
 
 private fun List<AnnotatedString.Range<Placeholder>>.getLocalPlaceholders(start: Int, end: Int) =
-    fastFilter { intersect(start, end, it.start, it.end) }
-        .fastMap {
-            require(start <= it.start && it.end <= end) {
-                "placeholder can not overlap with paragraph."
-            }
-            AnnotatedString.Range(it.item, it.start - start, it.end - start)
+    fastFilteredMap({ intersect(start, end, it.start, it.end) }) {
+        requirePrecondition(start <= it.start && it.end <= end) {
+            "placeholder can not overlap with paragraph."
         }
+        AnnotatedString.Range(it.item, it.start - start, it.end - start)
+    }
 
 internal data class ParagraphIntrinsicInfo(
     val intrinsics: ParagraphIntrinsics,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
index afeeab0..96b97b3 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
+import androidx.compose.ui.text.internal.checkPrecondition
 import androidx.compose.ui.text.style.Hyphens
 import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.LineHeightStyle
@@ -204,7 +205,9 @@
     init {
         if (lineHeight != TextUnit.Unspecified) {
             // Since we are checking if it's negative, no need to convert Sp into Px at this point.
-            check(lineHeight.value >= 0f) { "lineHeight can't be negative (${lineHeight.value})" }
+            checkPrecondition(lineHeight.value >= 0f) {
+                "lineHeight can't be negative (${lineHeight.value})"
+            }
         }
     }
 
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt
index 4afec70..a7fe2c5 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Placeholder.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.text
 
 import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.internal.requirePrecondition
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.isUnspecified
 
@@ -40,8 +41,8 @@
     val placeholderVerticalAlign: PlaceholderVerticalAlign
 ) {
     init {
-        require(!width.isUnspecified) { "width cannot be TextUnit.Unspecified" }
-        require(!height.isUnspecified) { "height cannot be TextUnit.Unspecified" }
+        requirePrecondition(!width.isUnspecified) { "width cannot be TextUnit.Unspecified" }
+        requirePrecondition(!height.isUnspecified) { "height cannot be TextUnit.Unspecified" }
     }
 
     fun copy(
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index cd65c98..b35f666 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -144,7 +144,7 @@
         overflow: TextOverflow = TextOverflow.Clip,
         softWrap: Boolean = true,
         maxLines: Int = Int.MAX_VALUE,
-        placeholders: List<AnnotatedString.Range<Placeholder>> = emptyList(),
+        placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
         constraints: Constraints = Constraints(),
         layoutDirection: LayoutDirection = this.defaultLayoutDirection,
         density: Density = this.defaultDensity,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
index 4ef4dad..cd3885e 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
@@ -148,7 +148,7 @@
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
-    placeholders: List<AnnotatedString.Range<Placeholder>> = emptyList(),
+    placeholders: List<AnnotatedString.Range<Placeholder>> = listOf(),
     size: Size = Size.Unspecified,
     blendMode: BlendMode = DrawScope.DefaultBlendMode
 ) {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextRange.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextRange.kt
index 209d1f6..f9c3c0a 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextRange.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextRange.kt
@@ -17,9 +17,13 @@
 package androidx.compose.ui.text
 
 import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.internal.requirePrecondition
+import androidx.compose.ui.util.fastCoerceIn
 import androidx.compose.ui.util.packInts
 import androidx.compose.ui.util.unpackInt1
 import androidx.compose.ui.util.unpackInt2
+import kotlin.math.max
+import kotlin.math.min
 
 fun CharSequence.substring(range: TextRange): String = this.substring(range.min, range.max)
 
@@ -53,11 +57,11 @@
 
     /** The minimum offset of the range. */
     val min: Int
-        get() = if (start > end) end else start
+        get() = min(start, end)
 
     /** The maximum offset of the range. */
     val max: Int
-        get() = if (start > end) start else end
+        get() = max(start, end)
 
     /** Returns true if the range is collapsed */
     val collapsed: Boolean
@@ -72,10 +76,10 @@
         get() = max - min
 
     /** Returns true if the given range has intersection with this range */
-    fun intersects(other: TextRange): Boolean = min < other.max && other.min < max
+    fun intersects(other: TextRange): Boolean = (min < other.max) and (other.min < max)
 
     /** Returns true if this range covers including equals with the given range. */
-    operator fun contains(other: TextRange): Boolean = min <= other.min && other.max <= max
+    operator fun contains(other: TextRange): Boolean = (min <= other.min) and (other.max <= max)
 
     /** Returns true if the given offset is a part of this range. */
     operator fun contains(offset: Int): Boolean = offset in min until max
@@ -102,8 +106,8 @@
  * @param maximumValue the exclusive maximum value that [TextRange.start] or [TextRange.end] can be.
  */
 fun TextRange.coerceIn(minimumValue: Int, maximumValue: Int): TextRange {
-    val newStart = start.coerceIn(minimumValue, maximumValue)
-    val newEnd = end.coerceIn(minimumValue, maximumValue)
+    val newStart = start.fastCoerceIn(minimumValue, maximumValue)
+    val newEnd = end.fastCoerceIn(minimumValue, maximumValue)
     if (newStart != start || newEnd != end) {
         return TextRange(newStart, newEnd)
     }
@@ -111,7 +115,8 @@
 }
 
 private fun packWithCheck(start: Int, end: Int): Long {
-    require(start >= 0) { "start cannot be negative. [start: $start, end: $end]" }
-    require(end >= 0) { "end cannot be negative. [start: $start, end: $end]" }
+    requirePrecondition(start >= 0 && end >= 0) {
+        "start and end cannot be negative. [start: $start, end: $end]"
+    }
     return packInts(start, end)
 }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
index b9953c1..fa98708 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
@@ -19,6 +19,7 @@
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
+import androidx.compose.ui.text.internal.checkPrecondition
 
 /**
  * The primary typography interface for Compose applications.
@@ -164,7 +165,7 @@
     val fonts: List<Font>
 ) : FileBasedFontFamily(), List<Font> by fonts {
     init {
-        check(fonts.isNotEmpty()) { "At least one font should be passed to FontFamily" }
+        checkPrecondition(fonts.isNotEmpty()) { "At least one font should be passed to FontFamily" }
     }
 
     override fun equals(other: Any?): Boolean {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt
index 54c0aa1..1c22f1f 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontListFontFamilyTypefaceAdapter.kt
@@ -26,7 +26,7 @@
 import androidx.compose.ui.text.platform.createSynchronizedObject
 import androidx.compose.ui.text.platform.synchronized
 import androidx.compose.ui.util.fastDistinctBy
-import androidx.compose.ui.util.fastFilter
+import androidx.compose.ui.util.fastFilteredMap
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 import kotlin.coroutines.CoroutineContext
@@ -68,8 +68,9 @@
         // only preload styles that can be satisfied by async fonts
         val asyncStyles =
             family.fonts
-                .fastFilter { it.loadingStrategy == FontLoadingStrategy.Async }
-                .fastMap { it.weight to it.style }
+                .fastFilteredMap({ it.loadingStrategy == FontLoadingStrategy.Async }) {
+                    it.weight to it.style
+                }
                 .fastDistinctBy { it }
 
         val asyncLoads: MutableList<Font> = mutableListOf()
@@ -252,8 +253,7 @@
     return asyncFontsToLoad to fallbackTypeface
 }
 
-internal class AsyncFontListLoader
-constructor(
+internal class AsyncFontListLoader(
     private val fontList: List<Font>,
     initialType: Any,
     private val typefaceRequest: TypefaceRequest,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontMatcher.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontMatcher.kt
index 43dbe88..7de23a6 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontMatcher.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontMatcher.kt
@@ -99,7 +99,7 @@
         return result
     }
 
-    @Suppress("NOTHING_TO_INLINE")
+    @Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
     // @VisibleForTesting
     internal inline fun List<Font>.filterByClosestWeight(
         fontWeight: FontWeight,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.kt
index 00e6084..e972956 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontSynthesis.kt
@@ -15,6 +15,10 @@
  */
 package androidx.compose.ui.text.font
 
+private const val AllFlags = 0xffff
+private const val WeightFlag = 0x1
+private const val StyleFlag = 0x2
+
 /**
  * Possible options for font synthesis.
  *
@@ -39,13 +43,14 @@
     override fun toString(): String {
         return when (this) {
             None -> "None"
-            All -> "All"
             Weight -> "Weight"
             Style -> "Style"
+            All -> "All"
             else -> "Invalid"
         }
     }
 
+    // NOTE: The values below are selected to be used as flags. See isWeightOn for instance.
     companion object {
         /**
          * Turns off font synthesis. Neither bold nor slanted faces are synthesized if they don't
@@ -54,29 +59,29 @@
         val None = FontSynthesis(0)
 
         /**
-         * The system synthesizes both bold and slanted fonts if either of them are not available in
-         * the [FontFamily]
-         */
-        val All = FontSynthesis(1)
-
-        /**
          * Only a bold font is synthesized, if it is not available in the [FontFamily]. Slanted
          * fonts will not be synthesized.
          */
-        val Weight = FontSynthesis(2)
+        val Weight = FontSynthesis(WeightFlag)
 
         /**
          * Only an slanted font is synthesized, if it is not available in the [FontFamily]. Bold
          * fonts will not be synthesized.
          */
-        val Style = FontSynthesis(3)
+        val Style = FontSynthesis(StyleFlag)
+
+        /**
+         * The system synthesizes both bold and slanted fonts if either of them are not available in
+         * the [FontFamily]
+         */
+        val All = FontSynthesis(AllFlags)
     }
 
     internal val isWeightOn: Boolean
-        get() = this == All || this == Weight
+        get() = value and WeightFlag != 0
 
     internal val isStyleOn: Boolean
-        get() = this == All || this == Style
+        get() = value and StyleFlag != 0
 }
 
 /**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontVariation.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontVariation.kt
index 53dcf8b..0537796c 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontVariation.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontVariation.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.text.font
 
 import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.internal.requirePrecondition
+import androidx.compose.ui.text.internal.requirePreconditionNotNull
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.util.fastAny
@@ -130,7 +132,7 @@
         override fun toVariationValue(density: Density?): Float {
             // we don't care about pixel density as 12sp is the same "visual" size on all devices
             // instead we only care about font scaling, which changes visual size
-            requireNotNull(density) { "density must not be null" }
+            requirePreconditionNotNull(density) { "density must not be null" }
             return value.value * density.fontScale
         }
 
@@ -206,7 +208,9 @@
      * @param value value for axis, not validated and directly passed to font
      */
     fun Setting(name: String, value: Float): Setting {
-        require(name.length == 4) { "Name must be exactly four characters. Actual: '$name'" }
+        requirePrecondition(name.length == 4) {
+            "Name must be exactly four characters. Actual: '$name'"
+        }
         return SettingFloat(name, value)
     }
 
@@ -226,7 +230,7 @@
      * @param value [0.0f, 1.0f]
      */
     fun italic(value: Float): Setting {
-        require(value in 0.0f..1.0f) { "'ital' must be in 0.0f..1.0f. Actual: $value" }
+        requirePrecondition(value in 0.0f..1.0f) { "'ital' must be in 0.0f..1.0f. Actual: $value" }
         return SettingFloat("ital", value)
     }
 
@@ -248,7 +252,7 @@
      * @param textSize font-size at the expected display, must be in sp
      */
     fun opticalSizing(textSize: TextUnit): Setting {
-        require(textSize.isSp) { "'opsz' must be provided in sp units" }
+        requirePrecondition(textSize.isSp) { "'opsz' must be provided in sp units" }
         return SettingTextUnit("opsz", textSize)
     }
 
@@ -263,7 +267,7 @@
      * @param value -90f to 90f, represents an angle
      */
     fun slant(value: Float): Setting {
-        require(value in -90f..90f) { "'slnt' must be in -90f..90f. Actual: $value" }
+        requirePrecondition(value in -90f..90f) { "'slnt' must be in -90f..90f. Actual: $value" }
         return SettingFloat("slnt", value)
     }
 
@@ -279,7 +283,7 @@
      * @param value > 0.0f represents the width
      */
     fun width(value: Float): Setting {
-        require(value > 0.0f) { "'wdth' must be strictly > 0.0f. Actual: $value" }
+        requirePrecondition(value > 0.0f) { "'wdth' must be strictly > 0.0f. Actual: $value" }
         return SettingFloat("wdth", value)
     }
 
@@ -304,7 +308,9 @@
      * @param value weight, in 1..1000
      */
     fun weight(value: Int): Setting {
-        require(value in 1..1000) { "'wght' value must be in [1, 1000]. Actual: $value" }
+        requirePrecondition(value in 1..1000) {
+            "'wght' value must be in [1, 1000]. Actual: $value"
+        }
         return SettingInt("wght", value)
     }
 
@@ -324,7 +330,7 @@
      * @param value grade, in -1000..1000
      */
     fun grade(value: Int): Setting {
-        require(value in -1000..1000) { "'GRAD' must be in -1000..1000" }
+        requirePrecondition(value in -1000..1000) { "'GRAD' must be in -1000..1000" }
         return SettingInt("GRAD", value)
     }
 
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontWeight.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontWeight.kt
index 20795b3..b3a8e29 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontWeight.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontWeight.kt
@@ -17,6 +17,7 @@
 
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
+import androidx.compose.ui.text.internal.requirePrecondition
 import androidx.compose.ui.util.lerp
 
 /**
@@ -74,7 +75,7 @@
     }
 
     init {
-        require(weight in 1..1000) {
+        requirePrecondition(weight in 1..1000) {
             "Font weight can be in range [1, 1000]. Current value: $weight"
         }
     }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
index 0034692..018da98 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
@@ -19,6 +19,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.findFollowingBreak
 import androidx.compose.ui.text.findPrecedingBreak
+import androidx.compose.ui.text.internal.requirePrecondition
 
 /**
  * [EditCommand] is a command representation for the platform IME API function calls. The commands
@@ -246,7 +247,7 @@
 class DeleteSurroundingTextCommand(val lengthBeforeCursor: Int, val lengthAfterCursor: Int) :
     EditCommand {
     init {
-        require(lengthBeforeCursor >= 0 && lengthAfterCursor >= 0) {
+        requirePrecondition(lengthBeforeCursor >= 0 && lengthAfterCursor >= 0) {
             "Expected lengthBeforeCursor and lengthAfterCursor to be non-negative, were " +
                 "$lengthBeforeCursor and $lengthAfterCursor respectively."
         }
@@ -305,7 +306,7 @@
     val lengthAfterCursor: Int
 ) : EditCommand {
     init {
-        require(lengthBeforeCursor >= 0 && lengthAfterCursor >= 0) {
+        requirePrecondition(lengthBeforeCursor >= 0 && lengthAfterCursor >= 0) {
             "Expected lengthBeforeCursor and lengthAfterCursor to be non-negative, were " +
                 "$lengthBeforeCursor and $lengthAfterCursor respectively."
         }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditingBuffer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditingBuffer.kt
index 4e1a985..26031c8 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditingBuffer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditingBuffer.kt
@@ -19,6 +19,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.internal.requirePrecondition
 
 /**
  * The editing buffer
@@ -46,14 +47,18 @@
     /** The inclusive selection start offset */
     internal var selectionStart = selection.min
         private set(value) {
-            require(value >= 0) { "Cannot set selectionStart to a negative value: $value" }
+            requirePrecondition(value >= 0) {
+                "Cannot set selectionStart to a negative value: $value"
+            }
             field = value
         }
 
     /** The exclusive selection end offset */
     internal var selectionEnd = selection.max
         private set(value) {
-            require(value >= 0) { "Cannot set selectionEnd to a negative value: $value" }
+            requirePrecondition(value >= 0) {
+                "Cannot set selectionEnd to a negative value: $value"
+            }
             field = value
         }
 
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/GapBuffer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/GapBuffer.kt
index 1275d48..8940352a 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/GapBuffer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/GapBuffer.kt
@@ -18,6 +18,7 @@
 
 import androidx.annotation.RestrictTo
 import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.internal.requirePrecondition
 
 /**
  * Like [toCharArray] but copies the entire source string. Workaround for compiler error when giving
@@ -238,10 +239,10 @@
      * @param text a text to replace
      */
     fun replace(start: Int, end: Int, text: String) {
-        require(start <= end) {
+        requirePrecondition(start <= end) {
             "start index must be less than or equal to end index: $start > $end"
         }
-        require(start >= 0) { "start must be non-negative, but was $start" }
+        requirePrecondition(start >= 0) { "start must be non-negative, but was $start" }
 
         val buffer = buffer
         if (buffer == null) { // First time to create gap buffer
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/MathUtils.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/MathUtils.kt
index 9f7a900..c4597f8 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/MathUtils.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/MathUtils.kt
@@ -16,17 +16,27 @@
 
 package androidx.compose.ui.text.input
 
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
 /** Adds [this] and [right], and if an overflow occurs returns result of [defaultValue]. */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
 internal inline fun Int.addExactOrElse(right: Int, defaultValue: () -> Int): Int {
+    contract { callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) }
     val result = this + right
-    // HD 2-12 Overflow iff both arguments have the opposite sign of the result
+    // HD 2-12 Overflow if both arguments have the opposite sign of the result
     return if (this xor result and (right xor result) < 0) defaultValue() else result
 }
 
 /** Subtracts [right] from [this], and if an overflow occurs returns result of [defaultValue]. */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
 internal inline fun Int.subtractExactOrElse(right: Int, defaultValue: () -> Int): Int {
+    contract { callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) }
     val result = this - right
-    // HD 2-12 Overflow iff the arguments have different signs and
+    // HD 2-12 Overflow if the arguments have different signs and
     // the sign of the result is different from the sign of x
     return if (this xor right and (this xor result) < 0) defaultValue() else result
 }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/internal/InlineClassHelper.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/internal/InlineClassHelper.kt
new file mode 100644
index 0000000..036f341d
--- /dev/null
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/internal/InlineClassHelper.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
+
+package androidx.compose.ui.text.internal
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+/**
+ * Throws an [IllegalStateException] with the specified [message]. This function is guaranteed to
+ * not be inlined, which reduces the amount of code generated at the call site. This code size
+ * reduction helps performance by not pre-caching instructions that will rarely/never be executed.
+ */
+internal fun throwIllegalStateException(message: String) {
+    throw IllegalStateException(message)
+}
+
+/**
+ * Throws an [IllegalStateException] with the specified [message]. This function is guaranteed to
+ * not be inlined, which reduces the amount of code generated at the call site. This code size
+ * reduction helps performance by not pre-caching instructions that will rarely/never be executed.
+ *
+ * This function returns [Nothing] to tell the compiler it's a terminating branch in the code,
+ * making it suitable for use in a `when` statement or when doing a `null` check to force a smart
+ * cast to non-null (see [checkPreconditionNotNull].
+ */
+internal fun throwIllegalStateExceptionForNullCheck(message: String): Nothing {
+    throw IllegalStateException(message)
+}
+
+/**
+ * Throws an [IllegalArgumentException] with the specified [message]. This function is guaranteed to
+ * not be inlined, which reduces the amount of code generated at the call site. This code size
+ * reduction helps performance by not pre-caching instructions that will rarely/never be executed.
+ */
+internal fun throwIllegalArgumentException(message: String) {
+    throw IllegalArgumentException(message)
+}
+
+/**
+ * Throws an [IllegalArgumentException] with the specified [message]. This function is guaranteed to
+ * not be inlined, which reduces the amount of code generated at the call site. This code size
+ * reduction helps performance by not pre-caching instructions that will rarely/never be executed.
+ *
+ * This function returns [Nothing] to tell the compiler it's a terminating branch in the code,
+ * making it suitable for use in a `when` statement or when doing a `null` check to force a smart
+ * cast to non-null (see [requirePreconditionNotNull].
+ */
+internal fun throwIllegalArgumentExceptionForNullCheck(message: String): Nothing {
+    throw IllegalArgumentException(message)
+}
+
+/**
+ * Like Kotlin's [check] but without the `toString()` call on the output of the lambda, and a
+ * non-inline throw. This implementation generates less code at the call site, which can help
+ * performance by not loading instructions that will rarely/never execute.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun checkPrecondition(value: Boolean, lazyMessage: () -> String) {
+    contract {
+        callsInPlace(lazyMessage, InvocationKind.AT_MOST_ONCE)
+        returns() implies value
+    }
+    if (!value) {
+        throwIllegalStateException(lazyMessage())
+    }
+}
+
+/**
+ * Like Kotlin's [checkNotNull] but without the `toString()` call on the output of the lambda, and a
+ * non-inline throw. This implementation generates less code at the call site, which can help
+ * performance by not loading instructions that will rarely/never execute.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T : Any> checkPreconditionNotNull(value: T?, lazyMessage: () -> String): T {
+    contract {
+        callsInPlace(lazyMessage, InvocationKind.AT_MOST_ONCE)
+        returns() implies (value != null)
+    }
+
+    if (value == null) {
+        throwIllegalStateExceptionForNullCheck(lazyMessage())
+    }
+
+    return value
+}
+
+/**
+ * Like Kotlin's [require] but without the `toString()` call on the output of the lambda, and a
+ * non-inline throw. This implementation generates less code at the call site, which can help
+ * performance by not loading instructions that will rarely/never execute.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class) // same opt-in as using Kotlin's require()
+internal inline fun requirePrecondition(value: Boolean, lazyMessage: () -> String) {
+    contract {
+        callsInPlace(lazyMessage, InvocationKind.AT_MOST_ONCE)
+        returns() implies value
+    }
+    if (!value) {
+        throwIllegalArgumentException(lazyMessage())
+    }
+}
+
+/**
+ * Like Kotlin's [requireNotNull] but without the `toString()` call on the output of the lambda, and
+ * a non-inline throw. This implementation generates less code at the call site, which can help
+ * performance by not loading instructions that will rarely/never execute.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T : Any> requirePreconditionNotNull(value: T?, lazyMessage: () -> String): T {
+    contract {
+        callsInPlace(lazyMessage, InvocationKind.AT_MOST_ONCE)
+        returns() implies (value != null)
+    }
+
+    if (value == null) {
+        throwIllegalArgumentExceptionForNullCheck(lazyMessage())
+    }
+
+    return value
+}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt
index 6cb71cf..f48e5f4 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/intl/LocaleList.kt
@@ -35,7 +35,7 @@
          * An empty instance of [LocaleList]. Usually used to reference a lack of explicit [Locale]
          * configuration.
          */
-        val Empty = LocaleList(emptyList())
+        val Empty = LocaleList(listOf())
 
         /** Returns Locale object which represents current locale */
         val current: LocaleList
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt
index 5e5af9d..414d048 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineHeightStyle.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.text.style
 
 import androidx.compose.ui.text.PlatformParagraphStyle
+import androidx.compose.ui.text.internal.checkPrecondition
 import kotlin.jvm.JvmInline
 
 /**
@@ -207,7 +208,7 @@
     value class Alignment constructor(internal val topRatio: Float) {
 
         init {
-            check(topRatio in 0f..1f || topRatio == -1f) {
+            checkPrecondition(topRatio in 0f..1f || topRatio == -1f) {
                 "topRatio should be in [0..1] range or -1"
             }
         }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextForegroundStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextForegroundStyle.kt
index 9cd6708..7b7878d 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextForegroundStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/TextForegroundStyle.kt
@@ -23,6 +23,7 @@
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.isSpecified
 import androidx.compose.ui.graphics.lerp as lerpColor
+import androidx.compose.ui.text.internal.requirePrecondition
 import androidx.compose.ui.text.lerpDiscrete
 import androidx.compose.ui.util.lerp
 import kotlin.jvm.JvmName
@@ -89,7 +90,7 @@
 
 private data class ColorStyle(val value: Color) : TextForegroundStyle {
     init {
-        require(value.isSpecified) {
+        requirePrecondition(value.isSpecified) {
             "ColorStyle value must be specified, use TextForegroundStyle.Unspecified instead."
         }
     }
diff --git a/compose/ui/ui-util/api/current.txt b/compose/ui/ui-util/api/current.txt
index e18cbf3..fbfa507 100644
--- a/compose/ui/ui-util/api/current.txt
+++ b/compose/ui/ui-util/api/current.txt
@@ -39,6 +39,7 @@
     method public static inline <T, K> java.util.List<T> fastDistinctBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends K> selector);
     method public static inline <T> java.util.List<T> fastFilter(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static <T> java.util.List<T> fastFilterNotNull(java.util.List<? extends T?>);
+    method public static inline <T, R> java.util.List<R> fastFilteredMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
     method public static inline <T> T fastFirst(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static inline <T> T? fastFirstOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static inline <T, R> java.util.List<R> fastFlatMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends java.lang.Iterable<? extends R>> transform);
diff --git a/compose/ui/ui-util/api/restricted_current.txt b/compose/ui/ui-util/api/restricted_current.txt
index be90ec4..3ef27e1 100644
--- a/compose/ui/ui-util/api/restricted_current.txt
+++ b/compose/ui/ui-util/api/restricted_current.txt
@@ -39,6 +39,7 @@
     method public static inline <T, K> java.util.List<T> fastDistinctBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends K> selector);
     method public static inline <T> java.util.List<T> fastFilter(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static <T> java.util.List<T> fastFilterNotNull(java.util.List<? extends T?>);
+    method public static inline <T, R> java.util.List<R> fastFilteredMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
     method public static inline <T> T fastFirst(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static inline <T> T? fastFirstOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method public static inline <T, R> java.util.List<R> fastFlatMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends java.lang.Iterable<? extends R>> transform);
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
index 889ba5c..ff8ae9b 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
@@ -234,6 +234,26 @@
 }
 
 /**
+ * Returns a list containing only elements matching the given [predicate], applying the given
+ * [transform] function to each element.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
+@OptIn(ExperimentalContracts::class)
+inline fun <T, R> List<T>.fastFilteredMap(predicate: (T) -> Boolean, transform: (T) -> R): List<R> {
+    contract {
+        callsInPlace(predicate)
+        callsInPlace(transform)
+    }
+    val target = ArrayList<R>(size)
+    fastForEach { if (predicate(it)) target += transform(it) }
+    return target
+}
+
+/**
  * Accumulates value starting with [initial] value and applying [operation] from left to right to
  * current accumulator value and each element.
  *
diff --git a/compose/ui/ui/proguard-rules.pro b/compose/ui/ui/proguard-rules.pro
index f5603a7..fa5b4c3 100644
--- a/compose/ui/ui/proguard-rules.pro
+++ b/compose/ui/ui/proguard-rules.pro
@@ -41,6 +41,9 @@
     # For methods returning Nothing
     static java.lang.Void throw*Exception(...);
     static java.lang.Void throw*ExceptionForNullCheck(...);
+    # For functions generating error messages
+    static java.lang.String exceptionMessage*(...);
+    java.lang.String exceptionMessage*(...);
 }
 
 # When pointer input modifier nodes are added dynamically and have the same keys (common when
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 4229de0..05d8c98 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -279,19 +279,8 @@
      * then [instance] will become [attach]ed also. [instance] must have a `null` [parent].
      */
     internal fun insertAt(index: Int, instance: LayoutNode) {
-        checkPrecondition(instance._foldedParent == null) {
-            "Cannot insert $instance because it already has a parent." +
-                " This tree: " +
-                debugTreeToString() +
-                " Other tree: " +
-                instance._foldedParent?.debugTreeToString()
-        }
-        checkPrecondition(instance.owner == null) {
-            "Cannot insert $instance because it already has an owner." +
-                " This tree: " +
-                debugTreeToString() +
-                " Other tree: " +
-                instance.debugTreeToString()
+        checkPrecondition(instance._foldedParent == null || instance.owner == null) {
+            exceptionMessageForParentingOrOwnership(instance)
         }
 
         if (DebugChanges) {
@@ -317,6 +306,13 @@
         }
     }
 
+    private fun exceptionMessageForParentingOrOwnership(instance: LayoutNode) =
+        "Cannot insert $instance because it already has a parent or an owner." +
+            " This tree: " +
+            debugTreeToString() +
+            " Other tree: " +
+            instance._foldedParent?.debugTreeToString()
+
     internal fun onZSortedChildrenInvalidated() {
         if (isVirtual) {
             parent?.onZSortedChildrenInvalidated()
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialRequestJavaTest.java
new file mode 100644
index 0000000..fcaaada
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialRequestJavaTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreateRestoreCredentialRequestJavaTest {
+    private static final String TEST_USERNAME = "[email protected]";
+    private static final String TEST_USER_DISPLAYNAME = "Test User";
+    private static final String TEST_REQUEST_JSON = String.format("{\"rp\":{\"name\":true,"
+                    + "\"id\":\"app-id\"},\"user\":{\"name\":\"%s\",\"id\":\"id-value\","
+                    + "\"displayName\":\"%s\",\"icon\":true}, \"challenge\":true,"
+                    + "\"pubKeyCredParams\":true,\"excludeCredentials\":true,"
+                    + "\"attestation\":true}", TEST_USERNAME,
+            TEST_USER_DISPLAYNAME);
+
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+    @Test
+    public void constructor_emptyJson_throwsIllegalArgumentException() {
+        assertThrows("Expected empty Json to throw error",
+                IllegalArgumentException.class,
+                () -> new CreateRestoreCredentialRequest("")
+        );
+    }
+
+    @Test
+    public void constructor_invalidJson_throwsIllegalArgumentException() {
+        assertThrows("Expected empty Json to throw error",
+                IllegalArgumentException.class,
+                () -> new CreateRestoreCredentialRequest("invalid")
+        );
+    }
+
+    @Test
+    public void constructor_jsonMissingUserName_throwsIllegalArgumentException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new CreateRestoreCredentialRequest(
+                        "{\"key\":{\"value\":{\"lol\":\"Value\"}}}"
+                )
+        );
+    }
+
+    @Test
+    public void constructor_nullJson_throwsNullPointerException() {
+        assertThrows("Expected null Json to throw NPE",
+                NullPointerException.class,
+                () -> new CreateRestoreCredentialRequest(null)
+        );
+    }
+
+    @Test
+    public void constructor_success() {
+        new CreateRestoreCredentialRequest(TEST_REQUEST_JSON);
+    }
+
+    @Test
+    public void constructor_setsIsCloudBackupEnabledByDefault() {
+        CreateRestoreCredentialRequest createRestoreCredentialRequest =
+                new CreateRestoreCredentialRequest(TEST_REQUEST_JSON);
+
+        assertThat(createRestoreCredentialRequest.isCloudBackupEnabled()).isTrue();
+    }
+
+    @Test
+    public void getter_requestJson_success() {
+        String testJsonExpected = TEST_REQUEST_JSON;
+        CreateRestoreCredentialRequest createRestoreCredentialRequest =
+                new CreateRestoreCredentialRequest(testJsonExpected);
+
+        String testJsonActual = createRestoreCredentialRequest.getRequestJson();
+        assertThat(testJsonActual).isEqualTo(testJsonExpected);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialRequestTest.kt
new file mode 100644
index 0000000..e55acd4
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialRequestTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreateRestoreCredentialRequestTest {
+    companion object Constant {
+        private const val TEST_USERNAME = "[email protected]"
+        private const val TEST_USER_DISPLAYNAME = "Test User"
+        private const val TEST_REQUEST_JSON =
+            "{\"rp\":{\"name\":true,\"id\":\"app-id\"}," +
+                "\"user\":{\"name\":\"$TEST_USERNAME\",\"id\":\"id-value\",\"displayName" +
+                "\":\"$TEST_USER_DISPLAYNAME\",\"icon\":true}, \"challenge\":true," +
+                "\"pubKeyCredParams\":true,\"excludeCredentials\":true," +
+                "\"attestation\":true}"
+    }
+
+    @Test
+    fun constructor_emptyJson_throwsIllegalArgumentException() {
+        assertThrows("Expected empty Json to throw error", IllegalArgumentException::class.java) {
+            CreateRestoreCredentialRequest("")
+        }
+    }
+
+    @Test
+    fun constructor_invalidJson_throwsIllegalArgumentException() {
+        assertThrows("Expected empty Json to throw error", IllegalArgumentException::class.java) {
+            CreateRestoreCredentialRequest("invalid")
+        }
+    }
+
+    @Test
+    fun constructor_jsonMissingUserName_throwsIllegalArgumentException() {
+        assertThrows(IllegalArgumentException::class.java) {
+            CreateRestoreCredentialRequest("{\"hey\":{\"hi\":{\"hello\":\"hii\"}}}")
+        }
+    }
+
+    @Test
+    fun constructor_setsIsCloudBackupEnabledByDefault() {
+        val createRestoreCredentialRequest = CreateRestoreCredentialRequest(TEST_REQUEST_JSON)
+
+        assertThat(createRestoreCredentialRequest.isCloudBackupEnabled).isTrue()
+    }
+
+    @Test
+    fun constructor_setsIsCloudBackupEnabledToFalse() {
+        val createRestoreCredentialRequest =
+            CreateRestoreCredentialRequest(TEST_REQUEST_JSON, /* isCloudBackupEnabled= */ false)
+
+        assertThat(createRestoreCredentialRequest.isCloudBackupEnabled).isFalse()
+    }
+
+    @Test
+    fun getter_requestJson_success() {
+        val createRestoreCredentialRequest = CreateRestoreCredentialRequest(TEST_REQUEST_JSON)
+
+        assertThat(createRestoreCredentialRequest.requestJson).isEqualTo(TEST_REQUEST_JSON)
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialResponseJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialResponseJavaTest.java
new file mode 100644
index 0000000..e58f803
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialResponseJavaTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreateRestoreCredentialResponseJavaTest {
+    private static final String TEST_RESPONSE_JSON = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+
+    @Test
+    public void constructor_emptyJson_throwsIllegalArgumentException() {
+        assertThrows("Expected empty Json to throw error",
+                IllegalArgumentException.class,
+                () -> new CreateRestoreCredentialResponse("")
+        );
+    }
+
+    @Test
+    public void constructor_nullJson_throwsNullPointerException() {
+        assertThrows("Expected null Json to throw NullPointerException",
+                NullPointerException.class,
+                () -> new CreateRestoreCredentialResponse(null)
+        );
+    }
+
+    @Test
+    public void constructor_success()  {
+        new CreateRestoreCredentialResponse(TEST_RESPONSE_JSON);
+    }
+
+    @Test
+    public void getter_registrationResponseJson_success() {
+        String testJsonExpected = "{\"input\":5}";
+        CreateRestoreCredentialResponse createRestoreCredentialResponse =
+                new CreateRestoreCredentialResponse(testJsonExpected);
+        String testJsonActual = createRestoreCredentialResponse.getResponseJson();
+        assertThat(testJsonActual).isEqualTo(testJsonExpected);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialResponseTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialResponseTest.kt
new file mode 100644
index 0000000..ad1d318
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreateRestoreCredentialResponseTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreateRestoreCredentialResponseTest {
+
+    companion object Constant {
+        private const val TEST_RESPONSE_JSON = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+    }
+
+    @Test
+    fun constructor_emptyJson_throwsIllegalArgumentException() {
+        Assert.assertThrows(
+            "Expected empty Json to throw error",
+            IllegalArgumentException::class.java
+        ) {
+            CreateRestoreCredentialResponse("")
+        }
+    }
+
+    @Test
+    fun constructor_invalidJson_throwsIllegalArgumentException() {
+        Assert.assertThrows(
+            "Expected empty Json to throw error",
+            IllegalArgumentException::class.java
+        ) {
+            CreateRestoreCredentialResponse("invalid")
+        }
+    }
+
+    @Test
+    fun constructor_success() {
+        CreateRestoreCredentialResponse(TEST_RESPONSE_JSON)
+    }
+
+    @Test
+    fun getter_registrationResponseJson_success() {
+        val testJsonExpected = "{\"input\":5}"
+        val createRestoreCredentialResponse = CreateRestoreCredentialResponse(testJsonExpected)
+        val testJsonActual = createRestoreCredentialResponse.responseJson
+        assertThat(testJsonActual).isEqualTo(testJsonExpected)
+    }
+
+    @Test
+    fun getter_frameworkProperties_success() {
+        val registrationResponseJsonExpected = "{\"input\":5}"
+        val expectedData = Bundle()
+        expectedData.putString(
+            CreateRestoreCredentialResponse.BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE,
+            registrationResponseJsonExpected
+        )
+
+        val response = CreateRestoreCredentialResponse(registrationResponseJsonExpected)
+
+        assertThat(response.type).isEqualTo(RestoreCredential.TYPE_RESTORE_CREDENTIAL)
+        assertThat(equals(response.data, expectedData)).isTrue()
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialProviderFactoryTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialProviderFactoryTest.kt
index beb4307..30a90a7 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialProviderFactoryTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialProviderFactoryTest.kt
@@ -16,6 +16,7 @@
 package androidx.credentials
 
 import android.os.Build
+import androidx.credentials.ClearCredentialStateRequest.Companion.TYPE_CLEAR_RESTORE_CREDENTIAL
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
@@ -29,6 +30,17 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class CredentialProviderFactoryTest {
+    companion object {
+        private const val TEST_USERNAME = "[email protected]"
+        private const val TEST_USER_DISPLAYNAME = "Test User"
+        private const val TEST_REQUEST_JSON =
+            "{\"rp\":{\"name\":true,\"id\":\"app-id\"}," +
+                "\"user\":{\"name\":\"$TEST_USERNAME\",\"id\":\"id-value\",\"displayName" +
+                "\":\"$TEST_USER_DISPLAYNAME\",\"icon\":true}, \"challenge\":true," +
+                "\"pubKeyCredParams\":true,\"excludeCredentials\":true," +
+                "\"attestation\":true}"
+    }
+
     private val context = InstrumentationRegistry.getInstrumentation().context
 
     private lateinit var credentialProviderFactory: CredentialProviderFactory
@@ -126,6 +138,35 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 34)
+    fun getBestAvailableProvider_postU_restoreCredential_returnsPreU() {
+        if (Build.VERSION.SDK_INT <= 33) {
+            return
+        }
+        clearState()
+        val expectedProvider = FakeProvider(success = true)
+        credentialProviderFactory.testMode = true
+        credentialProviderFactory.testPreUProvider = expectedProvider
+
+        val credentialOptions = ArrayList<CredentialOption>()
+        credentialOptions.add(GetRestoreCredentialOption(TEST_REQUEST_JSON))
+        val request = GetCredentialRequest(credentialOptions)
+
+        assertThat(
+                credentialProviderFactory.getBestAvailableProvider(TYPE_CLEAR_RESTORE_CREDENTIAL)
+            )
+            .isEqualTo(expectedProvider)
+        assertThat(
+                credentialProviderFactory.getBestAvailableProvider(
+                    CreateRestoreCredentialRequest(TEST_REQUEST_JSON)
+                )
+            )
+            .isEqualTo(expectedProvider)
+        assertThat(credentialProviderFactory.getBestAvailableProvider(request))
+            .isEqualTo(expectedProvider)
+    }
+
+    @Test
     @SdkSuppress(maxSdkVersion = 33)
     fun getBestAvailableProvider_preU_success() {
         if (Build.VERSION.SDK_INT >= 34) {
@@ -169,4 +210,33 @@
 
         assertNull(credentialProviderFactory.getBestAvailableProvider())
     }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33)
+    fun getBestAvailableProvider_preU_restoreCredential_returnsPreU() {
+        if (Build.VERSION.SDK_INT >= 34) {
+            return
+        }
+        clearState()
+        val expectedProvider = FakeProvider(success = true)
+        credentialProviderFactory.testMode = true
+        credentialProviderFactory.testPreUProvider = expectedProvider
+
+        val credentialOptions = ArrayList<CredentialOption>()
+        credentialOptions.add(GetRestoreCredentialOption(TEST_REQUEST_JSON))
+        val request = GetCredentialRequest(credentialOptions)
+
+        assertThat(
+                credentialProviderFactory.getBestAvailableProvider(TYPE_CLEAR_RESTORE_CREDENTIAL)
+            )
+            .isEqualTo(expectedProvider)
+        assertThat(
+                credentialProviderFactory.getBestAvailableProvider(
+                    CreateRestoreCredentialRequest(TEST_REQUEST_JSON)
+                )
+            )
+            .isEqualTo(expectedProvider)
+        assertThat(credentialProviderFactory.getBestAvailableProvider(request))
+            .isEqualTo(expectedProvider)
+    }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
index 65961ef..a1ab836 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetCredentialRequestTest.kt
@@ -27,6 +27,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalDigitalCredentialApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class GetCredentialRequestTest {
@@ -40,6 +41,41 @@
     }
 
     @Test
+    fun constructor_mixedUseOfGetRestoreCredentialOption_throws() {
+        assertThrows(IllegalArgumentException::class.java) {
+            val credentialOptions = ArrayList<CredentialOption>()
+            credentialOptions.add(GetRestoreCredentialOption(TEST_JSON))
+            credentialOptions.add(GetPasswordOption())
+            GetCredentialRequest(credentialOptions)
+        }
+    }
+
+    @Test
+    fun constructor_singleUseOfGetRestoreCredentialOption_doesNotThrow() {
+        val credentialOptions = ArrayList<CredentialOption>()
+        credentialOptions.add(GetRestoreCredentialOption(TEST_JSON))
+        GetCredentialRequest(credentialOptions)
+    }
+
+    @Test
+    fun constructor_mixedUseOfDigitalCredentialOption_throws() {
+        assertThrows(IllegalArgumentException::class.java) {
+            val credentialOptions = ArrayList<CredentialOption>()
+            credentialOptions.add(GetDigitalCredentialOption(TEST_JSON))
+            credentialOptions.add(GetPasswordOption())
+            GetCredentialRequest(credentialOptions)
+        }
+    }
+
+    @Test
+    fun constructor_singleUseOfDigitalCredentialOption_doesNotThrow() {
+        val credentialOptions = ArrayList<CredentialOption>()
+        credentialOptions.add(GetDigitalCredentialOption(TEST_JSON))
+        credentialOptions.add(GetDigitalCredentialOption(TEST_JSON))
+        GetCredentialRequest(credentialOptions)
+    }
+
+    @Test
     fun constructor() {
         val expectedCredentialOptions = ArrayList<CredentialOption>()
         expectedCredentialOptions.add(GetPasswordOption())
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetRestoreCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetRestoreCredentialOptionJavaTest.java
new file mode 100644
index 0000000..9b6d76f
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetRestoreCredentialOptionJavaTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GetRestoreCredentialOptionJavaTest {
+    private static final String TEST_REQUEST_JSON = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+    private static final int EXPECTED_PASSKEY_PRIORITY =
+            CredentialOption.PRIORITY_PASSKEY_OR_SIMILAR;
+
+
+    @Test
+    public void constructor_emptyJson_throwsIllegalArgumentException() {
+        assertThrows("Expected empty Json to throw error",
+                IllegalArgumentException.class,
+                () -> new GetRestoreCredentialOption("")
+        );
+    }
+
+    @Test
+    public void constructor_nullJson_throwsNullPointerException() {
+        assertThrows("Expected null Json to throw NPE",
+                NullPointerException.class,
+                () -> new GetRestoreCredentialOption(null)
+        );
+    }
+
+    @Test
+    public void constructor_success() {
+        new GetRestoreCredentialOption(TEST_REQUEST_JSON);
+    }
+
+    @Test
+    public void getter_requestJson_success() {
+        String testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        GetRestoreCredentialOption getRestoreCredentialOption =
+                new GetRestoreCredentialOption(testJsonExpected);
+        String testJsonActual = getRestoreCredentialOption.getRequestJson();
+        assertThat(testJsonActual).isEqualTo(testJsonExpected);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetRestoreCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetRestoreCredentialOptionTest.kt
new file mode 100644
index 0000000..626b125
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetRestoreCredentialOptionTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GetRestoreCredentialOptionTest {
+
+    @Test
+    fun constructor_emptyJson_throwsIllegalArgumentException() {
+        Assert.assertThrows(
+            "Expected empty Json to throw error",
+            IllegalArgumentException::class.java
+        ) {
+            GetRestoreCredentialOption("")
+        }
+    }
+
+    @Test
+    fun constructor_success() {
+        GetRestoreCredentialOption(TEST_REQUEST_JSON)
+    }
+
+    @Test
+    fun getter_requestJson_success() {
+        val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val getRestoreCredentialOption = GetRestoreCredentialOption(testJsonExpected)
+        val testJsonActual = getRestoreCredentialOption.requestJson
+        assertThat(testJsonActual).isEqualTo(testJsonExpected)
+    }
+
+    companion object Constant {
+        private const val TEST_REQUEST_JSON = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/RestoreCredentialJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/RestoreCredentialJavaTest.java
new file mode 100644
index 0000000..b7e7ae9
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/RestoreCredentialJavaTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RestoreCredentialJavaTest {
+    private static final String TEST_JSON = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+
+    @Test
+    public void typeConstant() {
+        assertThat(RestoreCredential.TYPE_RESTORE_CREDENTIAL)
+                .isEqualTo("androidx.credentials.TYPE_RESTORE_CREDENTIAL");
+    }
+
+    @Test
+    public void constructor_emptyJson_throwsIllegalArgumentException() {
+        assertThrows("Expected empty Json to throw IllegalArgumentException",
+                IllegalArgumentException.class,
+                () -> {
+                    Bundle bundle = new Bundle();
+                    bundle.putString(
+                            "androidx.credentials.BUNDLE_KEY_GET_RESTORE_CREDENTIAL_RESPONSE",
+                            ""
+                    );
+                    Credential.createFrom(RestoreCredential.TYPE_RESTORE_CREDENTIAL, bundle);
+                }
+        );
+    }
+
+    @Test
+    public void constructor_success() {
+        Bundle bundle = new Bundle();
+        bundle.putString(
+                "androidx.credentials.BUNDLE_KEY_GET_RESTORE_CREDENTIAL_RESPONSE",
+                TEST_JSON
+        );
+        Credential.createFrom(RestoreCredential.TYPE_RESTORE_CREDENTIAL, bundle);
+    }
+
+    @Test
+    public void getter_authJson_success() {
+        String testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        Bundle bundle = new Bundle();
+        bundle.putString(
+                "androidx.credentials.BUNDLE_KEY_GET_RESTORE_CREDENTIAL_RESPONSE",
+                testJsonExpected
+        );
+        RestoreCredential credential = (RestoreCredential) Credential
+                .createFrom(RestoreCredential.TYPE_RESTORE_CREDENTIAL, bundle);
+        String testJsonActual = credential.getAuthenticationResponseJson();
+        assertThat(testJsonActual).isEqualTo(testJsonExpected);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/RestoreCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/RestoreCredentialTest.kt
new file mode 100644
index 0000000..b1fb4dd
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/RestoreCredentialTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials
+
+import android.os.Bundle
+import androidx.credentials.Credential.Companion.createFrom
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class RestoreCredentialTest {
+
+    companion object Constant {
+        private const val TEST_JSON = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+    }
+
+    @Test
+    fun typeConstant() {
+        assertThat(RestoreCredential.TYPE_RESTORE_CREDENTIAL)
+            .isEqualTo("androidx.credentials.TYPE_RESTORE_CREDENTIAL")
+    }
+
+    @Test
+    fun createFrom_emptyJson_throwsIllegalArgumentException() {
+        Assert.assertThrows(
+            "Expected empty Json to throw IllegalArgumentException",
+            IllegalArgumentException::class.java
+        ) {
+            val bundle = Bundle()
+            bundle.putString("androidx.credentials.BUNDLE_KEY_GET_RESTORE_CREDENTIAL_RESPONSE", "")
+            RestoreCredential.createFrom(bundle)
+        }
+    }
+
+    @Test
+    fun createFrom_invalidJson_throwsIllegalArgumentException() {
+        Assert.assertThrows(
+            "Expected invalid Json to throw IllegalArgumentException",
+            IllegalArgumentException::class.java
+        ) {
+            val bundle = Bundle()
+            bundle.putString(
+                "androidx.credentials.BUNDLE_KEY_GET_RESTORE_CREDENTIAL_RESPONSE",
+                "invalid"
+            )
+            RestoreCredential.createFrom(bundle)
+        }
+    }
+
+    @Test
+    fun createFrom_success() {
+        val bundle = Bundle()
+        bundle.putString(
+            "androidx.credentials.BUNDLE_KEY_GET_RESTORE_CREDENTIAL_RESPONSE",
+            TEST_JSON
+        )
+        RestoreCredential.createFrom(bundle)
+    }
+
+    @Test
+    fun getter_authJson_success() {
+        val testJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val bundle = Bundle()
+        bundle.putString(
+            "androidx.credentials.BUNDLE_KEY_GET_RESTORE_CREDENTIAL_RESPONSE",
+            testJsonExpected
+        )
+        val restoreCredential = RestoreCredential.createFrom(bundle)
+        val testJsonActual = restoreCredential.authenticationResponseJson
+        assertThat(testJsonActual).isEqualTo(testJsonExpected)
+    }
+}
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialResponse.kt
index a2ed952..8d26574 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialResponse.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateRestoreCredentialResponse.kt
@@ -19,6 +19,7 @@
 import android.os.Bundle
 import androidx.annotation.RestrictTo
 import androidx.credentials.exceptions.CreateCredentialUnknownException
+import androidx.credentials.internal.RequestValidationHelper
 
 /**
  * A response of the [RestoreCredential] flow.
@@ -32,9 +33,19 @@
     data: Bundle,
 ) : CreateCredentialResponse(RestoreCredential.TYPE_RESTORE_CREDENTIAL, data) {
 
-    /** Constructs a [CreateRestoreCredentialResponse]. */
+    /**
+     * Constructs a [CreateRestoreCredentialResponse].
+     *
+     * @throws IllegalArgumentException If [responseJson] is empty, or an invalid JSON
+     */
     constructor(responseJson: String) : this(responseJson, toBundle(responseJson))
 
+    init {
+        require(RequestValidationHelper.isValidJSON(responseJson)) {
+            "registrationResponseJson must not be empty, and must be a valid JSON"
+        }
+    }
+
     companion object {
         const val BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE =
             "androidx.credentials.BUNDLE_KEY_CREATE_RESTORE_CREDENTIAL_RESPONSE"
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
index bf885cb..bad4fbf 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProviderFactory.kt
@@ -77,12 +77,6 @@
         } else if (request is GetCredentialRequest) {
             for (option in request.credentialOptions) {
                 if (option is GetRestoreCredentialOption || option is GetDigitalCredentialOption) {
-                    if (request.credentialOptions.any { it !is GetDigitalCredentialOption }) {
-                        throw IllegalArgumentException(
-                            "`GetDigitalCredentialOption` cannot be" +
-                                " combined with other option types in a single request"
-                        )
-                    }
                     return tryCreatePreUOemProvider()
                 }
             }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
index ef20a75..0c0133a 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialRequest.kt
@@ -62,6 +62,7 @@
  *   [GetRestoreCredentialOption] with another option (i.e. [GetPasswordOption] or
  *   [GetPublicKeyCredentialOption]).
  */
+@OptIn(ExperimentalDigitalCredentialApi::class)
 class GetCredentialRequest
 @JvmOverloads
 constructor(
@@ -76,6 +77,16 @@
     init {
         require(credentialOptions.isNotEmpty()) { "credentialOptions should not be empty" }
         if (credentialOptions.size > 1) {
+            val digitalCredentialOptionCount =
+                credentialOptions.count { it is GetDigitalCredentialOption }
+            if (
+                digitalCredentialOptionCount > 0 &&
+                    digitalCredentialOptionCount != credentialOptions.size
+            ) {
+                throw IllegalArgumentException(
+                    "Digital Credential Option cannot be used with other credential option."
+                )
+            }
             for (option in credentialOptions) {
                 if (option is GetRestoreCredentialOption) {
                     throw IllegalArgumentException(
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetRestoreCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetRestoreCredentialOption.kt
index feabf25..bcf4a90 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetRestoreCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetRestoreCredentialOption.kt
@@ -18,6 +18,7 @@
 
 import android.os.Bundle
 import androidx.credentials.exceptions.NoCredentialException
+import androidx.credentials.internal.RequestValidationHelper
 
 /**
  * A request to get the restore credential from the restore credential provider.
@@ -47,6 +48,13 @@
         allowedProviders = emptySet(),
         typePriorityHint = PRIORITY_DEFAULT,
     ) {
+
+    init {
+        require(RequestValidationHelper.isValidJSON(requestJson)) {
+            "requestJson must not be empty, and must be a valid JSON"
+        }
+    }
+
     private companion object {
         private const val BUNDLE_KEY_GET_RESTORE_CREDENTIAL_REQUEST =
             "androidx.credentials.BUNDLE_KEY_GET_RESTORE_CREDENTIAL_REQUEST"
diff --git a/libraryversions.toml b/libraryversions.toml
index 8a6f498..a098b34 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -162,10 +162,10 @@
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha02"
 WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
-WEAR_PROTOLAYOUT = "1.3.0-alpha01"
+WEAR_PROTOLAYOUT = "1.3.0-alpha02"
 WEAR_PROTOLAYOUT_MATERIAL3 = "1.0.0-alpha01"
 WEAR_REMOTE_INTERACTIONS = "1.1.0-rc01"
-WEAR_TILES = "1.5.0-alpha01"
+WEAR_TILES = "1.5.0-alpha02"
 WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
 WEAR_WATCHFACE = "1.3.0-alpha04"
 WEBKIT = "1.13.0-alpha01"
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index b50d0a2..d98f5f0 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -178,4 +178,5 @@
     inceptionYear = "2018"
     description = "Android Lifecycle ViewModel"
     legacyDisableKotlinStrictApiMode = true
+    metalavaK2UastEnabled = false // TODO(b/324624680)
 }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index 63d2579..c09b58d 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -402,6 +402,26 @@
                 .onUpdateMemberRoutes(Collections.singletonList(route.getDescriptorId()));
     }
 
+    /**
+     * Selects the given {@link MediaRouter.RouteInfo route}.
+     *
+     * <p>This method does two things:
+     *
+     * <ul>
+     *   <li>Updates the currently selected route ({@link #mSelectedRoute}).
+     *   <li>Notifies the {@link MediaRouteProvider} that published the selected route of said
+     *       selection. An exception to this is when {@code syncMediaRoute1Provider} is false, and
+     *       the provider of the route is {@link PlatformMediaRouter1RouteProvider}. This is to
+     *       prevent calling {@link android.media.MediaRouter#selectRoute} as a result of a {@link
+     *       android.media.MediaRouter} callback. See b/294968421#comment59 for details.
+     * </ul>
+     *
+     * @param route The {@link MediaRouter.RouteInfo} to select.
+     * @param unselectReason The reason associated with the route selection.
+     * @param syncMediaRoute1Provider Whether this selection should be passed through to {@link
+     *     PlatformMediaRouter1RouteProvider}. Must only be true when called as a result of an
+     *     explicit application route selection.
+     */
     /* package */ void selectRoute(
             @NonNull MediaRouter.RouteInfo route,
             @MediaRouter.UnselectReason int unselectReason,
@@ -1004,9 +1024,10 @@
                     mSelectedRoute != null
                             ? String.format(
                                     Locale.US,
-                                    "%s(BT=%b)",
+                                    "%s(BT=%b, syncMediaRoute1Provider=%b)",
                                     mSelectedRoute.getName(),
-                                    mSelectedRoute.isBluetooth())
+                                    mSelectedRoute.isBluetooth(),
+                                    syncMediaRoute1Provider)
                             : null;
             Log.w(
                     TAG,
@@ -1217,7 +1238,7 @@
         if (provider != null) {
             MediaRouter.RouteInfo route = provider.findRouteByDescriptorId(id);
             if (route != null) {
-                route.select();
+                route.select(/* syncMediaRoute1Provider= */ false);
             }
         }
     }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index 9765791..f4b39f0 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -1976,13 +1976,7 @@
             select(/* syncMediaRoute1Provider= */ true);
         }
 
-        /**
-         * Selects this media route.
-         *
-         * @param syncMediaRoute1Provider Whether this selection should be passed through to {@link
-         *     PlatformMediaRouter1RouteProvider}. Should be false when this call is the result of a
-         *     {@link MediaRouter.Callback#onRouteSelected} call.
-         */
+        /** See {@link GlobalMediaRouter#selectRoute}. */
         @RestrictTo(LIBRARY)
         @MainThread
         public void select(boolean syncMediaRoute1Provider) {
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
index 322240f..89d4178 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/PlatformMediaRouter1RouteProvider.java
@@ -382,7 +382,7 @@
                 if (index >= 0) {
                     SystemRouteRecord record = mSystemRouteRecords.get(index);
                     if (record.mRouteDescriptorId.equals(route.getDescriptorId())) {
-                        route.select();
+                        route.select(/* syncMediaRoute1Provider= */ false);
                     }
                 }
             }
diff --git a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/MockPdfViewerFragment.kt b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/MockPdfViewerFragment.kt
deleted file mode 100644
index 89a299d..0000000
--- a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/MockPdfViewerFragment.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.pdf
-
-import androidx.annotation.RestrictTo
-import androidx.pdf.idlingresource.PdfIdlingResource
-import androidx.pdf.viewer.fragment.PdfViewerFragment
-
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-internal class MockPdfViewerFragment : PdfViewerFragment() {
-    val pdfLoadingIdlingResource = PdfIdlingResource(PDF_LOAD_RESOURCE_NAME)
-
-    var documentLoaded = false
-    var documentError: Throwable? = null
-
-    override fun onLoadDocumentSuccess() {
-        documentLoaded = true
-        pdfLoadingIdlingResource.decrement()
-    }
-
-    override fun onLoadDocumentError(error: Throwable) {
-        documentError = error
-        pdfLoadingIdlingResource.decrement()
-    }
-
-    companion object {
-        private const val PDF_LOAD_RESOURCE_NAME = "PdfLoad"
-    }
-}
diff --git a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/PdfViewerFragmentTestSuite.kt b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/PdfViewerFragmentTestSuite.kt
index 1457b00..324d0cf 100644
--- a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/PdfViewerFragmentTestSuite.kt
+++ b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/PdfViewerFragmentTestSuite.kt
@@ -52,12 +52,12 @@
 @RunWith(AndroidJUnit4::class)
 class PdfViewerFragmentTestSuite {
 
-    private lateinit var scenario: FragmentScenario<MockPdfViewerFragment>
+    private lateinit var scenario: FragmentScenario<TestPdfViewerFragment>
 
     @Before
     fun setup() {
         scenario =
-            launchFragmentInContainer<MockPdfViewerFragment>(
+            launchFragmentInContainer<TestPdfViewerFragment>(
                 themeResId =
                     com.google.android.material.R.style.Theme_Material3_DayNight_NoActionBar,
                 initialState = Lifecycle.State.INITIALIZED
@@ -83,7 +83,7 @@
         filename: String,
         nextState: Lifecycle.State,
         orientation: Int
-    ): FragmentScenario<MockPdfViewerFragment> {
+    ): FragmentScenario<TestPdfViewerFragment> {
         val context = InstrumentationRegistry.getInstrumentation().context
         val inputStream = context.assets.open(filename)
 
@@ -179,6 +179,62 @@
             .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
     }
 
+    /**
+     * This test verifies the behavior of the Pdf viewer in immersive mode, specifically the
+     * visibility of the toolbox and the host app's search button.
+     */
+    @Test
+    fun testPdfViewerFragment_immersiveMode_toggleMenu() {
+        // Load a PDF document into the fragment
+        val scenario =
+            scenarioLoadDocument(
+                TEST_DOCUMENT_FILE,
+                Lifecycle.State.STARTED,
+                ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+            )
+
+        // Check that the document is loaded successfully
+        onView(withId(R.id.loadingView))
+            .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+        scenario.onFragment {
+            Preconditions.checkArgument(
+                it.documentLoaded,
+                "Unable to load document due to ${it.documentError?.message}"
+            )
+        }
+
+        // Show the toolbox and check visibility of buttons
+        scenario.onFragment { it.isToolboxVisible = true }
+        onView(withId(R.id.edit_fab)).check(matches(isDisplayed()))
+        onView(withId(androidx.pdf.testapp.R.id.host_Search)).check(matches(isDisplayed()))
+
+        // Hide the toolbox and check visibility of buttons
+        scenario.onFragment { it.isToolboxVisible = false }
+        onView(withId(R.id.edit_fab))
+            .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+        onView(withId(androidx.pdf.testapp.R.id.host_Search)).check(matches(isDisplayed()))
+
+        // Enter immersive mode and check visibility of buttons
+        scenario.onFragment { it.onRequestImmersiveMode(true) }
+        onView(withId(R.id.edit_fab))
+            .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+        onView(withId(androidx.pdf.testapp.R.id.host_Search))
+            .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+
+        // Exit immersive mode and check visibility of buttons
+        scenario.onFragment { it.onRequestImmersiveMode(false) }
+        onView(withId(R.id.edit_fab)).check(matches(isDisplayed()))
+        onView(withId(androidx.pdf.testapp.R.id.host_Search)).check(matches(isDisplayed()))
+
+        // Click the host app search button and check visibility of elements
+        onView(withId(androidx.pdf.testapp.R.id.host_Search)).perform(click())
+        onView(withId(R.id.search_container)).check(matches(isDisplayed()))
+        onView(withId(R.id.edit_fab))
+            .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+        onView(withId(androidx.pdf.testapp.R.id.host_Search))
+            .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
+    }
+
     fun testPdfViewerFragment_setDocumentUri_passwordProtected_portrait() {
         val scenario =
             scenarioLoadDocument(
diff --git a/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/TestPdfViewerFragment.kt b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/TestPdfViewerFragment.kt
new file mode 100644
index 0000000..1685383
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/androidTest/kotlin/androidx/pdf/TestPdfViewerFragment.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.annotation.RestrictTo
+import androidx.pdf.idlingresource.PdfIdlingResource
+import androidx.pdf.testapp.R
+import androidx.pdf.viewer.fragment.PdfViewerFragment
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+internal class TestPdfViewerFragment : PdfViewerFragment() {
+    private var hostView: FrameLayout? = null
+    private var search: FloatingActionButton? = null
+    val pdfLoadingIdlingResource = PdfIdlingResource(PDF_LOAD_RESOURCE_NAME)
+
+    var documentLoaded = false
+    var documentError: Throwable? = null
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        val view = super.onCreateView(inflater, container, savedInstanceState) as FrameLayout
+
+        // Inflate the custom layout for this fragment
+        hostView = inflater.inflate(R.layout.fragment_host, container, false) as FrameLayout
+        search = hostView?.findViewById(R.id.host_Search)
+
+        // Add the default PDF viewer to the custom layout
+        hostView?.addView(view)
+
+        // Show/hide the search button based on initial toolbox visibility
+        if (isToolboxVisible) search?.show() else search?.hide()
+
+        // Set up search button click listener
+        search?.setOnClickListener { isTextSearchActive = true }
+        return hostView
+    }
+
+    override fun onRequestImmersiveMode(enterImmersive: Boolean) {
+        super.onRequestImmersiveMode(enterImmersive)
+        if (!enterImmersive) search?.show() else search?.hide()
+    }
+
+    override fun onLoadDocumentSuccess() {
+        documentLoaded = true
+        pdfLoadingIdlingResource.decrement()
+    }
+
+    override fun onLoadDocumentError(error: Throwable) {
+        documentError = error
+        pdfLoadingIdlingResource.decrement()
+    }
+
+    companion object {
+        private const val PDF_LOAD_RESOURCE_NAME = "PdfLoad"
+    }
+}
diff --git a/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/HostFragment.kt b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/HostFragment.kt
new file mode 100644
index 0000000..ead82cf2
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/HostFragment.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.testapp
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.pdf.viewer.fragment.PdfViewerFragment
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+
+/**
+ * This fragment extends PdfViewerFragment to provide a custom layout and handle immersive mode. It
+ * adds a FloatingActionButton for search functionality and manages its visibility based on the
+ * immersive mode state.
+ */
+class HostFragment : PdfViewerFragment() {
+    private var hostView: FrameLayout? = null
+    private var search: FloatingActionButton? = null
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        val view = super.onCreateView(inflater, container, savedInstanceState) as FrameLayout
+
+        // Inflate the custom layout for this fragment.
+        hostView = inflater.inflate(R.layout.fragment_host, container, false) as FrameLayout
+        search = hostView?.findViewById(R.id.host_Search)
+
+        // Add the default PDF viewer to the custom layout
+        hostView?.addView(view)
+
+        // Show/hide the search button based on initial toolbox visibility
+        if (isToolboxVisible) search?.show() else search?.hide()
+
+        // Set up search button click listener
+        search?.setOnClickListener { isTextSearchActive = true }
+        return hostView
+    }
+
+    override fun onRequestImmersiveMode(enterImmersive: Boolean) {
+        super.onRequestImmersiveMode(enterImmersive)
+        if (!enterImmersive) search?.show() else search?.hide()
+    }
+}
diff --git a/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/MainActivity.kt b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/MainActivity.kt
index becbc76..25f23c3 100644
--- a/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/MainActivity.kt
+++ b/pdf/integration-tests/testapp/src/main/kotlin/androidx/pdf/testapp/MainActivity.kt
@@ -29,14 +29,13 @@
 import androidx.core.view.WindowInsetsCompat
 import androidx.fragment.app.FragmentManager
 import androidx.fragment.app.FragmentTransaction
-import androidx.pdf.viewer.fragment.PdfViewerFragment
 import com.google.android.material.button.MaterialButton
 
 @SuppressLint("RestrictedApiAndroidX")
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 class MainActivity : AppCompatActivity() {
 
-    private var pdfViewerFragment: PdfViewerFragment? = null
+    private var pdfViewerFragment: HostFragment? = null
     private var isPdfViewInitialized = false
 
     @VisibleForTesting
@@ -57,8 +56,7 @@
 
         if (pdfViewerFragment == null) {
             pdfViewerFragment =
-                supportFragmentManager.findFragmentByTag(PDF_VIEWER_FRAGMENT_TAG)
-                    as PdfViewerFragment?
+                supportFragmentManager.findFragmentByTag(PDF_VIEWER_FRAGMENT_TAG) as HostFragment?
         }
 
         val getContentButton: MaterialButton = findViewById(R.id.launch_button)
@@ -74,7 +72,7 @@
         val fragmentManager: FragmentManager = supportFragmentManager
 
         // Fragment initialization
-        pdfViewerFragment = PdfViewerFragment()
+        pdfViewerFragment = HostFragment()
         val transaction: FragmentTransaction = fragmentManager.beginTransaction()
 
         // Replace an existing fragment in a container with an instance of a new fragment
diff --git a/pdf/integration-tests/testapp/src/main/res/drawable/ic_action_search.xml b/pdf/integration-tests/testapp/src/main/res/drawable/ic_action_search.xml
new file mode 100644
index 0000000..876b264
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/res/drawable/ic_action_search.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="?attr/colorOnSecondaryContainer"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:pathData="M20.49,19l-5.73,-5.73C15.53,12.2 16,10.91 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.41,0 2.7,-0.47 3.77,-1.24L19,20.49 20.49,19zM5,9.5C5,7.01 7.01,5 9.5,5S14,7.01 14,9.5 11.99,14 9.5,14 5,11.99 5,9.5z"
+        android:fillColor="?attr/colorOnSecondaryContainer"/>
+</vector>
\ No newline at end of file
diff --git a/pdf/integration-tests/testapp/src/main/res/layout/fragment_host.xml b/pdf/integration-tests/testapp/src/main/res/layout/fragment_host.xml
new file mode 100644
index 0000000..7b7ffd0
--- /dev/null
+++ b/pdf/integration-tests/testapp/src/main/res/layout/fragment_host.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<FrameLayout 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=".HostFragment">
+
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/host_Search"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="80dp"
+        android:layout_marginEnd="16dp"
+        android:contentDescription="@string/action_edit"
+        android:layout_gravity="bottom|end"
+        android:visibility="gone"
+        android:src="@drawable/ic_action_search"
+        app:backgroundTint="?attr/colorSecondaryContainer"
+        app:tint="?attr/colorOnSecondaryContainer" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/pdf/pdf-viewer-fragment/api/current.txt b/pdf/pdf-viewer-fragment/api/current.txt
index 53ee770..752ae07 100644
--- a/pdf/pdf-viewer-fragment/api/current.txt
+++ b/pdf/pdf-viewer-fragment/api/current.txt
@@ -5,12 +5,16 @@
     ctor public PdfViewerFragment();
     method public final android.net.Uri? getDocumentUri();
     method public final boolean isTextSearchActive();
+    method public final boolean isToolboxVisible();
     method public void onLoadDocumentError(Throwable error);
     method public void onLoadDocumentSuccess();
+    method public void onRequestImmersiveMode(boolean enterImmersive);
     method public final void setDocumentUri(android.net.Uri?);
     method public final void setTextSearchActive(boolean);
+    method public final void setToolboxVisible(boolean);
     property public final android.net.Uri? documentUri;
     property public final boolean isTextSearchActive;
+    property public final boolean isToolboxVisible;
   }
 
 }
diff --git a/pdf/pdf-viewer-fragment/api/restricted_current.txt b/pdf/pdf-viewer-fragment/api/restricted_current.txt
index 53ee770..752ae07 100644
--- a/pdf/pdf-viewer-fragment/api/restricted_current.txt
+++ b/pdf/pdf-viewer-fragment/api/restricted_current.txt
@@ -5,12 +5,16 @@
     ctor public PdfViewerFragment();
     method public final android.net.Uri? getDocumentUri();
     method public final boolean isTextSearchActive();
+    method public final boolean isToolboxVisible();
     method public void onLoadDocumentError(Throwable error);
     method public void onLoadDocumentSuccess();
+    method public void onRequestImmersiveMode(boolean enterImmersive);
     method public final void setDocumentUri(android.net.Uri?);
     method public final void setTextSearchActive(boolean);
+    method public final void setToolboxVisible(boolean);
     property public final android.net.Uri? documentUri;
     property public final boolean isTextSearchActive;
+    property public final boolean isToolboxVisible;
   }
 
 }
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index ac0ae4c..9fabf61 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -52,6 +52,7 @@
 import androidx.pdf.util.Observables.ExposedValue
 import androidx.pdf.util.Preconditions
 import androidx.pdf.util.Uris
+import androidx.pdf.viewer.ImmersiveModeRequester
 import androidx.pdf.viewer.LayoutHandler
 import androidx.pdf.viewer.LoadingView
 import androidx.pdf.viewer.PageSelectionValueObserver
@@ -165,6 +166,13 @@
 
     private var mEventCallback: EventCallback? = null
 
+    private val mImmersiveModeRequester: ImmersiveModeRequester =
+        object : ImmersiveModeRequester {
+            override fun requestImmersiveModeChange(enterImmersive: Boolean) {
+                onRequestImmersiveMode(enterImmersive)
+            }
+        }
+
     /**
      * The URI of the PDF document to display defaulting to `null`.
      *
@@ -227,6 +235,34 @@
     }
 
     /**
+     * Indicates whether the toolbox should be visible.
+     *
+     * The host app can control this property to show/hide the toolbox based on its state and the
+     * `onRequestImmersiveMode` callback. The setter updates the UI elements within the fragment
+     * accordingly.
+     */
+    public var isToolboxVisible: Boolean
+        get() = arguments?.getBoolean(KEY_TOOLBOX_VISIBILITY) ?: true
+        set(value) {
+            (arguments ?: Bundle()).apply { putBoolean(KEY_TOOLBOX_VISIBILITY, value) }
+            if (value) annotationButton?.show() else annotationButton?.hide()
+        }
+
+    /**
+     * Called when the PDF view wants to enter or exit immersive mode based on user's interaction
+     * with the content. Apps would typically hide their top bar or other navigational interface
+     * when in immersive mode. The default implementation keeps toolbox visibility in sync with the
+     * enterImmersive mode. It is recommended that apps keep this behaviour by calling
+     * super.onRequestImmersiveMode while overriding this method.
+     *
+     * @param enterImmersive true to enter immersive mode, false to exit.
+     */
+    public open fun onRequestImmersiveMode(enterImmersive: Boolean) {
+        // Update toolbox visibility
+        isToolboxVisible = !enterImmersive
+    }
+
+    /**
      * Invoked when the document has been fully loaded, processed, and the initial pages are
      * displayed within the viewing area. This callback signifies that the document is ready for
      * user interaction.
@@ -262,6 +298,11 @@
     ): View? {
         super.onCreateView(inflater, container, savedInstanceState)
         this.container = container
+        if (!hasContents && delayedContentsAvailable == null) {
+            if (savedInstanceState != null) {
+                restoreContents(savedInstanceState)
+            }
+        }
 
         pdfViewer = inflater.inflate(R.layout.pdf_viewer_container, container, false) as FrameLayout
         pdfViewer?.isScrollContainer = true
@@ -308,9 +349,7 @@
                         // and no means to load it. The best way is to just kill the service which
                         // will restart on the next onStart.
                         pdfLoader?.disconnect()
-                        return@PdfLoaderCallbacksImpl true
                     }
-                    return@PdfLoaderCallbacksImpl false
                 },
                 onDocumentLoaded = {
                     documentLoaded = true
@@ -318,9 +357,9 @@
                     if (shouldRedrawOnDocumentLoaded) {
                         shouldRedrawOnDocumentLoaded = false
                     }
-                    annotationButton?.let { button ->
+                    annotationButton?.let {
                         if ((savedInstanceState == null) && isAnnotationIntentResolvable) {
-                            button.show()
+                            onRequestImmersiveMode(false)
                         }
                     }
                 },
@@ -351,12 +390,6 @@
             }
         }
 
-        if (!hasContents && delayedContentsAvailable == null) {
-            if (savedInstanceState != null) {
-                restoreContents(savedInstanceState)
-            }
-        }
-
         return pdfViewer
     }
 
@@ -405,6 +438,7 @@
         if (onScreen) {
             onExit()
         }
+        onScreen = false
         started = false
         super.onStop()
     }
@@ -541,7 +575,7 @@
                     isAnnotationIntentResolvable &&
                         state.getBoolean(KEY_ANNOTATION_BUTTON_VISIBILITY)
                 ) {
-                    annotationButton?.show()
+                    onRequestImmersiveMode(false)
                 }
             }
         }
@@ -613,7 +647,8 @@
                 zoomView!!,
                 selectionModel,
                 paginationModel!!,
-                layoutHandler!!
+                layoutHandler!!,
+                mImmersiveModeRequester
             )
         singleTapHandler!!.setAnnotationIntentResolvable(isAnnotationIntentResolvable)
 
@@ -655,7 +690,8 @@
                 findInFileView!!,
                 isAnnotationIntentResolvable,
                 selectionActionMode!!,
-                viewState
+                viewState,
+                mImmersiveModeRequester
             )
         zoomView?.zoomScroll()?.addObserver(zoomScrollObserver)
 
@@ -669,12 +705,12 @@
             )
         findInFileView!!.searchModel.selectedMatch().addObserver(selectedMatchObserver)
 
-        annotationButton?.let { findInFileView!!.setAnnotationButton(it) }
+        annotationButton?.let { findInFileView!!.setAnnotationButton(it, mImmersiveModeRequester) }
 
         fastScrollView?.setOnFastScrollActiveListener {
             annotationButton?.let { button ->
                 if (button.visibility == View.VISIBLE) {
-                    button.hide()
+                    onRequestImmersiveMode(true)
                 }
             }
         }
@@ -690,7 +726,6 @@
                 fileData?.let {
                     localUri = it.uri
                     postContentsAvailable(it)
-                    postEnter()
                 }
             } catch (e: Exception) {
                 // This can happen if the data is an instance of StreamOpenable, and the client
@@ -709,14 +744,14 @@
         }
         setAnnotationIntentResolvability()
         if (!isAnnotationIntentResolvable && annotationButton?.visibility == View.VISIBLE) {
-            annotationButton?.post { annotationButton?.hide() }
+            annotationButton?.post { onRequestImmersiveMode(true) }
         }
         if (
             isAnnotationIntentResolvable &&
                 annotationButton?.visibility != View.VISIBLE &&
                 findInFileView?.visibility != View.VISIBLE
         ) {
-            annotationButton?.post { annotationButton?.show() }
+            annotationButton?.post { onRequestImmersiveMode(false) }
         }
     }
 
@@ -738,7 +773,6 @@
 
         pdfLoaderCallbacks?.searchModel = null
 
-        pdfLoader?.disconnect()
         pdfLoader = null
         documentLoaded = false
     }
@@ -850,7 +884,7 @@
             }
         }
         if (localUri != null && localUri != fileUri) {
-            annotationButton?.hide()
+            onRequestImmersiveMode(true)
         }
         localUri = fileUri
     }
@@ -944,5 +978,6 @@
         private const val KEY_DOCUMENT_URI: String = "documentUri"
         private const val KEY_ANNOTATION_BUTTON_VISIBILITY = "isAnnotationVisible"
         private const val KEY_PENDING_DOCUMENT_LOAD = "pendingDocumentLoad"
+        private const val KEY_TOOLBOX_VISIBILITY = "isToolboxVisible"
     }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
index cddac28..05d927c 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/find/FindInFileView.java
@@ -45,6 +45,7 @@
 import androidx.pdf.util.CycleRange;
 import androidx.pdf.util.ObservableValue;
 import androidx.pdf.util.ObservableValue.ValueObserver;
+import androidx.pdf.viewer.ImmersiveModeRequester;
 import androidx.pdf.viewer.PaginatedView;
 import androidx.pdf.viewer.SearchModel;
 import androidx.pdf.viewer.SelectedMatch;
@@ -75,6 +76,7 @@
     private View mCloseButton;
     private FloatingActionButton mAnnotationButton;
     private PaginatedView mPaginatedView;
+    private ImmersiveModeRequester mImmersiveModeRequester;
 
     private FindInFileListener mFindInFileListener;
     private Runnable mOnClosedButtonCallback;
@@ -246,8 +248,10 @@
     }
 
     public void setAnnotationButton(
-            @NonNull FloatingActionButton annotationButton) {
+            @NonNull FloatingActionButton annotationButton,
+            @NonNull ImmersiveModeRequester immersiveModeRequester) {
         mAnnotationButton = annotationButton;
+        mImmersiveModeRequester = immersiveModeRequester;
     }
 
     public void setAnnotationIntentResolvable(
@@ -267,7 +271,7 @@
         if (visibility) {
             this.setVisibility(VISIBLE);
             if (mAnnotationButton != null && mAnnotationButton.getVisibility() == VISIBLE) {
-                mAnnotationButton.hide();
+                mImmersiveModeRequester.requestImmersiveModeChange(true);
             }
             setupFindInFileBtn();
             WindowCompat.getInsetsController(((Activity) getContext()).getWindow(), this)
@@ -306,7 +310,7 @@
         mCloseButton.setOnClickListener(view -> {
             resetFindInFile();
             if (mIsAnnotationIntentResolvable) {
-                mAnnotationButton.show();
+                mImmersiveModeRequester.requestImmersiveModeChange(false);
             }
         });
     }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ImmersiveModeRequester.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ImmersiveModeRequester.kt
new file mode 100644
index 0000000..f2f21e8
--- /dev/null
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ImmersiveModeRequester.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.pdf.viewer
+
+import androidx.annotation.RestrictTo
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ImmersiveModeRequester {
+    public fun requestImmersiveModeChange(enterImmersive: Boolean)
+}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index 027471d..95c6e4d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -556,9 +556,9 @@
     static class SavedState extends View.BaseSavedState {
         final PaginationModel mModel;
 
-        SavedState(Parcelable superState, PaginationModel model) {
-            super(superState);
-            mModel = model;
+        SavedState(Parcel source) {
+            super(source);
+            mModel = ParcelCompat.readParcelable(source, null, PaginationModel.class);
         }
 
         SavedState(Parcel source, ClassLoader loader) {
@@ -566,10 +566,34 @@
             mModel = ParcelCompat.readParcelable(source, loader, PaginationModel.class);
         }
 
+        SavedState(Parcelable superState, PaginationModel model) {
+            super(superState);
+            mModel = model;
+        }
+
         @Override
         public void writeToParcel(Parcel out, int flags) {
             super.writeToParcel(out, flags);
             out.writeParcelable(mModel, flags);
         }
+
+        public static final ClassLoaderCreator<SavedState> CREATOR =
+                new ClassLoaderCreator<SavedState>() {
+
+                    @Override
+                    public SavedState createFromParcel(Parcel in) {
+                        return new SavedState(in);
+                    }
+
+                    @Override
+                    public SavedState createFromParcel(Parcel source, ClassLoader loader) {
+                        return new SavedState(source, loader);
+                    }
+
+                    @Override
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                };
     }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
index bff9c8c..fac1129 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PdfViewer.java
@@ -182,6 +182,13 @@
 
     private EventCallback mEventCallback;
 
+    private final ImmersiveModeRequester mImmersiveModeRequester = new ImmersiveModeRequester() {
+        @Override
+        public void requestImmersiveModeChange(boolean enterImmersive) {
+            //TODO: remove this class
+        }
+    };
+
     public PdfViewer() {
         super(SELF_MANAGED_CONTENTS);
     }
@@ -284,7 +291,8 @@
         mSearchModel.query().addObserver(mSearchQueryObserver);
 
         mSingleTapHandler = new SingleTapHandler(getContext(), mAnnotationButton, mPaginatedView,
-                mFindInFileView, mZoomView, mSelectionModel, mPaginationModel, mLayoutHandler);
+                mFindInFileView, mZoomView, mSelectionModel, mPaginationModel, mLayoutHandler,
+                mImmersiveModeRequester);
         mPageViewFactory = new PageViewFactory(requireContext(), mPdfLoader,
                 mPaginatedView, mZoomView, mSingleTapHandler, mFindInFileView,
                 mEventCallback);
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
index 67cfb2d..2da4beb 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/SingleTapHandler.java
@@ -44,6 +44,7 @@
     private final PdfSelectionModel mPdfSelectionModel;
     private final PaginationModel mPaginationModel;
     private final LayoutHandler mLayoutHandler;
+    private final ImmersiveModeRequester mImmersiveModeRequester;
     private boolean mIsAnnotationIntentResolvable;
 
     public SingleTapHandler(@NonNull Context context,
@@ -53,7 +54,8 @@
             @NonNull ZoomView zoomView,
             @NonNull PdfSelectionModel pdfSelectionModel,
             @NonNull PaginationModel paginationModel,
-            @NonNull LayoutHandler layoutHandler) {
+            @NonNull LayoutHandler layoutHandler,
+            @NonNull ImmersiveModeRequester immersiveModeRequester) {
         mContext = context;
         mFloatingActionButton = floatingActionButton;
         mPaginatedView = paginatedView;
@@ -62,6 +64,7 @@
         mPdfSelectionModel = pdfSelectionModel;
         mPaginationModel = paginationModel;
         mLayoutHandler = layoutHandler;
+        mImmersiveModeRequester = immersiveModeRequester;
     }
 
     public void setAnnotationIntentResolvable(boolean annotationIntentResolvable) {
@@ -73,9 +76,9 @@
         if (mIsAnnotationIntentResolvable) {
             if (mFloatingActionButton.getVisibility() == View.GONE
                     && mFindInFileView.getVisibility() == GONE) {
-                mFloatingActionButton.show();
+                mImmersiveModeRequester.requestImmersiveModeChange(false);
             } else {
-                mFloatingActionButton.hide();
+                mImmersiveModeRequester.requestImmersiveModeChange(true);
             }
         }
 
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
index b266f91..c373b3c 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/ZoomScrollValueObserver.java
@@ -43,6 +43,7 @@
     private boolean mIsAnnotationIntentResolvable;
     private final SelectionActionMode mSelectionActionMode;
     private final ObservableValue<ViewState> mViewState;
+    private final ImmersiveModeRequester mImmersiveModeRequester;
 
     private boolean mIsPageScrollingUp;
 
@@ -51,7 +52,8 @@
             @NonNull LayoutHandler layoutHandler, @NonNull FloatingActionButton annotationButton,
             @NonNull FindInFileView findInFileView, boolean isAnnotationIntentResolvable,
             @NonNull SelectionActionMode selectionActionMode,
-            @NonNull ObservableValue<ViewState> viewState) {
+            @NonNull ObservableValue<ViewState> viewState,
+            @NonNull ImmersiveModeRequester immersiveModeRequester) {
         mZoomView = zoomView;
         mPaginatedView = paginatedView;
         mLayoutHandler = layoutHandler;
@@ -62,6 +64,7 @@
         mViewState = viewState;
         mAnnotationButtonHandler = new Handler(Looper.getMainLooper());
         mIsPageScrollingUp = false;
+        mImmersiveModeRequester = immersiveModeRequester;
     }
 
     @Override
@@ -93,18 +96,15 @@
 
             if (!isAnnotationButtonVisible() && position.scrollY == 0
                     && mFindInFileView.getVisibility() == View.GONE) {
-                mAnnotationButton.show();
+                mImmersiveModeRequester.requestImmersiveModeChange(false);
             } else if (isAnnotationButtonVisible() && mIsPageScrollingUp) {
                 clearAnnotationHandler();
                 return;
             }
             if (position.scrollY == oldPosition.scrollY) {
-                mAnnotationButtonHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        if (position.scrollY != 0) {
-                            mAnnotationButton.hide();
-                        }
+                mAnnotationButtonHandler.post(() -> {
+                    if (position.scrollY != 0) {
+                        mImmersiveModeRequester.requestImmersiveModeChange(true);
                     }
                 });
             }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
index a86c12b..df72681 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfConnection.java
@@ -74,11 +74,6 @@
         this.mOnConnectFailure = onConnectFailure;
     }
 
-    /** Checks if Connection to PdfDocumentService is established */
-    public boolean isConnected() {
-        return mConnected;
-    }
-
     /**
      * Returns a {@link PdfDocumentRemote} if the service is bound. It could be still initializing
      * (see {@link #setDocumentLoaded}).
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
index 4506163..cf49ad7 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoader.java
@@ -139,26 +139,7 @@
 
     /** Schedule task to load a PdfDocument. */
     public void reloadDocument() {
-        if (isConnected()) {
-            mExecutor.schedule(new LoadDocumentTask(mLoadedPassword));
-        } else {
-            /*
-            *  For password protected files we kill the service if the app goes into
-            *  background before the document is loaded hence here we just register a
-            *  task which will be executed once the service is reconnected onStart
-            */
-            mConnection.setOnConnectInitializer(
-                    () -> mExecutor.schedule(new LoadDocumentTask(mLoadedPassword)));
-            mConnection.setConnectionFailureHandler(
-                    () -> mCallbacks.documentNotLoaded(PdfStatus.NONE));
-        }
-    }
-
-    /**
-     * Check if PdfLoader is connected to PdfDocumentService
-     */
-    public boolean isConnected() {
-        return mConnection.isConnected();
+        mExecutor.schedule(new LoadDocumentTask(mLoadedPassword));
     }
 
     /**
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
index 2f5210b..7b2d1b0 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
@@ -67,7 +67,7 @@
     private var isTextSearchActive: Boolean,
     private var viewState: ExposedValue<ViewState>,
     private val fragmentContainerView: View?,
-    private val onRequestPassword: (Boolean) -> Boolean,
+    private val onRequestPassword: (Boolean) -> Unit,
     private val onDocumentLoaded: () -> Unit,
     private val onDocumentLoadFailure: (Throwable) -> Unit,
     private var eventCallback: EventCallback?,
@@ -148,39 +148,39 @@
 
     override fun requestPassword(incorrect: Boolean) {
         eventCallback?.onViewerReset()
-        if (onRequestPassword(onScreen)) return
+        onRequestPassword(onScreen)
 
         if (viewState.get() != ViewState.NO_VIEW) {
             var passwordDialog = currentPasswordDialog(fragmentManager)
             if (passwordDialog == null) {
                 passwordDialog = PdfPasswordDialog()
+                passwordDialog.setListener(
+                    object : PdfPasswordDialog.PasswordDialogEventsListener {
+                        override fun onPasswordTextChange(password: String) {
+                            pdfLoader?.applyPassword(password)
+                        }
+
+                        override fun onDialogCancelled() {
+                            val retryCallback = Runnable { requestPassword(false) }
+                            val snackbar =
+                                fragmentContainerView?.let {
+                                    Snackbar.make(
+                                        it,
+                                        R.string.password_not_entered,
+                                        Snackbar.LENGTH_INDEFINITE
+                                    )
+                                }
+                            val mResolveClickListener =
+                                View.OnClickListener { _: View? -> retryCallback.run() }
+                            snackbar?.setAction(R.string.retry_button_text, mResolveClickListener)
+                            snackbar?.show()
+                        }
+                    }
+                )
+
                 passwordDialog.show(fragmentManager, PASSWORD_DIALOG_TAG)
             }
 
-            passwordDialog.setListener(
-                object : PdfPasswordDialog.PasswordDialogEventsListener {
-                    override fun onPasswordTextChange(password: String) {
-                        pdfLoader?.applyPassword(password)
-                    }
-
-                    override fun onDialogCancelled() {
-                        val retryCallback = Runnable { requestPassword(false) }
-                        val snackbar =
-                            fragmentContainerView?.let {
-                                Snackbar.make(
-                                    it,
-                                    R.string.password_not_entered,
-                                    Snackbar.LENGTH_INDEFINITE
-                                )
-                            }
-                        val mResolveClickListener =
-                            View.OnClickListener { _: View? -> retryCallback.run() }
-                        snackbar?.setAction(R.string.retry_button_text, mResolveClickListener)
-                        snackbar?.show()
-                    }
-                }
-            )
-
             if (incorrect) {
                 passwordDialog.retry()
             }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java
index d887f00..a994a5d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/password/PasswordDialog.java
@@ -63,7 +63,6 @@
     private int mBlueColor;
     private int mTextErrorColor;
     private AlertDialog mPasswordDialog;
-    private static final String PASSWORD_INPUT = "password_input";
 
     private boolean mIncorrect;
     private boolean mFinishOnCancel;
@@ -93,11 +92,6 @@
         final EditText passwordField = (EditText) view.findViewById(R.id.password);
         setupPasswordField(passwordField);
 
-        if (savedInstanceState != null) {
-            String savedPassword = savedInstanceState.getString(PASSWORD_INPUT, "");
-            passwordField.setText(savedPassword);
-        }
-
         // Hijack the positive button to NOT dismiss the dialog immediately.
         dialog.setOnShowListener(
                 new OnShowListener() {
@@ -152,17 +146,6 @@
         return dialog;
     }
 
-    @Override
-    public void onSaveInstanceState(@NonNull Bundle outState) {
-        super.onSaveInstanceState(outState);
-        // Find the EditText by its ID
-        EditText editTextPassword = (EditText) mPasswordDialog.findViewById(R.id.password);
-        // Get the text from the EditText and store it in passwordInput
-        String passwordInput = editTextPassword.getText().toString();
-        // Save the input to the outState bundle
-        outState.putString(PASSWORD_INPUT, passwordInput);
-    }
-
     private void setupPasswordField(final EditText passwordField) {
         passwordField.setFocusable(true);
         passwordField.requestFocus();
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/find/FindInFileViewTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/find/FindInFileViewTest.java
index e100e41..1135636 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/find/FindInFileViewTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/find/FindInFileViewTest.java
@@ -26,6 +26,7 @@
 
 import android.os.Build;
 
+import androidx.pdf.viewer.ImmersiveModeRequester;
 import androidx.pdf.viewer.PaginatedView;
 import androidx.pdf.viewer.loader.PdfLoader;
 import androidx.test.core.app.ApplicationProvider;
@@ -58,6 +59,8 @@
     private PaginatedView mPaginatedView;
     @Mock
     private FloatingActionButton mAnnotationButton;
+    @Mock
+    private ImmersiveModeRequester mImmersiveModeRequester;
     private FindInFileView mFindInFileView;
     private AutoCloseable mOpenMocks;
 
@@ -67,7 +70,7 @@
         mFindInFileView = new FindInFileView(ApplicationProvider.getApplicationContext());
         mFindInFileView.setPdfLoader(mPdfLoader);
         mFindInFileView.setPaginatedView(mPaginatedView);
-        mFindInFileView.setAnnotationButton(mAnnotationButton);
+        mFindInFileView.setAnnotationButton(mAnnotationButton, mImmersiveModeRequester);
     }
 
     @After
diff --git a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
index 412b9c8..e9b5a39f 100644
--- a/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
+++ b/pdf/pdf-viewer/src/test/java/androidx/pdf/viewer/ZoomScrollValueObserverTest.java
@@ -26,13 +26,16 @@
 import android.os.Looper;
 import android.view.View;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.pdf.ViewState;
 import androidx.pdf.data.Range;
 import androidx.pdf.find.FindInFileView;
+import androidx.pdf.models.PageSelection;
+import androidx.pdf.models.SelectionBoundary;
 import androidx.pdf.select.SelectionActionMode;
 import androidx.pdf.util.ObservableValue;
 import androidx.pdf.util.Observables;
-import androidx.pdf.widget.FastScrollView;
 import androidx.pdf.widget.ZoomView;
 import androidx.test.filters.SmallTest;
 
@@ -65,14 +68,14 @@
     private final LayoutHandler mMockLayoutHandler = mock(LayoutHandler.class);
     private final FloatingActionButton mMockAnnotationButton = mock(FloatingActionButton.class);
     private final FindInFileView mMockFindInFileView = mock(FindInFileView.class);
-    private final FastScrollView mMockFastScrollView = mock(FastScrollView.class);
     private final PageRangeHandler mPageRangeHandler = mock(PageRangeHandler.class);
     private final SelectionActionMode mMockSelectionActionMode = mock(SelectionActionMode.class);
-
+    private final PdfSelectionModel mMockSelectionModel = mock(PdfSelectionModel.class);
+    private final PageSelection mMockPageSelection = mock(PageSelection.class);
+    private final ImmersiveModeRequester mMockImmersiveModeRequester = mock(
+            ImmersiveModeRequester.class);
     private boolean mIsAnnotationIntentResolvable;
     private ZoomView.ZoomScroll mNewPosition;
-    private ZoomView.ZoomScroll mOldPosition;
-
 
     @Before
     public void setUp() {
@@ -82,52 +85,54 @@
         when(mMockPaginatedView.getPageRangeHandler()).thenReturn(mPageRangeHandler);
         when(mMockPaginatedView.getModel()).thenReturn(mMockPaginationModel);
         when(mMockPaginationModel.isInitialized()).thenReturn(true);
-        when(mMockZoomView.getHeight()).thenReturn(100);
         when(mPageRangeHandler.computeVisibleRange(0, 1.0f, 100, false)).thenReturn(PAGE_RANGE);
-        when(mMockZoomView.getStableZoom()).thenReturn(1.0f);
-        when(mMockZoomView.getVisibleAreaInContentCoords()).thenReturn(RECT);
         when(mMockPaginatedView.createPageViewsForVisiblePageRange()).thenReturn(false);
         when(mPageRangeHandler.getVisiblePages()).thenReturn(PAGE_RANGE);
         when(mMockPaginationModel.isInitialized()).thenReturn(true);
         when(mMockPaginationModel.getSize()).thenReturn(1);
+        when(mMockPaginatedView.getSelectionModel()).thenReturn(mMockSelectionModel);
+        when(mMockSelectionModel.selection()).thenReturn(new ObservableValue<PageSelection>() {
+            @Nullable
+            @Override
+            public PageSelection get() {
+                return mMockPageSelection;
+            }
+
+            @NonNull
+            @Override
+            public Object addObserver(ValueObserver<PageSelection> observer) {
+                return 1;
+            }
+
+            @Override
+            public void removeObserver(@NonNull Object observerKey) {
+
+            }
+        });
+        when(mMockPageSelection.getStart()).thenReturn(new SelectionBoundary(0, 0, 0, false));
+        when(mMockPageSelection.getStop()).thenReturn(new SelectionBoundary(0, 100, 100, false));
+        when(mMockPaginatedView.getViewArea()).thenReturn(RECT);
+        when(mMockPaginationModel.getLookAtX(0, 0)).thenReturn(1);
+        when(mMockPaginationModel.getLookAtX(0, 100)).thenReturn(50);
+        when(mMockPaginationModel.getLookAtY(0, 0)).thenReturn(1);
+        when(mMockPaginationModel.getLookAtY(0, 100)).thenReturn(50);
     }
 
     @Test
-    public void onChange_loadPageAssets_stablePosition() {
+    public void onChange_stablePosition() {
         mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 0, true);
 
         ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
                 mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
                 mMockFindInFileView, mIsAnnotationIntentResolvable, mMockSelectionActionMode,
-                VIEW_STATE_EXPOSED_VALUE);
+                VIEW_STATE_EXPOSED_VALUE, mMockImmersiveModeRequester);
         zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
 
-        verify(mMockPaginatedView).setViewArea(RECT);
-        verify(mMockPaginatedView).refreshPageRangeInVisibleArea(mNewPosition, 100);
-        verify(mMockPaginatedView).handleGonePages(false);
-        verify(mMockPaginatedView).loadInvisibleNearPageRange(1.0f);
-        verify(mMockPaginatedView).refreshVisiblePages(false,
-                ViewState.NO_VIEW, 1.0f);
-        verify(mMockPaginatedView).handleGonePages(true);
-        verify(mMockLayoutHandler).maybeLayoutPages(100);
+        verify(mMockSelectionActionMode).resume();
     }
 
     @Test
-    public void onChange_loadPageAssets_stableZoom() {
-        mNewPosition = new ZoomView.ZoomScroll(2.0f, 0, 0, false);
-        when(mMockZoomView.getStableZoom()).thenReturn(2.0f);
-
-        ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
-                mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton, mMockFindInFileView,
-                mIsAnnotationIntentResolvable, mMockSelectionActionMode,
-                VIEW_STATE_EXPOSED_VALUE);
-        zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
-
-        verify(mMockPaginatedView).refreshVisibleTiles(false, ViewState.NO_VIEW);
-    }
-
-    @Test
-    public void onChange_showAnnotationButton() {
+    public void onChange_exitImmersiveMode() {
         mIsAnnotationIntentResolvable = true;
         when(mMockAnnotationButton.getVisibility()).thenReturn(View.GONE);
         when(mMockFindInFileView.getVisibility()).thenReturn(View.GONE);
@@ -136,29 +141,29 @@
                 mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
                 mMockFindInFileView,
                 mIsAnnotationIntentResolvable, mMockSelectionActionMode,
-                VIEW_STATE_EXPOSED_VALUE);
+                VIEW_STATE_EXPOSED_VALUE, mMockImmersiveModeRequester);
         zoomScrollValueObserver.onChange(OLD_POSITION, mNewPosition);
 
-        verify(mMockAnnotationButton).setVisibility(View.VISIBLE);
+        verify(mMockImmersiveModeRequester).requestImmersiveModeChange(false);
     }
 
     @Test
-    public void onChange_hideAnnotationButton() {
+    public void onChange_enterImmersiveMode() {
         mIsAnnotationIntentResolvable = true;
         mNewPosition = new ZoomView.ZoomScroll(1.0f, 0, 10, false);
-        mOldPosition = new ZoomView.ZoomScroll(1.0f, 0, 10, false);
+        ZoomView.ZoomScroll oldPosition = new ZoomView.ZoomScroll(1.0f, 0, 10, false);
         when(mMockAnnotationButton.getVisibility()).thenReturn(View.VISIBLE);
 
         ZoomScrollValueObserver zoomScrollValueObserver = new ZoomScrollValueObserver(mMockZoomView,
                 mMockPaginatedView, mMockLayoutHandler, mMockAnnotationButton,
                 mMockFindInFileView,
                 mIsAnnotationIntentResolvable, mMockSelectionActionMode,
-                VIEW_STATE_EXPOSED_VALUE);
-        zoomScrollValueObserver.onChange(mOldPosition, mNewPosition);
+                VIEW_STATE_EXPOSED_VALUE, mMockImmersiveModeRequester);
+        zoomScrollValueObserver.onChange(oldPosition, mNewPosition);
 //        TODO: Remove this hardcode dependency.
-        new Handler(Looper.getMainLooper()).postDelayed(() -> {
-            verify(mMockAnnotationButton).setVisibility(View.GONE);
-        }, ANIMATION_DELAY_MILLIS);
+        new Handler(Looper.getMainLooper()).postDelayed(
+                () -> verify(mMockImmersiveModeRequester).requestImmersiveModeChange(true),
+                ANIMATION_DELAY_MILLIS);
     }
 
 }
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainFragment.kt
deleted file mode 100644
index 8b13789..0000000
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainFragment.kt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl
index 47e58ed..63aa0ac 100644
--- a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/ISandboxedUiAdapter.aidl
@@ -21,6 +21,7 @@
 
 @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 oneway interface ISandboxedUiAdapter {
+    @JavaPassthrough(annotation="@androidx.annotation.RequiresApi(34)")
     void openRemoteSession(
         IBinder hostToken, int displayId, int initialWidth, int initialHeight, boolean isZOrderOnTop,
         IRemoteSessionClient remoteSessionClient);
diff --git a/privacysandbox/ui/ui-provider/build.gradle b/privacysandbox/ui/ui-provider/build.gradle
index 73065aa..6e49780 100644
--- a/privacysandbox/ui/ui-provider/build.gradle
+++ b/privacysandbox/ui/ui-provider/build.gradle
@@ -47,6 +47,7 @@
     androidTestImplementation(libs.espressoCore)
     androidTestImplementation(libs.testUiautomator)
     androidTestImplementation(project(":internal-testutils-runtime"))
+    androidTestImplementation(project(":internal-testutils-truth"))
 }
 
 android {
diff --git a/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/impl/DeferredObjectHolderTest.kt b/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/impl/DeferredObjectHolderTest.kt
new file mode 100644
index 0000000..43bd0a2
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/impl/DeferredObjectHolderTest.kt
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.provider.impl
+
+import android.os.MessageQueue
+import androidx.core.util.Consumer
+import androidx.core.util.Supplier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 23)
+class DeferredObjectHolderTest {
+
+    private lateinit var messageQueue: StubMessageQueue
+
+    private lateinit var errorObject: StubObject
+    private lateinit var errorHandler: StubErrorHandler
+
+    @Before
+    fun setUp() {
+        messageQueue = StubMessageQueue()
+        errorObject = StubObject().also(StubObject::initialize)
+        errorHandler = StubErrorHandler()
+    }
+
+    @After
+    fun tearDown() {
+        errorHandler.assertNoError()
+    }
+
+    @Test
+    fun demandObject_beforePreloadObject_createAndInitObject_success() {
+        val stubObject = StubObject()
+        val deferredObjectHolder = createDeferredObjectHolder(stubObject)
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+
+        // No handler registered as object already created
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isFalse()
+    }
+
+    @Test
+    fun demandObject_beforePreloadObject_createAndInitObject_createFail() {
+        val exceptionOnCreate = RuntimeException("Error during creation")
+        demandObject_beforePreloadObject_createAndInitObject_fail(
+            createDeferredObjectHolder(exceptionOnCreate = exceptionOnCreate),
+            exceptionOnCreate
+        )
+    }
+
+    @Test
+    fun demandObject_beforePreloadObject_createAndInitObject_initFail() {
+        val exceptionOnInit = RuntimeException("Error during initialization")
+        demandObject_beforePreloadObject_createAndInitObject_fail(
+            createDeferredObjectHolder(exceptionOnInit = exceptionOnInit),
+            exceptionOnInit
+        )
+    }
+
+    private fun demandObject_beforePreloadObject_createAndInitObject_fail(
+        deferredObjectHolder: DeferredObjectHolder<StubObject, StubObject>,
+        expectedError: Throwable
+    ) {
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertErrorAndReset(expectedError)
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertNoError()
+
+        // No handler registered as already failed
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isFalse()
+    }
+
+    @Test
+    fun demandObject_beforeIdle_createAndInitObject_success() {
+        val stubObject = StubObject()
+        val deferredObjectHolder = createDeferredObjectHolder(stubObject)
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+
+        // Handler unregistered during processIdle() as object already created and initialized
+        messageQueue.processIdle()
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+    }
+
+    @Test
+    fun demandObject_beforeIdle_createAndInitObject_createFail() {
+        val exceptionOnCreate = RuntimeException("Error during creation")
+        demandObject_beforeIdle_createAndInitObject_fail(
+            createDeferredObjectHolder(exceptionOnCreate = exceptionOnCreate),
+            exceptionOnCreate
+        )
+    }
+
+    @Test
+    fun demandObject_beforeIdle_createAndInitObject_initFail() {
+        val exceptionOnInit = RuntimeException("Error during initialization")
+        demandObject_beforeIdle_createAndInitObject_fail(
+            createDeferredObjectHolder(exceptionOnInit = exceptionOnInit),
+            exceptionOnInit
+        )
+    }
+
+    private fun demandObject_beforeIdle_createAndInitObject_fail(
+        deferredObjectHolder: DeferredObjectHolder<StubObject, StubObject>,
+        expectedError: Throwable
+    ) {
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertErrorAndReset(expectedError)
+
+        // Handler unregistered during processIdle() as already failed
+        messageQueue.processIdle()
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertNoError()
+    }
+
+    @Test
+    fun demandObject_afterIdleCreate_initObject_success() {
+        val stubObject = StubObject()
+        val deferredObjectHolder = createDeferredObjectHolder(stubObject)
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Created in first idle, but not initialized
+        messageQueue.processIdle()
+        assertThat(stubObject.isInitCalled()).isFalse()
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Initialize during demandObject()
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+        assertThat(stubObject.isInitCalled()).isTrue()
+
+        // Handler unregistered during processIdle() as object already created and initialized
+        messageQueue.processIdle()
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+    }
+
+    @Test
+    fun demandObject_afterIdleCreate_initObject_fail() {
+        val exceptionOnInit = RuntimeException("Error during initialization")
+        val deferredObjectHolder = createDeferredObjectHolder(exceptionOnInit = exceptionOnInit)
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Created in first idle, but not initialized
+        messageQueue.processIdle()
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertErrorAndReset(exceptionOnInit)
+
+        // Handler unregistered during processIdle() as already failed
+        messageQueue.processIdle()
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertNoError()
+    }
+
+    @Test
+    fun demandObject_afterIdleInit_differentCreateAndInitIdles() {
+        val stubObject = StubObject()
+        val deferredObjectHolder = createDeferredObjectHolder(stubObject)
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Create in first idle, but not initialize
+        messageQueue.processIdle(hasMessagesDue = true)
+        assertThat(stubObject.isInitCalled()).isFalse()
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Init in idle when no messages due while processing
+        messageQueue.processIdle(hasMessagesDue = false)
+        assertThat(stubObject.isInitCalled()).isTrue()
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+    }
+
+    @Test
+    fun demandObject_afterIdleInit_singleCreateAndInitIdle() {
+        val stubObject = StubObject()
+        val deferredObjectHolder = createDeferredObjectHolder(stubObject)
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Create and init in first idle (no messages due while processing)
+        messageQueue.processIdle(hasMessagesDue = false)
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(stubObject)
+    }
+
+    @Test
+    fun demandObject_afterIdleCreateFail_differentCreateAndInitIdles() {
+        demandObject_afterIdleCreateFail(hasMessagesDueWhileProcessIdle = true)
+    }
+
+    @Test
+    fun demandObject_afterIdleCreateFail_singleCreateAndInitIdle() {
+        demandObject_afterIdleCreateFail(hasMessagesDueWhileProcessIdle = false)
+    }
+
+    private fun demandObject_afterIdleCreateFail(hasMessagesDueWhileProcessIdle: Boolean) {
+        val exceptionOnCreate = RuntimeException("Error during creation")
+        val deferredObjectHolder = createDeferredObjectHolder(exceptionOnCreate = exceptionOnCreate)
+
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Fail object creation in first idle
+        messageQueue.processIdle(hasMessagesDue = hasMessagesDueWhileProcessIdle)
+        errorHandler.assertErrorAndReset(exceptionOnCreate)
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertNoError()
+    }
+
+    @Test
+    fun demandObject_afterIdleInitFail_differentCreateAndInitIdles() {
+        val exceptionOnInit = RuntimeException("Error during initialization")
+        val deferredObjectHolder = createDeferredObjectHolder(exceptionOnInit = exceptionOnInit)
+
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Create in first idle
+        messageQueue.processIdle(hasMessagesDue = true)
+        errorHandler.assertNoError()
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Init fail in idle when no messages due while processing
+        messageQueue.processIdle(hasMessagesDue = false)
+        errorHandler.assertErrorAndReset(exceptionOnInit)
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertNoError()
+    }
+
+    @Test
+    fun demandObject_afterIdleInitFail_singleCreateAndInitIdle() {
+        val exceptionOnInit = RuntimeException("Error during initialization")
+        val deferredObjectHolder = createDeferredObjectHolder(exceptionOnInit = exceptionOnInit)
+
+        deferredObjectHolder.preloadObject(messageQueue)
+        assertThat(messageQueue.handlerRegistered).isTrue()
+
+        // Successfully create object, but fail init in first idle
+        messageQueue.processIdle(hasMessagesDue = false)
+        errorHandler.assertErrorAndReset(exceptionOnInit)
+        assertThat(messageQueue.handlerRegistered).isFalse()
+
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+
+        // Check that creation / initialization logic triggered only once
+        deferredObjectHolder.demandObjectAndAssertThatSameInstanceAs(errorObject)
+        errorHandler.assertNoError()
+    }
+
+    private fun createDeferredObjectHolder(
+        stubObject: StubObject
+    ): DeferredObjectHolder<StubObject, StubObject> =
+        createDeferredObjectHolder(StubObjectFactory(stubObject = stubObject))
+
+    private fun createDeferredObjectHolder(
+        exceptionOnCreate: Throwable? = null,
+        exceptionOnInit: Throwable? = null
+    ): DeferredObjectHolder<StubObject, StubObject> =
+        createDeferredObjectHolder(
+            StubObjectFactory(
+                exceptionOnCreate = exceptionOnCreate,
+                stubObject = StubObject(exceptionOnInit = exceptionOnInit)
+            )
+        )
+
+    private fun createDeferredObjectHolder(
+        objectFactory: Supplier<StubObject>
+    ): DeferredObjectHolder<StubObject, StubObject> {
+        return DeferredObjectHolder(
+            objectFactory = objectFactory,
+            objectInit = StubObject::initialize,
+            errorHandler = errorHandler,
+            errorObject = errorObject
+        )
+    }
+
+    private class StubObjectFactory(
+        private val exceptionOnCreate: Throwable? = null,
+        private val stubObject: StubObject = StubObject(),
+    ) : Supplier<StubObject> {
+
+        val createMethodCallCounter = AtomicInteger(0)
+
+        override fun get(): StubObject {
+            // Check that called only once.
+            assertThat(createMethodCallCounter.incrementAndGet()).isEqualTo(1)
+            if (exceptionOnCreate != null) {
+                throw exceptionOnCreate
+            }
+            return stubObject
+        }
+    }
+
+    private class StubObject(private val exceptionOnInit: Throwable? = null) {
+        private val initMethodCallCounter = AtomicInteger(0)
+
+        fun initialize() {
+            // Check that called only once.
+            assertThat(initMethodCallCounter.incrementAndGet()).isEqualTo(1)
+            if (exceptionOnInit != null) {
+                throw exceptionOnInit
+            }
+        }
+
+        fun isInitCalled(): Boolean {
+            return initMethodCallCounter.get() != 0
+        }
+    }
+
+    private class StubErrorHandler : Consumer<Throwable> {
+        private var error: Throwable? = null
+
+        fun assertErrorAndReset(expected: Throwable) {
+            assertThat(error).isEqualTo(expected)
+            error = null
+        }
+
+        fun assertNoError() {
+            assertWithMessage("Unexpected error").that(error).isNull()
+        }
+
+        override fun accept(value: Throwable) {
+            // Check that error set only once before assertErrorAndReset() call.
+            assertThat(this.error).isNull()
+            this.error = value
+        }
+    }
+
+    private class StubMessageQueue : DeferredObjectHolder.MessageQueue {
+
+        val handlerRegistered: Boolean
+            get() = handlers.isNotEmpty()
+
+        private var idle: Boolean = false
+        private var handlers: MutableList<MessageQueue.IdleHandler> = mutableListOf()
+
+        /**
+         * Emulate Idle state - run registered IdleHandlers.
+         *
+         * If handler calls [isIdle] during processing it will receive ![hasMessagesDue] as result.
+         *
+         * In real world [hasMessagesDue] will be "true" more often that "false":
+         * 1) It will be used during intensive loading process - high chance of new messages arrive.
+         * 2) Logic inside IdleHandler takes time - increasing chance of new messages arrive.
+         * 3) Other IdleHandlers takes time / etc.
+         */
+        fun processIdle(hasMessagesDue: Boolean = true) {
+            idle = !hasMessagesDue
+            val it = handlers.iterator()
+            while (it.hasNext()) {
+                val handler = it.next()
+                val keep = handler.queueIdle()
+                if (!keep) {
+                    it.remove()
+                }
+            }
+            idle = false
+        }
+
+        override fun addIdleHandler(handler: MessageQueue.IdleHandler) {
+            handlers.add(handler)
+        }
+
+        override fun isIdle(): Boolean {
+            return idle
+        }
+    }
+
+    private fun DeferredObjectHolder<StubObject, StubObject>
+        .demandObjectAndAssertThatSameInstanceAs(expected: StubObject) {
+        val result = demandObject()
+        assertThat(result.isInitCalled()).isTrue()
+        assertThat(result).isSameInstanceAs(expected)
+    }
+}
diff --git a/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/impl/DeferredSessionClientTest.kt b/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/impl/DeferredSessionClientTest.kt
new file mode 100644
index 0000000..2634162
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/androidTest/kotlin/androidx/privacysandbox/ui/provider/impl/DeferredSessionClientTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.provider.impl
+
+import android.content.res.Configuration
+import android.os.Bundle
+import android.view.View
+import androidx.core.util.Consumer
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.atomic.AtomicReference
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 23)
+class DeferredSessionClientTest {
+
+    @Test
+    fun onSessionOpened_whenDemandObjectSuccess_delegates() {
+        val stubClient = StubClient()
+        val deferredClient = createDeferredClient(stubClient)
+
+        val session = StubSession()
+        deferredClient.onSessionOpened(session)
+
+        assertThat(stubClient.sessions).containsExactly(session)
+        assertThat(session.closed).isFalse()
+    }
+
+    @Test
+    fun onSessionOpened_whenDemandObjectFailed_callErrorHandlerAndCloseSession() {
+        val exception = RuntimeException("Something went wrong")
+        val stubClient = StubClient(exceptionOnInit = exception)
+        val fail = AtomicReference<Throwable>()
+        val deferredClient = createDeferredClient(stubClient, errorHandler = fail::set)
+
+        val session = StubSession()
+        deferredClient.onSessionOpened(session)
+
+        assertThat(fail.get()).isEqualTo(exception)
+        assertThat(stubClient.sessions).isEmpty()
+        assertThat(session.closed).isTrue()
+    }
+
+    @Test
+    fun onSessionError_whenDemandObjectSuccess_delegates() {
+        val stubClient = StubClient()
+        val deferredClient = createDeferredClient(stubClient)
+
+        val sessionError = RuntimeException("Session opening error")
+        deferredClient.onSessionError(sessionError)
+
+        assertThat(stubClient.errors).containsExactly(sessionError)
+    }
+
+    @Test
+    fun onSessionError_whenDemandObjectFailed_callErrorHandler() {
+        val exception = RuntimeException("Something went wrong")
+        val stubClient = StubClient(exceptionOnInit = exception)
+        val fail = AtomicReference<Throwable>()
+        val deferredClient = createDeferredClient(stubClient, errorHandler = fail::set)
+
+        val sessionError = RuntimeException("Session opening error")
+        deferredClient.onSessionError(sessionError)
+
+        assertThat(fail.get()).isEqualTo(exception)
+        assertThat(stubClient.errors).isEmpty()
+    }
+
+    @Test
+    fun onResizeRequested_whenDemandObjectSuccess_delegates() {
+        val stubClient = StubClient()
+        val deferredClient = createDeferredClient(stubClient)
+
+        deferredClient.onResizeRequested(1, 2)
+
+        assertThat(stubClient.resizes).containsExactly(Pair(1, 2))
+    }
+
+    @Test
+    fun onResizeRequested_whenDemandObjectFailed_callErrorHandler() {
+        val exception = RuntimeException("Something went wrong")
+        val stubClient = StubClient(exceptionOnInit = exception)
+        val fail = AtomicReference<Throwable>()
+        val deferredClient = createDeferredClient(stubClient, errorHandler = fail::set)
+
+        deferredClient.onResizeRequested(1, 2)
+
+        assertThat(fail.get()).isEqualTo(exception)
+        assertThat(stubClient.resizes).isEmpty()
+    }
+
+    private fun createDeferredClient(
+        stubClient: StubClient,
+        errorHandler: Consumer<Throwable> = Consumer {
+            Assert.fail("Unexpected fail " + it.message)
+        }
+    ): DeferredSessionClient {
+        return DeferredSessionClient.create(
+            clientFactory = { stubClient },
+            clientInit = StubClient::initialize,
+            errorHandler = errorHandler
+        )
+    }
+
+    private class StubClient(val exceptionOnInit: Throwable? = null) :
+        SandboxedUiAdapter.SessionClient {
+
+        val sessions: MutableList<SandboxedUiAdapter.Session> = mutableListOf()
+        val errors: MutableList<Throwable> = mutableListOf()
+        val resizes: MutableList<Pair<Int, Int>> = mutableListOf()
+
+        fun initialize() {
+            if (exceptionOnInit != null) {
+                throw exceptionOnInit
+            }
+        }
+
+        override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
+            sessions.add(session)
+        }
+
+        override fun onSessionError(throwable: Throwable) {
+            errors.add(throwable)
+        }
+
+        override fun onResizeRequested(width: Int, height: Int) {
+            resizes.add(Pair(width, height))
+        }
+    }
+
+    private class StubSession : SandboxedUiAdapter.Session {
+
+        var closed: Boolean = false
+
+        override val view: View
+            get() = throw UnsupportedOperationException("Not implemented")
+
+        override val signalOptions: Set<String>
+            get() = throw UnsupportedOperationException("Not implemented")
+
+        override fun notifyResized(width: Int, height: Int) {}
+
+        override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {}
+
+        override fun notifyConfigurationChanged(configuration: Configuration) {}
+
+        override fun notifyUiChanged(uiContainerInfo: Bundle) {}
+
+        override fun close() {
+            closed = true
+        }
+    }
+}
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
index ecb4e64..fff6cad6 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
@@ -38,6 +38,7 @@
 import androidx.privacysandbox.ui.core.SessionObserver
 import androidx.privacysandbox.ui.core.SessionObserverContext
 import androidx.privacysandbox.ui.core.SessionObserverFactory
+import androidx.privacysandbox.ui.provider.impl.DeferredSessionClient
 import java.util.concurrent.Executor
 
 /**
@@ -87,7 +88,6 @@
 
     override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
 
-    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     override fun openRemoteSession(
         windowInputToken: IBinder,
         displayId: Int,
@@ -96,60 +96,85 @@
         isZOrderOnTop: Boolean,
         remoteSessionClient: IRemoteSessionClient
     ) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            remoteSessionClient.onRemoteSessionError("openRemoteSession() requires API34+")
+            return
+        }
+
         val mHandler = Handler(Looper.getMainLooper())
         mHandler.post {
             try {
-                val mDisplayManager: DisplayManager =
+                val displayManager =
                     sandboxContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
-                val windowContext =
-                    sandboxContext.createDisplayContext(mDisplayManager.getDisplay(displayId))
-                val surfaceControlViewHost =
-                    CompatImpl.createSurfaceControlViewHost(
-                        windowContext,
-                        mDisplayManager.getDisplay(displayId),
-                        windowInputToken
+                val display = displayManager.getDisplay(displayId)
+                val displayContext = sandboxContext.createDisplayContext(display)
+
+                val deferredClient =
+                    DeferredSessionClient.create(
+                        clientFactory = {
+                            Api34PlusImpl.createSessionClientProxy(
+                                displayContext,
+                                display,
+                                windowInputToken,
+                                isZOrderOnTop,
+                                remoteSessionClient
+                            )
+                        },
+                        clientInit = { it.initialize(initialWidth, initialHeight) },
+                        errorHandler = { remoteSessionClient.onRemoteSessionError(it.message) }
                     )
-                checkNotNull(surfaceControlViewHost) {
-                    "SurfaceControlViewHost must be available when provider is remote"
-                }
-                val sessionClient =
-                    SessionClientProxy(
-                        surfaceControlViewHost,
-                        initialWidth,
-                        initialHeight,
-                        isZOrderOnTop,
-                        remoteSessionClient
-                    )
+
                 openSession(
-                    windowContext,
+                    displayContext,
                     windowInputToken,
                     initialWidth,
                     initialHeight,
                     isZOrderOnTop,
-                    Runnable::run,
-                    sessionClient
+                    MainThreadExecutor(mHandler),
+                    deferredClient
                 )
+
+                deferredClient.preloadClient()
             } catch (exception: Throwable) {
                 remoteSessionClient.onRemoteSessionError(exception.message)
             }
         }
     }
 
+    /** Avoiding all potential concurrency issues by executing callback only on main thread. */
+    private class MainThreadExecutor(private val mainHandler: Handler) : Executor {
+        override fun execute(command: Runnable) {
+            if (Looper.getMainLooper() == Looper.myLooper()) {
+                command.run()
+            } else {
+                mainHandler.post(command)
+            }
+        }
+    }
+
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    private inner class SessionClientProxy(
+    private class SessionClientProxy(
+        private val touchTransferringView: TouchFocusTransferringView,
         private val surfaceControlViewHost: SurfaceControlViewHost,
-        private val initialWidth: Int,
-        private val initialHeight: Int,
         private val isZOrderOnTop: Boolean,
         private val remoteSessionClient: IRemoteSessionClient
     ) : SandboxedUiAdapter.SessionClient {
 
+        /**
+         * Split SurfaceControlViewHost creation and calling setView() into 2 steps to minimize each
+         * step duration and interference with actual openSession() logic (reduce potential delays).
+         */
+        fun initialize(initialWidth: Int, initialHeight: Int) {
+            surfaceControlViewHost.setView(touchTransferringView, initialWidth, initialHeight)
+        }
+
         override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
             val view = session.view
-            val touchTransferringView =
-                TouchFocusTransferringView(sandboxContext, surfaceControlViewHost)
+
+            if (touchTransferringView.childCount > 0) {
+                touchTransferringView.removeAllViews()
+            }
             touchTransferringView.addView(view)
-            surfaceControlViewHost.setView(touchTransferringView, initialWidth, initialHeight)
 
             // This var is not locked as it will be set to false by the first event that can trigger
             // sending the remote session opened callback.
@@ -294,37 +319,25 @@
         }
     }
 
-    /**
-     * Provides backward compat support for APIs.
-     *
-     * If the API is available, it's called from a version-specific static inner class gated with
-     * version check, otherwise a fallback action is taken depending on the situation.
-     */
-    private object CompatImpl {
-
-        fun createSurfaceControlViewHost(
-            context: Context,
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    private object Api34PlusImpl {
+        fun createSessionClientProxy(
+            displayContext: Context,
             display: Display,
-            hostToken: IBinder
-        ): SurfaceControlViewHost? {
-            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
-                return Api34PlusImpl.createSurfaceControlViewHost(context, display, hostToken)
-            } else {
-                null
-            }
-        }
-
-        @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-        private object Api34PlusImpl {
-
-            @JvmStatic
-            fun createSurfaceControlViewHost(
-                context: Context,
-                display: Display,
-                hostToken: IBinder
-            ): SurfaceControlViewHost {
-                return SurfaceControlViewHost(context, display, hostToken)
-            }
+            windowInputToken: IBinder,
+            isZOrderOnTop: Boolean,
+            remoteSessionClient: IRemoteSessionClient
+        ): SessionClientProxy {
+            val surfaceControlViewHost =
+                SurfaceControlViewHost(displayContext, display, windowInputToken)
+            val touchTransferringView =
+                TouchFocusTransferringView(displayContext, surfaceControlViewHost)
+            return SessionClientProxy(
+                touchTransferringView,
+                surfaceControlViewHost,
+                isZOrderOnTop,
+                remoteSessionClient
+            )
         }
     }
 }
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/DeferredObjectHolder.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/DeferredObjectHolder.kt
new file mode 100644
index 0000000..25fabfa
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/DeferredObjectHolder.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.provider.impl
+
+import android.os.Build
+import android.os.Looper
+import android.os.MessageQueue.IdleHandler
+import androidx.annotation.RequiresApi
+import androidx.core.util.Consumer
+import androidx.core.util.Supplier
+import org.jetbrains.annotations.TestOnly
+
+/**
+ * Tries to postpone object creation/initialization until Idle state.
+ *
+ * Uses [objectFactory] to create object and [objectInit] to initialize it later. In case of
+ * creation/initialization failure reports error to [errorHandler] and uses [errorObject] as return
+ * value for [demandObject]
+ *
+ * @param objectFactory Creates uninitialized object. Called only once.
+ * @param objectInit Initialize object created by [objectFactory]. Called only once.
+ * @param errorHandler Handler for error during creation/initialization. Called only once.
+ * @param errorObject Return value for [demandObject] in case of creation/initialization errors.
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+internal class DeferredObjectHolder<BaseClass : Any, ImplementationClass : BaseClass>(
+    private val objectFactory: Supplier<ImplementationClass>,
+    private val objectInit: Consumer<ImplementationClass>,
+    private val errorHandler: Consumer<Throwable>,
+    private val errorObject: BaseClass
+) : ObjectHolder<BaseClass> {
+
+    private lateinit var impl: ImplementationClass
+    private var state: ObjectState = ObjectState.NONE
+
+    override fun demandObject(): BaseClass {
+        createObjectIfNeeded()
+        initializeObjectIfNeeded()
+        return if (state == ObjectState.INITIALIZED) {
+            impl
+        } else {
+            errorObject
+        }
+    }
+
+    /**
+     * Schedule object preloading in upcoming Idles.
+     *
+     * Creates object in a first Idle and tries to initialize it in one of subsequent Idles
+     * (depending on how busy/utilized main thread).
+     *
+     * This combination provides good balance between Idle time utilization and interference with
+     * other tasks scheduled for main thread.
+     *
+     * Although checking (or not) Idle each time could give better utilization for some use cases,
+     * using combination of 2 different approaches should give better average results for more
+     * scenarios by covering weak points of single approaches.
+     */
+    override fun preloadObject() {
+        preloadObject(AndroidMessageQueue)
+    }
+
+    @TestOnly
+    fun preloadObject(messageQueue: MessageQueue) {
+        if (state.isFinalState()) {
+            // Already initialized or failed - no further steps required.
+            return
+        }
+        messageQueue.addIdleHandler {
+            createObjectIfNeeded()
+
+            if (messageQueue.isIdle()) {
+                initializeObjectIfNeeded()
+            }
+
+            return@addIdleHandler !state.isFinalState()
+        }
+    }
+
+    private fun createObjectIfNeeded() = tryWithErrorHandling {
+        if (state == ObjectState.NONE) {
+            impl = objectFactory.get()
+            state = ObjectState.CREATED
+        }
+    }
+
+    private fun initializeObjectIfNeeded() = tryWithErrorHandling {
+        if (state == ObjectState.CREATED) {
+            objectInit.accept(impl)
+            state = ObjectState.INITIALIZED
+        }
+    }
+
+    private inline fun tryWithErrorHandling(function: () -> Unit) {
+        try {
+            function()
+        } catch (exception: Throwable) {
+            state = ObjectState.FAILED
+            errorHandler.accept(exception)
+        }
+    }
+
+    private enum class ObjectState {
+        NONE,
+        CREATED,
+        INITIALIZED,
+        FAILED;
+
+        fun isFinalState(): Boolean {
+            return this == INITIALIZED || this == FAILED
+        }
+    }
+
+    interface MessageQueue {
+        fun addIdleHandler(handler: IdleHandler)
+
+        fun isIdle(): Boolean
+    }
+
+    private object AndroidMessageQueue : MessageQueue {
+        override fun addIdleHandler(handler: IdleHandler) = Looper.myQueue().addIdleHandler(handler)
+
+        override fun isIdle() = Looper.myQueue().isIdle
+    }
+}
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/DeferredSessionClient.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/DeferredSessionClient.kt
new file mode 100644
index 0000000..2a65bb9
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/DeferredSessionClient.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.provider.impl
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.util.Consumer
+import androidx.core.util.Supplier
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+
+/**
+ * Placeholder client passed to [SandboxedUiAdapter.openSession] before actual client will be
+ * created.
+ *
+ * If adapter uses background threads, calling openSession() earlier could improve latency by
+ * scheduling provider tasks earlier and creating actual client while waiting for results.
+ *
+ * Using [DeferredObjectHolder] to create actual client in background.
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+internal class DeferredSessionClient(
+    private val objectHolder: ObjectHolder<SandboxedUiAdapter.SessionClient>
+) : SandboxedUiAdapter.SessionClient {
+
+    override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
+        objectHolder.demandObject().onSessionOpened(session)
+    }
+
+    override fun onSessionError(throwable: Throwable) {
+        objectHolder.demandObject().onSessionError(throwable)
+    }
+
+    override fun onResizeRequested(width: Int, height: Int) {
+        objectHolder.demandObject().onResizeRequested(width, height)
+    }
+
+    fun preloadClient() = objectHolder.preloadObject()
+
+    companion object {
+        private const val TAG = "DeferredSessionClient"
+
+        fun <T : SandboxedUiAdapter.SessionClient> create(
+            clientFactory: Supplier<T>,
+            clientInit: Consumer<T>,
+            errorHandler: Consumer<Throwable>
+        ): DeferredSessionClient {
+            return DeferredSessionClient(
+                DeferredObjectHolder(
+                    objectFactory = clientFactory,
+                    objectInit = clientInit,
+                    errorHandler = {
+                        Log.e(TAG, "Exception during actual client initialization", it)
+                        errorHandler.accept(it)
+                    },
+                    FailClient
+                )
+            )
+        }
+
+        private object FailClient : SandboxedUiAdapter.SessionClient {
+            override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
+                Log.w(TAG, "Auto-closing session on actual client initialization error")
+                session.close()
+            }
+
+            override fun onSessionError(throwable: Throwable) {}
+
+            override fun onResizeRequested(width: Int, height: Int) {}
+        }
+    }
+}
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/ObjectHolder.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/ObjectHolder.kt
new file mode 100644
index 0000000..436ea5b
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/impl/ObjectHolder.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.provider.impl
+
+/** Interface for object holder that could create object on demand. */
+internal interface ObjectHolder<T> {
+    fun demandObject(): T
+
+    fun preloadObject()
+}
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
index 7e17123..921c73c 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
@@ -265,8 +265,8 @@
         activityScenarioRule.withActivity {
             testSession.sessionClient.onResizeRequested(newWidth, newHeight)
         }
-        assertWithMessage("Resized height").that(testSession.resizedWidth).isEqualTo(newWidth)
-        assertWithMessage("Resized width").that(testSession.resizedHeight).isEqualTo(newHeight)
+        assertWithMessage("Resized width").that(testSession.resizedWidth).isEqualTo(newWidth)
+        assertWithMessage("Resized height").that(testSession.resizedHeight).isEqualTo(newHeight)
         testSession.assertResizeOccurred(
             /* expectedWidth=*/ newWidth,
             /* expectedHeight=*/ newHeight
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 5cc082e..85f9661 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -604,8 +604,8 @@
   }
 
   public final class PagerKt {
-    method @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.wear.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional int beyondViewportPageCount, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional @FloatRange(from=0.0, to=1.0) float swipeToDismissEdgeZoneFraction, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void VerticalPager(androidx.wear.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional int beyondViewportPageCount, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.wear.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional int beyondViewportPageCount, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional @FloatRange(from=0.0, to=1.0) float swipeToDismissEdgeZoneFraction, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void VerticalPager(androidx.wear.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional int beyondViewportPageCount, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
   }
 
   public abstract class PagerState extends androidx.compose.foundation.pager.PagerState {
@@ -630,6 +630,7 @@
     method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior behavior(androidx.compose.foundation.gestures.ScrollableState scrollableState, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional boolean hapticFeedbackEnabled);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior snapBehavior(androidx.compose.foundation.gestures.ScrollableState scrollableState, androidx.wear.compose.foundation.rotary.RotarySnapLayoutInfoProvider layoutInfoProvider, optional float snapOffset, optional boolean hapticFeedbackEnabled);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior snapBehavior(androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollableState, optional float snapOffset, optional boolean hapticFeedbackEnabled);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior snapBehavior(androidx.wear.compose.foundation.pager.PagerState pagerState, optional float snapOffset, optional boolean hapticFeedbackEnabled);
     field public static final androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults INSTANCE;
   }
 
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 5cc082e..85f9661 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -604,8 +604,8 @@
   }
 
   public final class PagerKt {
-    method @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.wear.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional int beyondViewportPageCount, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional @FloatRange(from=0.0, to=1.0) float swipeToDismissEdgeZoneFraction, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void VerticalPager(androidx.wear.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional int beyondViewportPageCount, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void HorizontalPager(androidx.wear.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional int beyondViewportPageCount, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional @FloatRange(from=0.0, to=1.0) float swipeToDismissEdgeZoneFraction, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void VerticalPager(androidx.wear.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional int beyondViewportPageCount, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional boolean userScrollEnabled, optional boolean reverseLayout, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
   }
 
   public abstract class PagerState extends androidx.compose.foundation.pager.PagerState {
@@ -630,6 +630,7 @@
     method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior behavior(androidx.compose.foundation.gestures.ScrollableState scrollableState, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional boolean hapticFeedbackEnabled);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior snapBehavior(androidx.compose.foundation.gestures.ScrollableState scrollableState, androidx.wear.compose.foundation.rotary.RotarySnapLayoutInfoProvider layoutInfoProvider, optional float snapOffset, optional boolean hapticFeedbackEnabled);
     method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior snapBehavior(androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollableState, optional float snapOffset, optional boolean hapticFeedbackEnabled);
+    method @androidx.compose.runtime.Composable public androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior snapBehavior(androidx.wear.compose.foundation.pager.PagerState pagerState, optional float snapOffset, optional boolean hapticFeedbackEnabled);
     field public static final androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults INSTANCE;
   }
 
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/PagerTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/PagerTest.kt
index 4446e58..6e76ce7 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/PagerTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/PagerTest.kt
@@ -17,27 +17,44 @@
 package androidx.wear.compose.foundation
 
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.RotaryInjectionScope
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performRotaryScrollInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeLeft
 import androidx.compose.ui.test.swipeRight
 import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.pager.HorizontalPager
 import androidx.wear.compose.foundation.pager.PagerState
 import androidx.wear.compose.foundation.pager.VerticalPager
 import androidx.wear.compose.foundation.pager.rememberPagerState
+import androidx.wear.compose.foundation.rotary.MockRotaryResolution
+import androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.foundation.rotary.RotarySnapSensitivity
+import androidx.wear.compose.foundation.rotary.rotaryScrollable
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.junit.Assert
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 
@@ -45,8 +62,14 @@
     @get:Rule val rule = createComposeRule()
 
     private val pagerTestTag = "Pager"
+    private var lcItemSizePx: Float = 20f
+    private var lcItemSizeDp: Dp = Dp.Infinity
 
-    @OptIn(ExperimentalWearFoundationApi::class)
+    @Before
+    fun before() {
+        with(rule.density) { lcItemSizeDp = lcItemSizePx.toDp() }
+    }
+
     @Test
     fun horizontal_pager_with_nested_scaling_lazy_column_swipes_along_one_page_at_a_time() {
         val pageCount = 5
@@ -238,4 +261,357 @@
 
         verifyScrollsToEachPage(pageCount, pagerState, scrollScope)
     }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun vertical_pager_scrolled_by_2_pages_with_rotary_high_res() {
+        verticalPagerRotaryScrolledBy(
+            lowRes = false,
+            userScrollEnabled = true,
+            rotaryScrollableBehavior = { RotaryScrollableDefaults.snapBehavior(it) },
+            rotaryScrollInput = { pagerState ->
+                for (i in 0..1) {
+                    rotateToScrollVertically(
+                        pagerState.layoutInfo.pageSize.toFloat() /
+                            RotarySnapSensitivity.HIGH.minThresholdDivider + 1
+                    )
+                    advanceEventTime(100)
+                }
+            },
+            expectedPageTarget = 2
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun vertical_pager_scrolled_by_2_pages_with_rotary_lowRes() {
+        verticalPagerRotaryScrolledBy(
+            lowRes = true,
+            userScrollEnabled = true,
+            rotaryScrollableBehavior = { RotaryScrollableDefaults.snapBehavior(it) },
+            rotaryScrollInput = {
+                for (i in 0..1) {
+                    rotateToScrollVertically(100f)
+                    advanceEventTime(100)
+                }
+            },
+            expectedPageTarget = 2
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun vertical_pager_not_rotary_scrolled_with_disabled_userScrolledEnabled() {
+        verticalPagerRotaryScrolledBy(
+            lowRes = false,
+            userScrollEnabled = false,
+            rotaryScrollableBehavior = { RotaryScrollableDefaults.snapBehavior(it) },
+            rotaryScrollInput = { pagerState ->
+                rotateToScrollVertically(
+                    pagerState.layoutInfo.pageSize.toFloat() /
+                        RotarySnapSensitivity.HIGH.minThresholdDivider + 1
+                )
+            },
+            expectedPageTarget = 0
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun vertical_pager_not_rotary_scrolled_without_rotaryScrollableBehavior() {
+        verticalPagerRotaryScrolledBy(
+            lowRes = false,
+            userScrollEnabled = true,
+            rotaryScrollableBehavior = { null },
+            rotaryScrollInput = { pagerState ->
+                rotateToScrollVertically(
+                    pagerState.layoutInfo.pageSize.toFloat() /
+                        RotarySnapSensitivity.HIGH.minThresholdDivider + 1
+                )
+            },
+            expectedPageTarget = 0
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun horizontal_pager_scrolled_by_2_pages_with_rotary_high_res() {
+        horizontalPagerRotaryScrolledBy(
+            lowRes = false,
+            userScrollEnabled = true,
+            rotaryScrollableBehavior = { RotaryScrollableDefaults.snapBehavior(it) },
+            rotaryScrollInput = { pagerState ->
+                for (i in 0..1) {
+                    rotateToScrollVertically(
+                        pagerState.layoutInfo.pageSize.toFloat() /
+                            RotarySnapSensitivity.HIGH.minThresholdDivider + 1
+                    )
+                    advanceEventTime(100)
+                }
+            },
+            expectedPageTarget = 2
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun horizontal_pager_scrolled_by_2_pages_with_rotary_lowRes() {
+        horizontalPagerRotaryScrolledBy(
+            lowRes = true,
+            userScrollEnabled = true,
+            rotaryScrollableBehavior = { RotaryScrollableDefaults.snapBehavior(it) },
+            rotaryScrollInput = {
+                for (i in 0..1) {
+                    rotateToScrollVertically(100f)
+                    advanceEventTime(100)
+                }
+            },
+            expectedPageTarget = 2
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun horizontal_pager_not_rotary_scrolled_with_disabled_userScrolledEnabled() {
+        horizontalPagerRotaryScrolledBy(
+            lowRes = false,
+            userScrollEnabled = false,
+            rotaryScrollableBehavior = { RotaryScrollableDefaults.snapBehavior(it) },
+            rotaryScrollInput = { pagerState ->
+                rotateToScrollVertically(
+                    pagerState.layoutInfo.pageSize.toFloat() /
+                        RotarySnapSensitivity.HIGH.minThresholdDivider + 1
+                )
+            },
+            expectedPageTarget = 0
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun horizontal_pager_not_rotary_scrolled_without_rotaryScrollableBehavior() {
+        horizontalPagerRotaryScrolledBy(
+            lowRes = false,
+            userScrollEnabled = true,
+            rotaryScrollableBehavior = { null },
+            rotaryScrollInput = { pagerState ->
+                rotateToScrollVertically(
+                    pagerState.layoutInfo.pageSize.toFloat() /
+                        RotarySnapSensitivity.HIGH.minThresholdDivider + 1
+                )
+            },
+            expectedPageTarget = 0
+        )
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun content_in_horizontalPager_rotary_scrolled_without_rotaryScrollableBehavior() {
+        lateinit var pagerState: PagerState
+        val pageCount = 5
+        lateinit var lcStates: MutableList<LazyListState>
+
+        rule.setContent {
+            pagerState = rememberPagerState { pageCount }
+            lcStates = MutableList(pageCount) { rememberLazyListState() }
+            MockRotaryResolution(lowRes = false) {
+                HorizontalPager(
+                    modifier = Modifier.testTag(pagerTestTag).size(100.dp),
+                    state = pagerState,
+                ) { page ->
+                    DefaultLazyColumn(lcStates[page])
+                }
+            }
+        }
+
+        rule.onNodeWithTag(pagerTestTag).performRotaryScrollInput {
+            rotateToScrollVertically(lcItemSizePx * 5)
+        }
+
+        // We expect HorizontalPager not to be scrolled and remain on the 0th page.
+        // At the same time a LazyColumn on this page should be scrolled.
+        rule.runOnIdle { Assert.assertEquals(0, pagerState.currentPage) }
+        rule.runOnIdle { Assert.assertEquals(5, lcStates[0].firstVisibleItemIndex) }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun content_in_horizontalPager_not_rotary_scrolled_with_rotaryScrollableBehavior() {
+        lateinit var pagerState: PagerState
+        val pageCount = 5
+        lateinit var lcStates: MutableList<LazyListState>
+
+        rule.setContent {
+            pagerState = rememberPagerState { pageCount }
+            lcStates = MutableList(pageCount) { rememberLazyListState() }
+            MockRotaryResolution(lowRes = false) {
+                HorizontalPager(
+                    modifier = Modifier.testTag(pagerTestTag).size(100.dp),
+                    state = pagerState,
+                    rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(pagerState)
+                ) { page ->
+                    DefaultLazyColumn(lcStates[page])
+                }
+            }
+        }
+
+        rule.onNodeWithTag(pagerTestTag).performRotaryScrollInput {
+            rotateToScrollVertically(
+                pagerState.layoutInfo.pageSize.toFloat() /
+                    RotarySnapSensitivity.HIGH.minThresholdDivider + 1
+            )
+        }
+
+        // We expect HorizontalPager to be scrolled by 1 page.
+        rule.runOnIdle { Assert.assertEquals(1, pagerState.currentPage) }
+        // At the same time LazyColumns shouldn't be scrolled.
+        for (lcState in lcStates) {
+            rule.runOnIdle { Assert.assertEquals(0, lcState.firstVisibleItemIndex) }
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun content_in_verticalPager_rotary_scrolled_without_rotaryScrollableBehavior() {
+        lateinit var pagerState: PagerState
+        val pageCount = 5
+        lateinit var lcStates: MutableList<LazyListState>
+
+        rule.setContent {
+            pagerState = rememberPagerState { pageCount }
+            lcStates = MutableList(pageCount) { rememberLazyListState() }
+            MockRotaryResolution(lowRes = false) {
+                VerticalPager(
+                    modifier = Modifier.testTag(pagerTestTag).size(100.dp),
+                    state = pagerState,
+                    rotaryScrollableBehavior = null
+                ) { page ->
+                    DefaultLazyColumn(lcStates[page])
+                }
+            }
+        }
+
+        rule.onNodeWithTag(pagerTestTag).performRotaryScrollInput {
+            rotateToScrollVertically(lcItemSizePx * 5)
+        }
+
+        // We expect VerticalPager not to be scrolled and remain on the 0th page.
+        // At the same time a LazyColumn on this page should be scrolled.
+        rule.runOnIdle { Assert.assertEquals(0, pagerState.currentPage) }
+        rule.runOnIdle { Assert.assertEquals(5, lcStates[0].firstVisibleItemIndex) }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun content_in_verticalPager_not_rotary_scrolled_with_rotaryScrollableBehavior() {
+        lateinit var pagerState: PagerState
+        val pageCount = 5
+        lateinit var lcStates: MutableList<LazyListState>
+
+        rule.setContent {
+            pagerState = rememberPagerState { pageCount }
+            lcStates = MutableList(pageCount) { rememberLazyListState() }
+            MockRotaryResolution(lowRes = false) {
+                HorizontalPager(
+                    modifier = Modifier.testTag(pagerTestTag).size(100.dp),
+                    state = pagerState,
+                    rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(pagerState)
+                ) { page ->
+                    DefaultLazyColumn(lcStates[page])
+                }
+            }
+        }
+
+        rule.onNodeWithTag(pagerTestTag).performRotaryScrollInput {
+            rotateToScrollVertically(
+                pagerState.layoutInfo.pageSize.toFloat() /
+                    RotarySnapSensitivity.HIGH.minThresholdDivider + 1
+            )
+        }
+
+        // We expect VerticalPager to be scrolled by 1 page.
+        rule.runOnIdle { Assert.assertEquals(1, pagerState.currentPage) }
+        // At the same time LazyColumns shouldn't be scrolled.
+        for (lcState in lcStates) {
+            rule.runOnIdle { Assert.assertEquals(0, lcState.firstVisibleItemIndex) }
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    private fun verticalPagerRotaryScrolledBy(
+        expectedPageTarget: Int,
+        lowRes: Boolean,
+        userScrollEnabled: Boolean,
+        rotaryScrollableBehavior: @Composable (pagerState: PagerState) -> RotaryScrollableBehavior?,
+        rotaryScrollInput: RotaryInjectionScope.(pagerState: PagerState) -> Unit
+    ) {
+        lateinit var pagerState: PagerState
+        val pageCount = 5
+
+        rule.setContent {
+            pagerState = rememberPagerState { pageCount }
+
+            MockRotaryResolution(lowRes = lowRes) {
+                VerticalPager(
+                    modifier = Modifier.testTag(pagerTestTag),
+                    state = pagerState,
+                    userScrollEnabled = userScrollEnabled,
+                    rotaryScrollableBehavior = rotaryScrollableBehavior(pagerState)
+                ) { page ->
+                    BasicText(text = "Page $page")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(pagerTestTag).performRotaryScrollInput { rotaryScrollInput(pagerState) }
+
+        rule.runOnIdle { Assert.assertEquals(expectedPageTarget, pagerState.currentPage) }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    private fun horizontalPagerRotaryScrolledBy(
+        expectedPageTarget: Int,
+        lowRes: Boolean,
+        userScrollEnabled: Boolean,
+        rotaryScrollableBehavior: @Composable (pagerState: PagerState) -> RotaryScrollableBehavior?,
+        rotaryScrollInput: RotaryInjectionScope.(pagerState: PagerState) -> Unit
+    ) {
+        lateinit var pagerState: PagerState
+        val pageCount = 5
+
+        rule.setContent {
+            pagerState = rememberPagerState { pageCount }
+
+            MockRotaryResolution(lowRes = lowRes) {
+                HorizontalPager(
+                    modifier = Modifier.testTag(pagerTestTag),
+                    state = pagerState,
+                    userScrollEnabled = userScrollEnabled,
+                    rotaryScrollableBehavior = rotaryScrollableBehavior(pagerState)
+                ) { page ->
+                    BasicText(text = "Page $page")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(pagerTestTag).performRotaryScrollInput { rotaryScrollInput(pagerState) }
+
+        rule.runOnIdle { Assert.assertEquals(expectedPageTarget, pagerState.currentPage) }
+    }
+
+    @Composable
+    fun DefaultLazyColumn(state: LazyListState) {
+        LazyColumn(
+            state = state,
+            modifier =
+                Modifier.rotaryScrollable(
+                    RotaryScrollableDefaults.behavior(state),
+                    rememberActiveFocusRequester()
+                )
+        ) {
+            for (i in 0..20) {
+                item { BasicText(modifier = Modifier.height(lcItemSizeDp), text = "Page content") }
+            }
+        }
+    }
 }
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/rotary/RotaryTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/rotary/RotaryTest.kt
index 2ade292..42350ba 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/rotary/RotaryTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/rotary/RotaryTest.kt
@@ -257,20 +257,7 @@
         rule.setContent {
             state = rememberLazyListState()
 
-            val context = LocalContext.current
-
-            // Mocking low-res flag
-            val mockContext = spy(context)
-            val mockPackageManager = spy(context.packageManager)
-            `when`(mockPackageManager.hasSystemFeature("android.hardware.rotaryencoder.lowres"))
-                .thenReturn(lowRes)
-
-            doReturn(mockPackageManager).`when`(mockContext).packageManager
-
-            CompositionLocalProvider(
-                LocalContext provides mockContext,
-                LocalOverscrollConfiguration provides null
-            ) {
+            MockRotaryResolution(lowRes = lowRes) {
                 DefaultLazyColumnItemsWithRotary(
                     itemSize = itemSizeDp,
                     scrollableState = state,
@@ -304,3 +291,24 @@
         const val TEST_TAG = "test-tag"
     }
 }
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun MockRotaryResolution(lowRes: Boolean = false, content: @Composable () -> Unit) {
+    val context = LocalContext.current
+
+    // Mocking low-res flag
+    val mockContext = spy(context)
+    val mockPackageManager = spy(context.packageManager)
+    `when`(mockPackageManager.hasSystemFeature("android.hardware.rotaryencoder.lowres"))
+        .thenReturn(lowRes)
+
+    doReturn(mockPackageManager).`when`(mockContext).packageManager
+
+    CompositionLocalProvider(
+        LocalContext provides mockContext,
+        LocalOverscrollConfiguration provides null
+    ) {
+        content()
+    }
+}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/pager/Pager.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/pager/Pager.kt
index 680f343..2ab41d2 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/pager/Pager.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/pager/Pager.kt
@@ -52,6 +52,10 @@
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.HierarchicalFocusCoordinator
+import androidx.wear.compose.foundation.rememberActiveFocusRequester
+import androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
+import androidx.wear.compose.foundation.rotary.rotaryScrollable
 import kotlinx.coroutines.coroutineScope
 
 /**
@@ -88,6 +92,9 @@
  *   the leftmost 25% of the screen will trigger the gesture. Even when RTL mode is enabled, this
  *   parameter only ever applies to the left edge of the screen. Setting this to 0 will disable the
  *   gesture.
+ * @param rotaryScrollableBehavior Parameter for changing rotary behavior. By default rotary support
+ *   is disabled for [HorizontalPager]. It can be enabled by passing
+ *   [RotaryScrollableDefaults.snapBehavior] with pagerState parameter.
  * @param content A composable function that defines the content of each page displayed by the
  *   Pager. This is where the UI elements that should appear within each page should be placed.
  */
@@ -103,6 +110,7 @@
     key: ((index: Int) -> Any)? = null,
     @FloatRange(from = 0.0, to = 1.0)
     swipeToDismissEdgeZoneFraction: Float = PagerDefaults.SwipeToDismissEdgeZoneFraction,
+    rotaryScrollableBehavior: RotaryScrollableBehavior? = null,
     content: @Composable PagerScope.(page: Int) -> Unit
 ) {
     val swipeToDismissEnabled = swipeToDismissEdgeZoneFraction != 0f
@@ -117,6 +125,15 @@
             if (swipeToDismissEnabled) originalTouchSlop * CustomTouchSlopMultiplier
             else originalTouchSlop
     ) {
+        val rotaryModifier =
+            if (rotaryScrollableBehavior != null && userScrollEnabled)
+                Modifier.rotaryScrollable(
+                    behavior = rotaryScrollableBehavior,
+                    focusRequester = rememberActiveFocusRequester(),
+                    reverseDirection = reverseLayout
+                )
+            else Modifier
+
         HorizontalPager(
             state = state,
             modifier =
@@ -143,7 +160,8 @@
                                 // signals system swipe to dismiss that it can take over
                                 ScrollAxisRange(value = { 0f }, maxValue = { 0f })
                             }
-                    },
+                    }
+                    .then(rotaryModifier),
             contentPadding = contentPadding,
             pageSize = PageSize.Fill,
             beyondViewportPageCount = beyondViewportPageCount,
@@ -156,7 +174,12 @@
             snapPosition = SnapPosition.Start,
         ) { page ->
             CustomTouchSlopProvider(newTouchSlop = originalTouchSlop) {
-                FocusedPageContent(page = page, pagerState = state, content = { content(page) })
+                HierarchicalFocusCoordinator(
+                    requiresFocus = {
+                        rotaryScrollableBehavior == null && state.currentPage == page
+                    },
+                    content = { content(page) }
+                )
             }
         }
     }
@@ -167,6 +190,10 @@
  * standard Compose Foundation [VerticalPager] and provides Wear-specific enhancements to improve
  * performance, usability, and adherence to Wear OS design guidelines.
  *
+ * [VerticalPager] supports rotary input by default. Rotary input allows users to scroll through the
+ * pager's content - by using a crown or a rotating bezel on their Wear OS device. It can be
+ * modified or turned off using the [rotaryScrollableBehavior] parameter.
+ *
  * Please refer to the sample to learn how to use this API.
  *
  * @sample androidx.wear.compose.foundation.samples.SimpleVerticalPagerSample
@@ -190,6 +217,9 @@
  *   position will be maintained based on the key, which means if you add/remove items before the
  *   current visible item the item with the given key will be kept as the first visible one. If null
  *   is passed the position in the list will represent the key.
+ * @param rotaryScrollableBehavior Parameter for changing rotary behavior. We recommend to use
+ *   [RotaryScrollableDefaults.snapBehavior] with pagerState parameter. Passing null turns off the
+ *   rotary handling if it is not required.
  * @param content A composable function that defines the content of each page displayed by the
  *   Pager. This is where the UI elements that should appear within each page should be placed.
  */
@@ -203,11 +233,22 @@
     userScrollEnabled: Boolean = true,
     reverseLayout: Boolean = false,
     key: ((index: Int) -> Any)? = null,
+    rotaryScrollableBehavior: RotaryScrollableBehavior? =
+        RotaryScrollableDefaults.snapBehavior(state),
     content: @Composable PagerScope.(page: Int) -> Unit
 ) {
+    val rotaryModifier =
+        if (rotaryScrollableBehavior != null && userScrollEnabled)
+            Modifier.rotaryScrollable(
+                behavior = rotaryScrollableBehavior,
+                focusRequester = rememberActiveFocusRequester(),
+                reverseDirection = reverseLayout
+            )
+        else Modifier
+
     VerticalPager(
         state = state,
-        modifier = modifier.fillMaxSize(),
+        modifier = modifier.fillMaxSize().then(rotaryModifier),
         contentPadding = contentPadding,
         pageSize = PageSize.Fill,
         beyondViewportPageCount = beyondViewportPageCount,
@@ -219,7 +260,10 @@
         key = key,
         snapPosition = SnapPosition.Start,
     ) { page ->
-        FocusedPageContent(page = page, pagerState = state, content = { content(page) })
+        HierarchicalFocusCoordinator(
+            requiresFocus = { rotaryScrollableBehavior == null && state.currentPage == page },
+            content = { content(page) }
+        )
     }
 }
 
@@ -282,18 +326,6 @@
 }
 
 @Composable
-internal fun FocusedPageContent(
-    page: Int,
-    pagerState: PagerState,
-    content: @Composable () -> Unit
-) {
-    HierarchicalFocusCoordinator(
-        requiresFocus = { pagerState.currentPage == page },
-        content = content
-    )
-}
-
-@Composable
 private fun CustomTouchSlopProvider(newTouchSlop: Float, content: @Composable () -> Unit) {
     CompositionLocalProvider(
         value =
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
index 98e459b..b2fcee3 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
@@ -50,6 +50,9 @@
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
 import androidx.wear.compose.foundation.lazy.inverseLerp
+import androidx.wear.compose.foundation.pager.HorizontalPager
+import androidx.wear.compose.foundation.pager.PagerState
+import androidx.wear.compose.foundation.pager.VerticalPager
 import androidx.wear.compose.foundation.rememberActiveFocusRequester
 import kotlin.math.abs
 import kotlin.math.absoluteValue
@@ -216,24 +219,14 @@
         layoutInfoProvider: RotarySnapLayoutInfoProvider,
         snapOffset: Dp = 0.dp,
         hapticFeedbackEnabled: Boolean = true
-    ): RotaryScrollableBehavior {
-        val isLowRes = isLowResInput()
-        val snapOffsetPx = with(LocalDensity.current) { snapOffset.roundToPx() }
-        val rotaryHaptics: RotaryHapticHandler =
-            rememberRotaryHapticHandler(scrollableState, hapticFeedbackEnabled)
-
-        return remember(scrollableState, layoutInfoProvider, rotaryHaptics, snapOffset, isLowRes) {
-            snapBehavior(
-                scrollableState,
-                layoutInfoProvider,
-                rotaryHaptics,
-                snapOffsetPx,
-                ThresholdDivider,
-                ResistanceFactor,
-                isLowRes
-            )
-        }
-    }
+    ): RotaryScrollableBehavior =
+        snapBehavior(
+            scrollableState = scrollableState,
+            layoutInfoProvider = layoutInfoProvider,
+            snapSensitivity = RotarySnapSensitivity.DEFAULT,
+            snapOffset = snapOffset,
+            hapticFeedbackEnabled = hapticFeedbackEnabled
+        )
 
     /**
      * Implementation of [RotaryScrollableBehavior] to define scrolling behaviour with snap for
@@ -259,9 +252,62 @@
                     ScalingLazyColumnRotarySnapLayoutInfoProvider(scrollableState)
                 },
             snapOffset = snapOffset,
+            snapSensitivity = RotarySnapSensitivity.DEFAULT,
             hapticFeedbackEnabled = hapticFeedbackEnabled
         )
 
+    /**
+     * Implementation of [RotaryScrollableBehavior] to define scrolling behaviour with snap for
+     * [HorizontalPager] and [VerticalPager].
+     *
+     * @param pagerState [PagerState] to which rotary scroll will be connected.
+     * @param snapOffset An optional offset to be applied when snapping the item. Defines the
+     *   distance from the center of the scrollable to the center of the snapped item.
+     * @param hapticFeedbackEnabled Controls whether haptic feedback is given during rotary
+     *   scrolling (true by default). It's recommended to keep the default value of true for premium
+     *   scrolling experience.
+     */
+    @Composable
+    fun snapBehavior(
+        pagerState: PagerState,
+        snapOffset: Dp = 0.dp,
+        hapticFeedbackEnabled: Boolean = true
+    ): RotaryScrollableBehavior {
+        return snapBehavior(
+            scrollableState = pagerState,
+            layoutInfoProvider =
+                remember(pagerState) { PagerRotarySnapLayoutInfoProvider(pagerState) },
+            snapSensitivity = RotarySnapSensitivity.HIGH,
+            snapOffset = snapOffset,
+            hapticFeedbackEnabled = hapticFeedbackEnabled
+        )
+    }
+
+    @Composable
+    private fun snapBehavior(
+        scrollableState: ScrollableState,
+        layoutInfoProvider: RotarySnapLayoutInfoProvider,
+        snapSensitivity: RotarySnapSensitivity,
+        snapOffset: Dp,
+        hapticFeedbackEnabled: Boolean
+    ): RotaryScrollableBehavior {
+        val isLowRes = isLowResInput()
+        val snapOffsetPx = with(LocalDensity.current) { snapOffset.roundToPx() }
+        val rotaryHaptics: RotaryHapticHandler =
+            rememberRotaryHapticHandler(scrollableState, hapticFeedbackEnabled)
+
+        return remember(scrollableState, layoutInfoProvider, rotaryHaptics, snapOffset, isLowRes) {
+            snapBehavior(
+                scrollableState,
+                layoutInfoProvider,
+                rotaryHaptics,
+                snapSensitivity,
+                snapOffsetPx,
+                isLowRes
+            )
+        }
+    }
+
     /** Returns whether the input is Low-res (a bezel) or high-res (a crown/rsb). */
     @Composable
     private fun isLowResInput(): Boolean =
@@ -269,9 +315,6 @@
             "android.hardware.rotaryencoder.lowres"
         )
 
-    private const val ThresholdDivider: Float = 1.5f
-    private const val ResistanceFactor: Float = 3f
-
     // These values represent the timeframe for a fling event. A bigger value is assigned
     // to low-res input due to the lower frequency of low-res rotary events.
     internal const val LowResFlingTimeframe: Long = 100L
@@ -303,6 +346,27 @@
         get() = scrollableState.layoutInfo.totalItemsCount
 }
 
+/** An implementation of rotary scroll adapter for Pager */
+internal class PagerRotarySnapLayoutInfoProvider(private val pagerState: PagerState) :
+    RotarySnapLayoutInfoProvider {
+
+    /** Calculates the average item height by just taking the pageSize. */
+    override val averageItemSize: Float
+        get() = pagerState.layoutInfo.pageSize.toFloat()
+
+    /** Current page */
+    override val currentItemIndex: Int
+        get() = pagerState.currentPage
+
+    /** The offset from the page center. */
+    override val currentItemOffset: Float
+        get() = pagerState.currentPageOffsetFraction * averageItemSize
+
+    /** The total count of items in Pager */
+    override val totalItemCount: Int
+        get() = pagerState.pageCount
+}
+
 /**
  * Handles scroll with fling.
  *
@@ -353,9 +417,7 @@
  *   usage
  * @param snapOffset An offset to be applied when snapping the item. After the snap the snapped
  *   items offset will be [snapOffset]. In pixels.
- * @param maxThresholdDivider Factor to divide item size when calculating threshold.
- * @param scrollDistanceDivider A value which is used to slow down or speed up the scroll before
- *   snap happens. The higher the value the slower the scroll.
+ * @param snapSensitivity Sensitivity of the rotary snap.
  * @param isLowRes Whether the input is Low-res (a bezel) or high-res(a crown/rsb)
  * @return A snap implementation of [RotaryScrollableBehavior] which is either suitable for low-res
  *   or high-res input (see [Modifier.rotaryScrollable] for descriptions of low-res and high-res
@@ -365,9 +427,8 @@
     scrollableState: ScrollableState,
     layoutInfoProvider: RotarySnapLayoutInfoProvider,
     rotaryHaptics: RotaryHapticHandler,
+    snapSensitivity: RotarySnapSensitivity,
     snapOffset: Int,
-    maxThresholdDivider: Float,
-    scrollDistanceDivider: Float,
     isLowRes: Boolean
 ): RotaryScrollableBehavior {
     return if (isLowRes) {
@@ -384,10 +445,11 @@
     } else {
         HighResSnapRotaryScrollableBehavior(
             rotaryHaptics = rotaryHaptics,
-            scrollDistanceDivider = scrollDistanceDivider,
+            scrollDistanceDivider = snapSensitivity.resistanceFactor,
             thresholdHandlerFactory = {
                 ThresholdHandler(
-                    maxThresholdDivider,
+                    minThresholdDivider = snapSensitivity.minThresholdDivider,
+                    maxThresholdDivider = snapSensitivity.maxThresholdDivider,
                     averageItemSize = { layoutInfoProvider.averageItemSize }
                 )
             },
@@ -1061,7 +1123,9 @@
  */
 internal class ThresholdHandler(
     // Factor to divide item size when calculating threshold.
-    // Depending on the speed, it dynamically varies in range 1..maxThresholdDivider
+    // Threshold is divided by a linear interpolation value between minThresholdDivider and
+    // maxThresholdDivider, based on the scrolling speed.
+    private val minThresholdDivider: Float,
     private val maxThresholdDivider: Float,
     // Min velocity for threshold calculation
     private val minVelocity: Float = 300f,
@@ -1105,7 +1169,8 @@
             )
         // Calculate the final threshold size by dividing the average item size by a dynamically
         // adjusted threshold divider.
-        return averageItemSize() / lerp(1f, maxThresholdDivider, thresholdDividerFraction)
+        return averageItemSize() /
+            lerp(minThresholdDivider, maxThresholdDivider, thresholdDividerFraction)
     }
 
     /**
@@ -1213,6 +1278,28 @@
     }
 }
 
+/**
+ * Enum class representing the sensitivity of the rotary scroll.
+ *
+ * It defines two types of parameters that influence scroll behavior:
+ * - min/max thresholdDivider : these parameters reduce the scroll threshold based on the speed of
+ *   rotary input, making the UI more responsive to both slow, deliberate rotations and fast flicks
+ *   of the rotary.
+ * - resistanceFactor : Used to dampen the visual scroll effect. This allows the UI to scroll less
+ *   than the actual input from the rotary device, providing a more controlled scrolling experience.
+ */
+internal enum class RotarySnapSensitivity(
+    val minThresholdDivider: Float,
+    val maxThresholdDivider: Float,
+    val resistanceFactor: Float,
+) {
+    // Default sensitivity
+    DEFAULT(1f, 1.5f, 3f),
+
+    // Used for full-screen pagers
+    HIGH(5f, 7.5f, 5f),
+}
+
 /** Debug logging that can be enabled. */
 private const val DEBUG = false
 
diff --git a/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/RotaryTest.kt b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/RotaryTest.kt
index 666080e..cb5d03c 100644
--- a/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/RotaryTest.kt
+++ b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/RotaryTest.kt
@@ -27,29 +27,42 @@
     @Test
     fun testMinVelocityThreshold() {
         val itemHeight = 100f
-        val thresholdHandler = ThresholdHandler(2.0f, averageItemSize = { itemHeight })
+        val minThresholdDivider = 1f
+        val maxThresholdDivider = 2f
+        val thresholdHandler =
+            ThresholdHandler(
+                minThresholdDivider = minThresholdDivider,
+                maxThresholdDivider = maxThresholdDivider,
+                averageItemSize = { itemHeight },
+            )
 
         thresholdHandler.startThresholdTracking(0L)
         // Simulate very slow scroll
         thresholdHandler.updateTracking(100L, 1f)
         val result = thresholdHandler.calculateSnapThreshold()
 
-        // Threshold should be equal to the height of an item
-        assertEquals(itemHeight, result, 0.01f)
+        // Threshold should be equal to the height of an item divided by minThresholdDivider
+        assertEquals(itemHeight / minThresholdDivider, result, 0.01f)
     }
 
     @Test
     fun testMaxVelocityThreshold() {
         val itemHeight = 100f
-        val thresholdDivider = 2.0f
-        val thresholdHandler = ThresholdHandler(thresholdDivider, averageItemSize = { itemHeight })
+        val minThresholdDivider = 1f
+        val maxThresholdDivider = 2f
+        val thresholdHandler =
+            ThresholdHandler(
+                minThresholdDivider = minThresholdDivider,
+                maxThresholdDivider = maxThresholdDivider,
+                averageItemSize = { itemHeight },
+            )
 
         thresholdHandler.startThresholdTracking(0L)
         // Simulate very fast scroll
         thresholdHandler.updateTracking(1L, 100f)
         val result = thresholdHandler.calculateSnapThreshold()
 
-        // Threshold should be equal to the height of an item divided by threshold
-        assertEquals(itemHeight / thresholdDivider, result, 0.01f)
+        // Threshold should be equal to the height of an item divided by maxThresholdDivider
+        assertEquals(itemHeight / maxThresholdDivider, result, 0.01f)
     }
 }
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Icon.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Icon.kt
index a52d381..b8e2346 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Icon.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/Icon.kt
@@ -46,9 +46,8 @@
  * @param modifier Optional [Modifier] for this Icon
  * @param tint Tint to be applied to [painter]. If [Color.Unspecified] is provided, then no tint is
  *   applied
- *
- * TODO:// Add link to Chip for a clickable icon
  */
+// TODO: Add link to Chip for a clickable icon
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @Composable
 fun Icon(painter: Painter, contentDescription: String?, modifier: Modifier, tint: Color) {
diff --git a/wear/compose/compose-material3/api/current.txt b/wear/compose/compose-material3/api/current.txt
index 17e1f37..0ee8e16 100644
--- a/wear/compose/compose-material3/api/current.txt
+++ b/wear/compose/compose-material3/api/current.txt
@@ -2,12 +2,13 @@
 package androidx.wear.compose.material3 {
 
   public final class AlertDialogDefaults {
-    method @androidx.compose.runtime.Composable public void BottomButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public void ConfirmButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public void DismissButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public void GroupSeparator();
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues confirmDismissContentPadding();
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding(boolean hasBottomButton);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding();
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPaddingWithEdgeButton(optional androidx.wear.compose.material3.EdgeButtonSize edgeButtonSize);
     method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getConfirmIcon();
     method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getDismissIcon();
     method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement();
@@ -18,7 +19,8 @@
   }
 
   public final class AlertDialogKt {
-    method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+    method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+    method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> edgeButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
     method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> confirmButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
   }
 
@@ -83,10 +85,6 @@
     method public androidx.compose.foundation.layout.PaddingValues getCompactButtonTapTargetPadding();
     method public float getCompactButtonVerticalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method public float getEdgeButtonHeightExtraSmall();
-    method public float getEdgeButtonHeightLarge();
-    method public float getEdgeButtonHeightMedium();
-    method public float getEdgeButtonHeightSmall();
     method public float getExtraLargeIconSize();
     method public float getHeight();
     method public float getIconSize();
@@ -109,10 +107,6 @@
     property public final androidx.compose.foundation.layout.PaddingValues CompactButtonTapTargetPadding;
     property public final float CompactButtonVerticalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property public final float EdgeButtonHeightExtraSmall;
-    property public final float EdgeButtonHeightLarge;
-    property public final float EdgeButtonHeightMedium;
-    property public final float EdgeButtonHeightSmall;
     property public final float ExtraLargeIconSize;
     property public final float Height;
     property public final float IconSize;
@@ -457,7 +451,22 @@
   }
 
   public final class EdgeButtonKt {
-    method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional float preferredHeight, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.EdgeButtonSize buttonSize, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  public final class EdgeButtonSize {
+    field public static final androidx.wear.compose.material3.EdgeButtonSize.Companion Companion;
+  }
+
+  public static final class EdgeButtonSize.Companion {
+    method public androidx.wear.compose.material3.EdgeButtonSize getExtraSmall();
+    method public androidx.wear.compose.material3.EdgeButtonSize getLarge();
+    method public androidx.wear.compose.material3.EdgeButtonSize getMedium();
+    method public androidx.wear.compose.material3.EdgeButtonSize getSmall();
+    property public final androidx.wear.compose.material3.EdgeButtonSize ExtraSmall;
+    property public final androidx.wear.compose.material3.EdgeButtonSize Large;
+    property public final androidx.wear.compose.material3.EdgeButtonSize Medium;
+    property public final androidx.wear.compose.material3.EdgeButtonSize Small;
   }
 
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This Wear Material3 API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWearMaterial3Api {
@@ -725,8 +734,8 @@
   }
 
   public final class PagerScaffoldKt {
-    method @androidx.compose.runtime.Composable public static void HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? pageIndicator, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? pageIndicatorAnimationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? pageIndicator, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? pageIndicatorAnimationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? pageIndicator, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? pageIndicatorAnimationSpec, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? pageIndicator, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? pageIndicatorAnimationSpec, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
   }
 
   public final class PickerDefaults {
@@ -901,6 +910,11 @@
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
   }
 
+  public final class ScreenScaffoldDefaults {
+    method public androidx.compose.foundation.layout.PaddingValues contentPaddingWithEdgeButton(androidx.wear.compose.material3.EdgeButtonSize edgeButtonSize, optional float start, optional float top, optional float end, optional float extraBottom);
+    field public static final androidx.wear.compose.material3.ScreenScaffoldDefaults INSTANCE;
+  }
+
   public final class ScreenScaffoldKt {
     method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.lazy.LazyListState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
diff --git a/wear/compose/compose-material3/api/restricted_current.txt b/wear/compose/compose-material3/api/restricted_current.txt
index 17e1f37..0ee8e16 100644
--- a/wear/compose/compose-material3/api/restricted_current.txt
+++ b/wear/compose/compose-material3/api/restricted_current.txt
@@ -2,12 +2,13 @@
 package androidx.wear.compose.material3 {
 
   public final class AlertDialogDefaults {
-    method @androidx.compose.runtime.Composable public void BottomButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public void ConfirmButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public void DismissButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public void GroupSeparator();
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues confirmDismissContentPadding();
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding(boolean hasBottomButton);
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPadding();
+    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.PaddingValues contentPaddingWithEdgeButton(optional androidx.wear.compose.material3.EdgeButtonSize edgeButtonSize);
     method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getConfirmIcon();
     method public kotlin.jvm.functions.Function1<androidx.compose.foundation.layout.RowScope,kotlin.Unit> getDismissIcon();
     method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement();
@@ -18,7 +19,8 @@
   }
 
   public final class AlertDialogKt {
-    method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+    method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
+    method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> edgeButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
     method @androidx.compose.runtime.Composable public static void AlertDialog(boolean show, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> confirmButton, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.window.DialogProperties properties, optional kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.lazy.ScalingLazyListScope,kotlin.Unit>? content);
   }
 
@@ -83,10 +85,6 @@
     method public androidx.compose.foundation.layout.PaddingValues getCompactButtonTapTargetPadding();
     method public float getCompactButtonVerticalPadding();
     method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
-    method public float getEdgeButtonHeightExtraSmall();
-    method public float getEdgeButtonHeightLarge();
-    method public float getEdgeButtonHeightMedium();
-    method public float getEdgeButtonHeightSmall();
     method public float getExtraLargeIconSize();
     method public float getHeight();
     method public float getIconSize();
@@ -109,10 +107,6 @@
     property public final androidx.compose.foundation.layout.PaddingValues CompactButtonTapTargetPadding;
     property public final float CompactButtonVerticalPadding;
     property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
-    property public final float EdgeButtonHeightExtraSmall;
-    property public final float EdgeButtonHeightLarge;
-    property public final float EdgeButtonHeightMedium;
-    property public final float EdgeButtonHeightSmall;
     property public final float ExtraLargeIconSize;
     property public final float Height;
     property public final float IconSize;
@@ -457,7 +451,22 @@
   }
 
   public final class EdgeButtonKt {
-    method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional float preferredHeight, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void EdgeButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material3.EdgeButtonSize buttonSize, optional boolean enabled, optional androidx.wear.compose.material3.ButtonColors colors, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  public final class EdgeButtonSize {
+    field public static final androidx.wear.compose.material3.EdgeButtonSize.Companion Companion;
+  }
+
+  public static final class EdgeButtonSize.Companion {
+    method public androidx.wear.compose.material3.EdgeButtonSize getExtraSmall();
+    method public androidx.wear.compose.material3.EdgeButtonSize getLarge();
+    method public androidx.wear.compose.material3.EdgeButtonSize getMedium();
+    method public androidx.wear.compose.material3.EdgeButtonSize getSmall();
+    property public final androidx.wear.compose.material3.EdgeButtonSize ExtraSmall;
+    property public final androidx.wear.compose.material3.EdgeButtonSize Large;
+    property public final androidx.wear.compose.material3.EdgeButtonSize Medium;
+    property public final androidx.wear.compose.material3.EdgeButtonSize Small;
   }
 
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This Wear Material3 API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWearMaterial3Api {
@@ -725,8 +734,8 @@
   }
 
   public final class PagerScaffoldKt {
-    method @androidx.compose.runtime.Composable public static void HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? pageIndicator, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? pageIndicatorAnimationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? pageIndicator, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? pageIndicatorAnimationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void HorizontalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? pageIndicator, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? pageIndicatorAnimationSpec, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void VerticalPagerScaffold(androidx.wear.compose.foundation.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? pageIndicator, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? pageIndicatorAnimationSpec, optional androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior? rotaryScrollableBehavior, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.pager.PagerScope,? super java.lang.Integer,kotlin.Unit> content);
   }
 
   public final class PickerDefaults {
@@ -901,6 +910,11 @@
     method @androidx.compose.runtime.Stable public static androidx.compose.foundation.IndicationNodeFactory ripple(optional boolean bounded, optional float radius, optional long color);
   }
 
+  public final class ScreenScaffoldDefaults {
+    method public androidx.compose.foundation.layout.PaddingValues contentPaddingWithEdgeButton(androidx.wear.compose.material3.EdgeButtonSize edgeButtonSize, optional float start, optional float top, optional float end, optional float extraBottom);
+    field public static final androidx.wear.compose.material3.ScreenScaffoldDefaults INSTANCE;
+  }
+
   public final class ScreenScaffoldKt {
     method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.lazy.LazyListState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? bottomButton, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void ScreenScaffold(androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? timeText, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit>? scrollIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
index c8618c6..59d5d83 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AlertDialogs.kt
@@ -52,13 +52,13 @@
 import androidx.wear.compose.material3.ScreenScaffold
 import androidx.wear.compose.material3.SwitchButton
 import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.samples.AlertDialogWithBottomButtonSample
 import androidx.wear.compose.material3.samples.AlertDialogWithConfirmAndDismissSample
 import androidx.wear.compose.material3.samples.AlertDialogWithContentGroupsSample
+import androidx.wear.compose.material3.samples.AlertDialogWithEdgeButtonSample
 
 val AlertDialogs =
     listOf(
-        ComposableDemo("Bottom button") { AlertDialogWithBottomButtonSample() },
+        ComposableDemo("Edge button") { AlertDialogWithEdgeButtonSample() },
         ComposableDemo("Confirm and Dismiss") { AlertDialogWithConfirmAndDismissSample() },
         ComposableDemo("Content groups") { AlertDialogWithContentGroupsSample() },
         ComposableDemo("Button stack") { AlertDialogWithButtonStack() },
@@ -73,7 +73,7 @@
     var showMessage by remember { mutableStateOf(false) }
     var showSecondaryButton by remember { mutableStateOf(false) }
     var showCaption by remember { mutableStateOf(false) }
-    var buttonsType by remember { mutableStateOf(AlertButtonsType.BOTTOM_BUTTON) }
+    var buttonsType by remember { mutableStateOf(AlertButtonsType.EDGE_BUTTON) }
 
     var showDialog by remember { mutableStateOf(false) }
 
@@ -127,9 +127,9 @@
             item {
                 RadioButton(
                     modifier = Modifier.fillMaxWidth(),
-                    selected = buttonsType == AlertButtonsType.BOTTOM_BUTTON,
-                    onSelect = { buttonsType = AlertButtonsType.BOTTOM_BUTTON },
-                    label = { Text("Single Bottom button") },
+                    selected = buttonsType == AlertButtonsType.EDGE_BUTTON,
+                    onSelect = { buttonsType = AlertButtonsType.EDGE_BUTTON },
+                    label = { Text("Single EdgeButton") },
                 )
             }
             item {
@@ -145,7 +145,7 @@
                     modifier = Modifier.fillMaxWidth(),
                     selected = buttonsType == AlertButtonsType.NO_BUTTONS,
                     onSelect = { buttonsType = AlertButtonsType.NO_BUTTONS },
-                    label = { Text("No bottom button") },
+                    label = { Text("No EdgeButton") },
                 )
             }
             item { Button(onClick = { showDialog = true }, label = { Text("Show dialog") }) }
@@ -190,8 +190,7 @@
             )
         },
         title = { Text("Allow access to your photos?") },
-        text = { Text("Lerp ipsum dolor sit amet.") },
-        bottomButton = null,
+        text = { Text("Lerp ipsum dolor sit amet.") }
     ) {
         item {
             Button(
@@ -258,8 +257,8 @@
             if (buttonsType == AlertButtonsType.CONFIRM_DISMISS) {
                 { /* dismiss action */ }
             } else null,
-        onBottomButton =
-            if (buttonsType == AlertButtonsType.BOTTOM_BUTTON) {
+        onEdgeButton =
+            if (buttonsType == AlertButtonsType.EDGE_BUTTON) {
                 onConfirmButton
             } else null,
         content =
@@ -270,7 +269,7 @@
                     }
                     if (showCaption) {
                         item { Caption(captionHorizontalPadding) }
-                        if (buttonsType == AlertButtonsType.BOTTOM_BUTTON) {
+                        if (buttonsType == AlertButtonsType.EDGE_BUTTON) {
                             item { AlertDialogDefaults.GroupSeparator() }
                         }
                     }
@@ -331,7 +330,7 @@
     message: @Composable (() -> Unit)?,
     onDismissButton: (() -> Unit)?,
     onConfirmButton: (() -> Unit)?,
-    onBottomButton: (() -> Unit)?,
+    onEdgeButton: (() -> Unit)?,
     content: (ScalingLazyListScope.() -> Unit)?
 ) {
     if (onConfirmButton != null && onDismissButton != null) {
@@ -347,7 +346,7 @@
             dismissButton = { AlertDialogDefaults.DismissButton(onDismissButton) },
             content = content
         )
-    } else
+    } else if (onEdgeButton != null) {
         AlertDialog(
             show = show,
             onDismissRequest = onDismissRequest,
@@ -356,16 +355,25 @@
             title = title,
             icon = icon,
             text = message,
-            bottomButton =
-                if (onBottomButton != null) {
-                    { AlertDialogDefaults.BottomButton(onBottomButton) }
-                } else null,
+            edgeButton = { AlertDialogDefaults.EdgeButton(onEdgeButton) },
             content = content
         )
+    } else {
+        AlertDialog(
+            show = show,
+            onDismissRequest = onDismissRequest,
+            modifier = modifier,
+            properties = properties,
+            title = title,
+            icon = icon,
+            text = message,
+            content = content
+        )
+    }
 }
 
 private enum class AlertButtonsType {
     NO_BUTTONS,
-    BOTTOM_BUTTON,
+    EDGE_BUTTON,
     CONFIRM_DISMISS
 }
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
index ba4a754..b12498f 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
@@ -45,8 +44,10 @@
 import androidx.wear.compose.material3.ButtonDefaults
 import androidx.wear.compose.material3.Card
 import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.EdgeButtonSize
 import androidx.wear.compose.material3.RadioButton
 import androidx.wear.compose.material3.ScreenScaffold
+import androidx.wear.compose.material3.ScreenScaffoldDefaults
 import androidx.wear.compose.material3.Text
 import androidx.wear.compose.material3.TextButton
 import androidx.wear.compose.material3.TextButtonDefaults
@@ -72,7 +73,7 @@
             bottomButton = {
                 EdgeButton(
                     onClick = {},
-                    preferredHeight = ButtonDefaults.EdgeButtonHeightLarge,
+                    buttonSize = EdgeButtonSize.Large,
                     colors = ButtonDefaults.buttonColors(containerColor = Color.DarkGray)
                 ) {
                     Text(labels[selectedLabel.intValue], color = Color.White)
@@ -83,7 +84,14 @@
                 state = state,
                 modifier = Modifier.fillMaxSize(),
                 verticalArrangement = Arrangement.spacedBy(4.dp),
-                contentPadding = PaddingValues(10.dp, 20.dp, 10.dp, 80.dp),
+                contentPadding =
+                    ScreenScaffoldDefaults.contentPaddingWithEdgeButton(
+                        edgeButtonSize = EdgeButtonSize.Medium,
+                        10.dp,
+                        20.dp,
+                        10.dp,
+                        10.dp
+                    ),
                 horizontalAlignment = Alignment.CenterHorizontally
             ) {
                 items(labels.size) {
@@ -120,7 +128,7 @@
             bottomButton = {
                 EdgeButton(
                     onClick = {},
-                    preferredHeight = ButtonDefaults.EdgeButtonHeightLarge,
+                    buttonSize = EdgeButtonSize.Large,
                     colors = ButtonDefaults.buttonColors(containerColor = Color.DarkGray)
                 ) {
                     Text(labels[selectedLabel.intValue], color = Color.White)
@@ -131,7 +139,14 @@
                 state = state,
                 modifier = Modifier.fillMaxSize(),
                 autoCentering = null,
-                contentPadding = PaddingValues(10.dp, 20.dp, 10.dp, 100.dp),
+                contentPadding =
+                    ScreenScaffoldDefaults.contentPaddingWithEdgeButton(
+                        edgeButtonSize = EdgeButtonSize.Medium,
+                        10.dp,
+                        20.dp,
+                        10.dp,
+                        10.dp
+                    ),
                 horizontalAlignment = Alignment.CenterHorizontally
             ) {
                 items(labels.size) {
@@ -152,10 +167,10 @@
 fun EdgeButtonMultiDemo() {
     val sizes =
         listOf(
-            ButtonDefaults.EdgeButtonHeightExtraSmall,
-            ButtonDefaults.EdgeButtonHeightSmall,
-            ButtonDefaults.EdgeButtonHeightMedium,
-            ButtonDefaults.EdgeButtonHeightLarge
+            EdgeButtonSize.ExtraSmall,
+            EdgeButtonSize.Small,
+            EdgeButtonSize.Medium,
+            EdgeButtonSize.Large
         )
     val sizeNames = listOf("XS", "S", "M", "L")
     var size by remember { mutableIntStateOf(0) }
@@ -215,7 +230,7 @@
             EdgeButton(
                 onClick = {},
                 enabled = colorNames[color] != "D",
-                preferredHeight = sizes[size],
+                buttonSize = sizes[size],
                 colors = colors[color],
                 border =
                     if (colorNames[color] == "O")
@@ -232,10 +247,10 @@
 fun EdgeButtonConfigurableDemo() {
     val sizes =
         listOf(
-            "Extra Small" to ButtonDefaults.EdgeButtonHeightExtraSmall,
-            "Small" to ButtonDefaults.EdgeButtonHeightSmall,
-            "Medium" to ButtonDefaults.EdgeButtonHeightMedium,
-            "Large" to ButtonDefaults.EdgeButtonHeightLarge
+            "Extra Small" to EdgeButtonSize.ExtraSmall,
+            "Small" to EdgeButtonSize.Small,
+            "Medium" to EdgeButtonSize.Medium,
+            "Large" to EdgeButtonSize.Large,
         )
     var selectedSize by remember { mutableIntStateOf(0) }
     val colors =
@@ -257,7 +272,7 @@
             bottomButton = {
                 EdgeButton(
                     onClick = {},
-                    preferredHeight = sizes[selectedSize].second,
+                    buttonSize = sizes[selectedSize].second,
                     colors = colors[selectedColor].second,
                     border =
                         if (colors[selectedColor].first == "Outlined")
@@ -278,7 +293,12 @@
                 modifier = Modifier.fillMaxSize(),
                 autoCentering = null,
                 contentPadding =
-                    PaddingValues(10.dp, 20.dp, 10.dp, sizes[selectedSize].second + 6.dp),
+                    ScreenScaffoldDefaults.contentPaddingWithEdgeButton(
+                        sizes[selectedSize].second,
+                        10.dp,
+                        20.dp,
+                        10.dp
+                    ),
                 horizontalAlignment = Alignment.CenterHorizontally
             ) {
                 selection(
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/AlertDialogSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/AlertDialogSample.kt
index dae4eef..6308c19 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/AlertDialogSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/AlertDialogSample.kt
@@ -84,7 +84,7 @@
 
 @Sampled
 @Composable
-fun AlertDialogWithBottomButtonSample() {
+fun AlertDialogWithEdgeButtonSample() {
     var showDialog by remember { mutableStateOf(false) }
 
     Box(Modifier.fillMaxSize()) {
@@ -107,8 +107,8 @@
             )
         },
         title = { Text("Mobile network is not currently available") },
-        bottomButton = {
-            AlertDialogDefaults.BottomButton(
+        edgeButton = {
+            AlertDialogDefaults.EdgeButton(
                 onClick = {
                     // Perform confirm action here
                     showDialog = false
@@ -135,8 +135,8 @@
         onDismissRequest = { showDialog = false },
         title = { Text("Share your location") },
         text = { Text(" The following apps have asked you to share your location") },
-        bottomButton = {
-            AlertDialogDefaults.BottomButton(
+        edgeButton = {
+            AlertDialogDefaults.EdgeButton(
                 onClick = {
                     // Perform confirm action here
                     showDialog = false
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/EdgeButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/EdgeButtonSample.kt
index 2aa006b..4bcebfb 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/EdgeButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/EdgeButtonSample.kt
@@ -36,6 +36,7 @@
 import androidx.wear.compose.material3.ButtonDefaults.buttonColors
 import androidx.wear.compose.material3.Card
 import androidx.wear.compose.material3.EdgeButton
+import androidx.wear.compose.material3.EdgeButtonSize
 import androidx.wear.compose.material3.Icon
 import androidx.wear.compose.material3.ScreenScaffold
 import androidx.wear.compose.material3.Text
@@ -47,7 +48,7 @@
         Text("Confirm", Modifier.align(Alignment.Center))
         EdgeButton(
             onClick = { /* Do something */ },
-            preferredHeight = ButtonDefaults.EdgeButtonHeightMedium,
+            buttonSize = EdgeButtonSize.Medium,
             modifier = Modifier.align(Alignment.BottomEnd)
         ) {
             Icon(
@@ -68,7 +69,7 @@
         bottomButton = {
             EdgeButton(
                 onClick = {},
-                preferredHeight = ButtonDefaults.EdgeButtonHeightLarge,
+                buttonSize = EdgeButtonSize.Large,
                 colors = buttonColors(containerColor = Color.DarkGray)
             ) {
                 Text("Ok", textAlign = TextAlign.Center)
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
index b593267..a02f665 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
@@ -307,7 +307,7 @@
             title = title,
             icon = icon,
             text = text,
-            bottomButton = { AlertDialogDefaults.BottomButton({}) },
+            edgeButton = { AlertDialogDefaults.EdgeButton({}) },
             content = content
         )
     }
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
index a987c30..bac176a 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
@@ -58,7 +58,7 @@
             AlertDialog(
                 modifier = Modifier.testTag(TEST_TAG),
                 title = {},
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -87,7 +87,7 @@
             AlertDialog(
                 icon = { TestImage(TEST_TAG) },
                 title = {},
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -115,7 +115,7 @@
         rule.setContentWithTheme {
             AlertDialog(
                 title = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -143,7 +143,7 @@
             AlertDialog(
                 title = {},
                 text = { Text("Text", modifier = Modifier.testTag(TEST_TAG)) },
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -169,7 +169,7 @@
     @Test
     fun displays_content_with_bottomButton() {
         rule.setContentWithTheme {
-            AlertDialog(title = {}, bottomButton = {}, onDismissRequest = {}, show = true) {
+            AlertDialog(title = {}, edgeButton = {}, onDismissRequest = {}, show = true) {
                 item { Text("Text", modifier = Modifier.testTag(TEST_TAG)) }
             }
         }
@@ -225,7 +225,7 @@
         rule.setContentWithTheme {
             AlertDialog(
                 title = {},
-                bottomButton = { Button(onClick = {}, modifier = Modifier.testTag(TEST_TAG)) {} },
+                edgeButton = { Button(onClick = {}, modifier = Modifier.testTag(TEST_TAG)) {} },
                 onDismissRequest = {},
                 show = true
             )
@@ -264,7 +264,7 @@
             AlertDialog(
                 modifier = Modifier.testTag(TEST_TAG),
                 title = {},
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {
                     showDialog = false
                     dismissCounter++
@@ -287,7 +287,7 @@
             AlertDialog(
                 modifier = Modifier.testTag(TEST_TAG),
                 title = {},
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = { dismissCounter++ },
                 show = show.value
             )
@@ -304,7 +304,7 @@
             AlertDialog(
                 modifier = Modifier.testTag(TEST_TAG),
                 title = {},
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = false
             )
@@ -338,7 +338,7 @@
                     actualTextAlign = LocalTextConfiguration.current.textAlign
                     actualTextMaxLines = LocalTextConfiguration.current.maxLines
                 },
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -373,7 +373,7 @@
                     actualTextStyle = LocalTextStyle.current
                     actualTextAlign = LocalTextConfiguration.current.textAlign
                 },
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -394,7 +394,7 @@
             AlertDialog(
                 modifier = Modifier.testTag(TEST_TAG),
                 title = { Text("Title", color = expectedContentColor) },
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -413,7 +413,7 @@
                 modifier = Modifier.testTag(TEST_TAG),
                 title = {},
                 text = { Text("Text", color = expectedContentColor) },
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -431,7 +431,7 @@
             AlertDialog(
                 modifier = Modifier.testTag(TEST_TAG).background(expectedBackgroundColor),
                 title = {},
-                bottomButton = {},
+                edgeButton = {},
                 onDismissRequest = {},
                 show = true
             )
@@ -474,7 +474,6 @@
                     show = true,
                     title = { Text("Title") },
                     onDismissRequest = {},
-                    bottomButton = null,
                     verticalArrangement =
                         Arrangement.spacedBy(space = 0.dp, alignment = Alignment.CenterVertically),
                     modifier = Modifier.size(SmallScreenSize.dp).testTag(TEST_TAG),
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
index 7eada56..9409890 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/EdgeButtonScreenshotTest.kt
@@ -64,50 +64,39 @@
 
     @Test
     fun edge_button_xsmall() = verifyScreenshot {
-        BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightExtraSmall)
+        BasicEdgeButton(buttonSize = EdgeButtonSize.ExtraSmall)
     }
 
     @Test
     fun edge_button_small() =
-        verifyScreenshot() { BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightSmall) }
+        verifyScreenshot() { BasicEdgeButton(buttonSize = EdgeButtonSize.Small) }
 
     @Test
     fun edge_button_medium() =
-        verifyScreenshot() { BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightMedium) }
+        verifyScreenshot() { BasicEdgeButton(buttonSize = EdgeButtonSize.Medium) }
 
     @Test
     fun edge_button_large() =
-        verifyScreenshot() { BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightLarge) }
+        verifyScreenshot() { BasicEdgeButton(buttonSize = EdgeButtonSize.Large) }
 
     @Test
     fun edge_button_disabled() =
-        verifyScreenshot() {
-            BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightMedium, enabled = false)
-        }
+        verifyScreenshot() { BasicEdgeButton(buttonSize = EdgeButtonSize.Medium, enabled = false) }
 
     @Test
     fun edge_button_small_space_very_limited() =
         verifyScreenshot() {
-            BasicEdgeButton(
-                buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
-                constrainedHeight = 10.dp
-            )
+            BasicEdgeButton(buttonSize = EdgeButtonSize.Small, constrainedHeight = 10.dp)
         }
 
     @Test
     fun edge_button_small_space_limited() = verifyScreenshot {
-        BasicEdgeButton(
-            buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
-            constrainedHeight = 30.dp
-        )
+        BasicEdgeButton(buttonSize = EdgeButtonSize.Small, constrainedHeight = 30.dp)
     }
 
     @Test
     fun edge_button_small_slightly_limited() = verifyScreenshot {
-        BasicEdgeButton(
-            buttonHeight = ButtonDefaults.EdgeButtonHeightSmall,
-            constrainedHeight = 40.dp
-        )
+        BasicEdgeButton(buttonSize = EdgeButtonSize.Small, constrainedHeight = 40.dp)
     }
 
     private val LONG_TEXT =
@@ -116,17 +105,17 @@
 
     @Test
     fun edge_button_xsmall_long_text() = verifyScreenshot {
-        BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightExtraSmall, text = LONG_TEXT)
+        BasicEdgeButton(buttonSize = EdgeButtonSize.ExtraSmall, text = LONG_TEXT)
     }
 
     @Test
     fun edge_button_large_long_text() = verifyScreenshot {
-        BasicEdgeButton(buttonHeight = ButtonDefaults.EdgeButtonHeightLarge, text = LONG_TEXT)
+        BasicEdgeButton(buttonSize = EdgeButtonSize.Large, text = LONG_TEXT)
     }
 
     @Composable
     private fun BasicEdgeButton(
-        buttonHeight: Dp,
+        buttonSize: EdgeButtonSize,
         constrainedHeight: Dp? = null,
         enabled: Boolean = true,
         text: String = "Text"
@@ -135,7 +124,7 @@
             EdgeButton(
                 onClick = { /* Do something */ },
                 enabled = enabled,
-                preferredHeight = buttonHeight,
+                buttonSize = buttonSize,
                 modifier =
                     Modifier.align(Alignment.BottomEnd)
                         .testTag(TEST_TAG)
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
index 5fd2645..10cfeea 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
@@ -46,6 +46,7 @@
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.material3.AlertDialogDefaults.edgeButtonExtraTopPadding
 import androidx.wear.compose.material3.PaddingDefaults.horizontalContentPadding
 import androidx.wear.compose.material3.PaddingDefaults.verticalContentPadding
 import androidx.wear.compose.materialcore.isSmallScreen
@@ -124,23 +125,9 @@
  * information, or help users accomplish a task. The dialog is scrollable by default if the content
  * exceeds the viewport height.
  *
- * This overload has a single slot for a confirm [EdgeButton] at the bottom of the dialog. It should
- * be used when the user will be presented with either a single acknowledgement, or a stack of
- * options.
- *
- * Example of an [AlertDialog] with an icon, title, text and bottom [EdgeButton]:
- *
- * @sample androidx.wear.compose.material3.samples.AlertDialogWithBottomButtonSample
- *
- * Example of an [AlertDialog] with content groups and a bottom [EdgeButton]:
- *
- * @sample androidx.wear.compose.material3.samples.AlertDialogWithContentGroupsSample
  * @param show A boolean indicating whether the dialog should be displayed.
  * @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping to
  *   the right or by other dismiss action.
- * @param bottomButton Optional slot for a [EdgeButton] indicating positive sentiment. Clicking the
- *   button must remove the dialog from the composition hierarchy e.g. by setting [show] to false.
- *   It's recommended to use [AlertDialogDefaults.BottomButton] in this slot with onClick callback.
  * @param title A slot for displaying the title of the dialog. Title should contain a summary of the
  *   dialog's purpose or content and should not exceed 3 lines of text.
  * @param modifier Modifier to be applied to the dialog content.
@@ -159,13 +146,12 @@
 fun AlertDialog(
     show: Boolean,
     onDismissRequest: () -> Unit,
-    bottomButton: (@Composable BoxScope.() -> Unit)?,
     title: @Composable () -> Unit,
     modifier: Modifier = Modifier,
     icon: @Composable (() -> Unit)? = null,
     text: @Composable (() -> Unit)? = null,
     verticalArrangement: Arrangement.Vertical = AlertDialogDefaults.VerticalArrangement,
-    contentPadding: PaddingValues = AlertDialogDefaults.contentPadding(bottomButton != null),
+    contentPadding: PaddingValues = AlertDialogDefaults.contentPadding(),
     properties: DialogProperties = DialogProperties(),
     content: (ScalingLazyListScope.() -> Unit)? = null
 ) {
@@ -179,29 +165,100 @@
         title = title,
         icon = icon,
         text = text,
-        alertButtonsParams =
-            if (bottomButton != null) AlertButtonsParams.BottomButton(bottomButton)
-            else AlertButtonsParams.NoButtons,
+        alertButtonsParams = AlertButtonsParams.NoButtons,
+        content = content
+    )
+}
+
+/**
+ * Dialogs provide important prompts in a user flow. They can require an action, communicate
+ * information, or help users accomplish a task. The dialog is scrollable by default if the content
+ * exceeds the viewport height.
+ *
+ * This overload has a single slot for a confirm [EdgeButton] at the bottom of the dialog. It should
+ * be used when the user will be presented with a single acknowledgement.
+ *
+ * Example of an [AlertDialog] with an icon, title, text and bottom [EdgeButton]:
+ *
+ * @sample androidx.wear.compose.material3.samples.AlertDialogWithEdgeButtonSample
+ *
+ * Example of an [AlertDialog] with content groups and a bottom [EdgeButton]:
+ *
+ * @sample androidx.wear.compose.material3.samples.AlertDialogWithContentGroupsSample
+ * @param show A boolean indicating whether the dialog should be displayed.
+ * @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping to
+ *   the right or by other dismiss action.
+ * @param edgeButton Slot for a [EdgeButton] indicating positive sentiment. Clicking the button must
+ *   remove the dialog from the composition hierarchy e.g. by setting [show] to false. It's
+ *   recommended to use [AlertDialogDefaults.EdgeButton] in this slot with onClick callback. Note
+ *   that when using a [EdgeButton] which is not Medium size, the contentPadding parameters should
+ *   be specified.
+ * @param title A slot for displaying the title of the dialog. Title should contain a summary of the
+ *   dialog's purpose or content and should not exceed 3 lines of text.
+ * @param modifier Modifier to be applied to the dialog content.
+ * @param icon Optional slot for an icon to be shown at the top of the dialog.
+ * @param text Optional slot for displaying the message of the dialog below the title. Should
+ *   contain additional text that presents further details about the dialog's purpose if the title
+ *   is insufficient.
+ * @param verticalArrangement The vertical arrangement of the dialog's children. There is a default
+ *   padding between icon, title, and text, which will be added to the spacing specified in this
+ *   [verticalArrangement] parameter.
+ * @param contentPadding The padding to apply around the entire dialog's contents. Ensure there is
+ *   enough space for the [EdgeButton], for example, using
+ *   [AlertDialogDefaults.contentPaddingWithEdgeButton]
+ * @param properties An optional [DialogProperties] object for configuring the dialog's behavior.
+ * @param content A slot for additional content, displayed within a scrollable [ScalingLazyColumn].
+ */
+@Composable
+fun AlertDialog(
+    show: Boolean,
+    onDismissRequest: () -> Unit,
+    edgeButton: (@Composable BoxScope.() -> Unit),
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (() -> Unit)? = null,
+    text: @Composable (() -> Unit)? = null,
+    verticalArrangement: Arrangement.Vertical = AlertDialogDefaults.VerticalArrangement,
+    contentPadding: PaddingValues = AlertDialogDefaults.contentPaddingWithEdgeButton(),
+    properties: DialogProperties = DialogProperties(),
+    content: (ScalingLazyListScope.() -> Unit)? = null
+) {
+    AlertDialogImpl(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        modifier = modifier,
+        properties = properties,
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding,
+        title = title,
+        icon = icon,
+        text = text,
+        alertButtonsParams = AlertButtonsParams.EdgeButton(edgeButton),
         content = content
     )
 }
 
 /** Contains the default values used by [AlertDialog] */
 object AlertDialogDefaults {
-
     /**
-     * Default composable for the bottom button in an [AlertDialog]. Should be used with
-     * [AlertDialog] overload which contains a single bottomButton slot.
+     * Default composable for the edge button in an [AlertDialog]. This is a medium sized
+     * [EdgeButton]. Should be used with [AlertDialog] overload which contains a single edgeButton
+     * slot.
      *
      * @param onClick The callback to be invoked when the button is clicked.
+     * @param modifier The [Modifier] to be applied to the button.
      * @param content The composable content of the button. Defaults to [ConfirmIcon].
      */
     @Composable
-    fun BottomButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit = ConfirmIcon) {
+    fun EdgeButton(
+        onClick: () -> Unit,
+        modifier: Modifier = Modifier,
+        content: @Composable RowScope.() -> Unit = ConfirmIcon
+    ) {
         EdgeButton(
-            modifier = Modifier.padding(top = edgeButtonExtraTopPadding),
+            modifier = modifier,
             onClick = onClick,
-            preferredHeight = ButtonDefaults.EdgeButtonHeightMedium,
+            buttonSize = EdgeButtonSize.Medium,
             content = content
         )
     }
@@ -267,19 +324,36 @@
     }
 
     /**
-     * The padding to apply around the content for the [AlertDialog] variation with a stack of
-     * options and optional bottom button.
+     * The padding to apply around the content for the [AlertDialog] variation with a bottom button.
+     * If you need to configure custom paddings, consider using
+     * [ScreenScaffoldDefaults.contentPaddingWithEdgeButton]
      */
     @Composable
-    fun contentPadding(hasBottomButton: Boolean): PaddingValues {
+    fun contentPaddingWithEdgeButton(
+        edgeButtonSize: EdgeButtonSize = EdgeButtonSize.Medium
+    ): PaddingValues {
         val topPadding = verticalContentPadding()
         val horizontalPadding = horizontalContentPadding()
-        val bottomPadding =
-            if (hasBottomButton) edgeButtonHeightWithPadding
-            else screenHeightDp().dp * noEdgeButtonBottomPaddingFraction
+        return ScreenScaffoldDefaults.contentPaddingWithEdgeButton(
+            edgeButtonSize,
+            top = topPadding,
+            start = horizontalPadding,
+            end = horizontalPadding,
+            extraBottom = edgeButtonExtraTopPadding
+        )
+    }
+
+    /**
+     * The padding to apply around the content for the [AlertDialog] variation with a stack of
+     * options and no buttons at the end.
+     */
+    @Composable
+    fun contentPadding(): PaddingValues {
+        val topPadding = verticalContentPadding()
+        val horizontalPadding = horizontalContentPadding()
         return PaddingValues(
             top = topPadding,
-            bottom = bottomPadding,
+            bottom = screenHeightDp().dp * noEdgeButtonBottomPaddingFraction,
             start = horizontalPadding,
             end = horizontalPadding,
         )
@@ -317,8 +391,7 @@
     }
 
     /** The extra top padding to apply to the edge button. */
-    private val edgeButtonExtraTopPadding = 1.dp
-    private val edgeButtonHeightWithPadding = ButtonDefaults.EdgeButtonHeightMedium + 7.dp
+    internal val edgeButtonExtraTopPadding = 1.dp
     internal val noEdgeButtonBottomPaddingFraction = 0.3646f
     internal val cancelButtonPadding = 1.dp
 }
@@ -348,9 +421,14 @@
             scrollState = state,
             modifier = modifier,
             bottomButton =
-                if (alertButtonsParams is AlertButtonsParams.BottomButton)
-                    alertButtonsParams.bottomButton
-                else null
+                if (alertButtonsParams is AlertButtonsParams.EdgeButton) {
+                    {
+                        Box(
+                            Modifier.padding(top = edgeButtonExtraTopPadding),
+                            content = alertButtonsParams.edgeButton
+                        )
+                    }
+                } else null
         ) {
             ScalingLazyColumn(
                 state = state,
@@ -375,7 +453,7 @@
                 when (alertButtonsParams) {
                     is AlertButtonsParams.ConfirmDismissButtons ->
                         item { ConfirmDismissButtons(alertButtonsParams) }
-                    is AlertButtonsParams.BottomButton ->
+                    is AlertButtonsParams.EdgeButton ->
                         if (content == null) {
                             item { Spacer(Modifier.height(AlertBottomSpacing)) }
                         }
@@ -451,8 +529,8 @@
 private sealed interface AlertButtonsParams {
     object NoButtons : AlertButtonsParams
 
-    class BottomButton(
-        val bottomButton: @Composable BoxScope.() -> Unit,
+    class EdgeButton(
+        val edgeButton: @Composable BoxScope.() -> Unit,
     ) : AlertButtonsParams
 
     class ConfirmDismissButtons(
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 1fddec4..7c6dc37 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
@@ -112,9 +112,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content Slot for composable body content displayed on the Button
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun Button(
     onClick: () -> Unit,
@@ -191,9 +190,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content Slot for composable body content displayed on the Button
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun FilledTonalButton(
     onClick: () -> Unit,
@@ -269,9 +267,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content Slot for composable body content displayed on the OutlinedButton
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun OutlinedButton(
     onClick: () -> Unit,
@@ -347,9 +344,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content Slot for composable body content displayed on the ChildButton
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun ChildButton(
     onClick: () -> Unit,
@@ -443,9 +439,8 @@
  *   still happen internally.
  * @param label A slot for providing the button's main label. The contents are expected to be text
  *   which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun Button(
     onClick: () -> Unit,
@@ -564,9 +559,8 @@
  *   still happen internally.
  * @param label A slot for providing the button's main label. The contents are expected to be text
  *   which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun FilledTonalButton(
     onClick: () -> Unit,
@@ -680,9 +674,8 @@
  *   still happen internally.
  * @param label A slot for providing the button's main label. The contents are expected to be text
  *   which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun OutlinedButton(
     onClick: () -> Unit,
@@ -796,9 +789,8 @@
  *   still happen internally.
  * @param label A slot for providing the button's main label. The contents are expected to be text
  *   which is "start" aligned if there is an icon preset and "start" or "center" aligned if not.
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun ChildButton(
     onClick: () -> Unit,
@@ -933,9 +925,8 @@
  *   shape is a key characteristic of the Wear Material3 Theme
  * @param border Optional [BorderStroke] that will be used to resolve the border for this button in
  *   different states.
- *
- * TODO(b/261838497) Add Material3 samples and UX guidance links
  */
+// TODO(b/261838497) Add Material3 samples and UX guidance links
 @Composable
 fun CompactButton(
     onClick: () -> Unit,
@@ -1462,18 +1453,6 @@
      */
     val CompactButtonHeight = CompactButtonTokens.ContainerHeight
 
-    /** The height to be applied for an extra small [EdgeButton]. */
-    val EdgeButtonHeightExtraSmall = 46.dp
-
-    /** The height to be applied for a small [EdgeButton]. */
-    val EdgeButtonHeightSmall = 56.dp
-
-    /** The height to be applied for a medium [EdgeButton]. */
-    val EdgeButtonHeightMedium = 70.dp
-
-    /** The height to be applied for a large [EdgeButton]. */
-    val EdgeButtonHeightLarge = 96.dp
-
     /**
      * The default padding to be provided around a [CompactButton] in order to ensure that its
      * tappable area meets minimum UX guidance.
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
index c25b822..2070be7 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/EdgeButton.kt
@@ -107,12 +107,7 @@
  * @param modifier Modifier to be applied to the button. When animating the button to appear/
  *   disappear from the screen, a Modifier.height can be used to change the height of the component,
  *   but that won't change the space available for the content (though it may be scaled)
- * @param preferredHeight Defines the base size of the button, see the 4 standard sizes specified in
- *   [ButtonDefaults]. This is used to determine the size constraints passed on to the content, and
- *   the size of the edge button if no height Modifier is specified. Note that if a height Modifier
- *   is specified for the EdgeButton, that will determine the size of the container, and the content
- *   may be scaled and or have alpha applied to fit. The default is
- *   [ButtonDefaults.EdgeButtonHeightSmall]
+ * @param buttonSize Defines the size of the button. See [EdgeButtonSize].
  * @param enabled Controls the enabled state of the button. When `false`, this button will not be
  *   clickable
  * @param colors [ButtonColors] that will be used to resolve the background and content color for
@@ -130,7 +125,7 @@
 fun EdgeButton(
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
-    preferredHeight: Dp = ButtonDefaults.EdgeButtonHeightSmall,
+    buttonSize: EdgeButtonSize = EdgeButtonSize.Small,
     enabled: Boolean = true,
     colors: ButtonColors = ButtonDefaults.buttonColors(),
     border: BorderStroke? = null,
@@ -142,6 +137,8 @@
     val density = LocalDensity.current
     val screenWidthDp = screenWidthDp().dp
 
+    val preferredHeight = buttonSize.maximumHeight
+
     val contentShapeHelper =
         remember(preferredHeight) {
             ShapeHelper(density).apply {
@@ -269,6 +266,29 @@
     )
 }
 
+/**
+ * Size of the [EdgeButton]. This in turns determines the full shape of the edge button, including
+ * width, height, rounding radius for the top corners, the ellipsis size for the bottom part of the
+ * shape and the space available for the content.
+ */
+class EdgeButtonSize internal constructor(internal val maximumHeight: Dp) {
+    internal fun maximumHeightPlusPadding() = maximumHeight + VERTICAL_PADDING * 2
+
+    companion object {
+        /** The Size to be applied for an extra small [EdgeButton]. */
+        val ExtraSmall = EdgeButtonSize(46.dp)
+
+        /** The Size to be applied for an small [EdgeButton]. */
+        val Small = EdgeButtonSize(56.dp)
+
+        /** The Size to be applied for an medium [EdgeButton]. */
+        val Medium = EdgeButtonSize(70.dp)
+
+        /** The Size to be applied for an large [EdgeButton]. */
+        val Large = EdgeButtonSize(96.dp)
+    }
+}
+
 private fun Modifier.sizeAndOffset(rectFn: (Constraints) -> Rect) =
     layout { measurable, constraints ->
         val rect = rectFn(constraints)
@@ -294,7 +314,7 @@
 
 internal class ShapeHelper(private val density: Density) {
     private val extraSmallHeightPx =
-        with(density) { ButtonDefaults.EdgeButtonHeightExtraSmall.toPx() }
+        with(density) { EdgeButtonSize.ExtraSmall.maximumHeight.toPx() }
     private val bottomPaddingPx = with(density) { VERTICAL_PADDING.toPx() }
     private val extraSmallEllipsisHeightPx = with(density) { EXTRA_SMALL_ELLIPSIS_HEIGHT.toPx() }
     private val targetSidePadding = with(density) { TARGET_SIDE_PADDING.toPx() }
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
index 2d6ce2b..a7f7193 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/IconButton.kt
@@ -88,9 +88,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content The content displayed on the icon button, expected to be icon or image.
- *
- * TODO(b/261838497) Add Material3 samples and UX guidance links
  */
+// TODO(b/261838497) Add Material3 samples and UX guidance links
 @Composable
 fun IconButton(
     onClick: () -> Unit,
@@ -156,9 +155,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content The content displayed on the icon button, expected to be icon or image.
- *
- * TODO(b/261838497) Add Material3 samples and UX guidance links
  */
+// TODO(b/261838497) Add Material3 samples and UX guidance links
 @Composable
 fun FilledIconButton(
     onClick: () -> Unit,
@@ -225,9 +223,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content The content displayed on the icon button, expected to be icon or image.
- *
- * TODO(b/261838497) Add Material3 samples and UX guidance links
  */
+// TODO(b/261838497) Add Material3 samples and UX guidance links
 @Composable
 fun FilledTonalIconButton(
     onClick: () -> Unit,
@@ -298,9 +295,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content The content displayed on the icon button, expected to be icon or image.
- *
- * TODO(b/261838497) Add Material3 samples and UX guidance links
  */
+// TODO(b/261838497) Add Material3 samples and UX guidance links
 @Composable
 fun OutlinedIconButton(
     onClick: () -> Unit,
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 51f3b6c..22fc16e 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
@@ -49,9 +49,8 @@
  * @param shapes A set of shapes to be used by the components in this hierarchy
  * @param motionScheme a set of motion specs used to animate content for this hierarchy.
  * @param content Slot for composable content displayed with this theme
- *
- * TODO(b/273543423) Update references to Material3 design specs
  */
+// TODO(b/273543423) Update references to Material3 design specs
 @Composable
 fun MaterialTheme(
     colorScheme: ColorScheme = MaterialTheme.colorScheme,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PagerScaffold.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PagerScaffold.kt
index 2676d72..e0a017e 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PagerScaffold.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/PagerScaffold.kt
@@ -52,6 +52,8 @@
 import androidx.wear.compose.foundation.pager.PagerDefaults
 import androidx.wear.compose.foundation.pager.PagerState
 import androidx.wear.compose.foundation.pager.VerticalPager
+import androidx.wear.compose.foundation.rotary.RotaryScrollableBehavior
+import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
 import kotlin.math.absoluteValue
 
 /**
@@ -76,6 +78,9 @@
  *   conflicts with the page indicator. By default this is null, so the page indicator will be
  *   visible at all times, setting this to [PagerScaffoldDefaults.FadeOutAnimation] ensures the
  *   indicator only shows during paging, and fades out when the Pager is idle.
+ * @param rotaryScrollableBehavior Parameter for changing rotary behavior. By default rotary support
+ *   is disabled for [HorizontalPagerScaffold]. It can be enabled by passing
+ *   [RotaryScrollableDefaults.snapBehavior] with pagerState parameter.
  * @param content A composable function that takes the current page index as a parameter and defines
  *   the content to be displayed on that page.
  */
@@ -85,11 +90,18 @@
     modifier: Modifier = Modifier,
     pageIndicator: (@Composable BoxScope.() -> Unit)? = { HorizontalPageIndicator(pagerState) },
     pageIndicatorAnimationSpec: AnimationSpec<Float>? = null,
+    rotaryScrollableBehavior: RotaryScrollableBehavior? = null,
     content: @Composable PagerScope.(page: Int) -> Unit,
 ) =
     PagerScaffoldImpl(
         scrollInfoProvider = ScrollInfoProvider(pagerState, orientation = Orientation.Horizontal),
-        pagerContent = { AnimatedHorizontalPager(pagerState, content = content) },
+        pagerContent = {
+            AnimatedHorizontalPager(
+                state = pagerState,
+                rotaryScrollableBehavior = rotaryScrollableBehavior,
+                content = content
+            )
+        },
         modifier = modifier,
         pagerState = pagerState,
         pageIndicator = pageIndicator,
@@ -106,6 +118,10 @@
  * default and coordinates showing/hiding [TimeText] and [VerticalPageIndicator] according to
  * whether the Pager is being paged, this is determined by the [PagerState].
  *
+ * [VerticalPagerScaffold] supports rotary input by default. Rotary input allows users to scroll
+ * through the pager's content - by using a crown or a rotating bezel on their Wear OS device. It
+ * can be modified or turned off using the [rotaryScrollableBehavior] parameter.
+ *
  * Example of using [AppScaffold] and [VerticalPagerScaffold]:
  *
  * @sample androidx.wear.compose.material3.samples.VerticalPagerScaffoldSample
@@ -118,6 +134,9 @@
  *   conflicts with the page indicator. By default this is null, so the page indicator will be
  *   visible at all times, setting this to [PagerScaffoldDefaults.FadeOutAnimation] ensures the
  *   indicator only shows during paging, and fades out when the Pager is idle.
+ * @param rotaryScrollableBehavior Parameter for changing rotary behavior. We recommend to use
+ *   [RotaryScrollableDefaults.snapBehavior] with pagerState parameter. Passing null turns off the
+ *   rotary handling if it is not required.
  * @param content A composable function that takes the current page index as a parameter and defines
  *   the content to be displayed on that page.
  */
@@ -127,11 +146,19 @@
     modifier: Modifier = Modifier,
     pageIndicator: (@Composable BoxScope.() -> Unit)? = { VerticalPageIndicator(pagerState) },
     pageIndicatorAnimationSpec: AnimationSpec<Float>? = null,
+    rotaryScrollableBehavior: RotaryScrollableBehavior? =
+        RotaryScrollableDefaults.snapBehavior(pagerState),
     content: @Composable PagerScope.(page: Int) -> Unit,
 ) =
     PagerScaffoldImpl(
         scrollInfoProvider = ScrollInfoProvider(pagerState, orientation = Orientation.Vertical),
-        pagerContent = { AnimatedVerticalPager(pagerState, content = content) },
+        pagerContent = {
+            AnimatedVerticalPager(
+                state = pagerState,
+                rotaryScrollableBehavior = rotaryScrollableBehavior,
+                content = content
+            )
+        },
         modifier = modifier,
         pagerState = pagerState,
         pageIndicator = pageIndicator,
@@ -220,6 +247,7 @@
  *   the leftmost 25% of the screen will trigger the gesture. Even when RTL mode is enabled, this
  *   parameter only ever applies to the left edge of the screen. Setting this to 0 will disable the
  *   gesture.
+ * @param rotaryScrollableBehavior Parameter for changing rotary behavior
  * @param content A composable function that defines the content of each page displayed by the
  *   Pager. This is where the UI elements that should appear within each page should be placed.
  */
@@ -236,6 +264,7 @@
     key: ((index: Int) -> Any)? = null,
     @FloatRange(from = 0.0, to = 1.0)
     swipeToDismissEdgeZoneFraction: Float = PagerDefaults.SwipeToDismissEdgeZoneFraction,
+    rotaryScrollableBehavior: RotaryScrollableBehavior?,
     content: @Composable PagerScope.(page: Int) -> Unit
 ) {
     val touchExplorationStateProvider = remember { DefaultTouchExplorationStateProvider() }
@@ -252,6 +281,7 @@
         key = key,
         swipeToDismissEdgeZoneFraction =
             if (touchExplorationServicesEnabled) 0f else swipeToDismissEdgeZoneFraction,
+        rotaryScrollableBehavior = rotaryScrollableBehavior
     ) { page ->
         AnimatedPageContent(
             orientation = Orientation.Horizontal,
@@ -289,6 +319,7 @@
  *   position will be maintained based on the key, which means if you add/remove items before the
  *   current visible item the item with the given key will be kept as the first visible one. If null
  *   is passed the position in the list will represent the key.
+ * @param rotaryScrollableBehavior Parameter for changing rotary behavior.
  * @param content A composable function that defines the content of each page displayed by the
  *   Pager. This is where the UI elements that should appear within each page should be placed.
  */
@@ -303,6 +334,7 @@
     userScrollEnabled: Boolean = true,
     reverseLayout: Boolean = false,
     key: ((index: Int) -> Any)? = null,
+    rotaryScrollableBehavior: RotaryScrollableBehavior?,
     content: @Composable PagerScope.(page: Int) -> Unit
 ) {
     VerticalPager(
@@ -314,6 +346,7 @@
         userScrollEnabled = userScrollEnabled,
         reverseLayout = reverseLayout,
         key = key,
+        rotaryScrollableBehavior = rotaryScrollableBehavior
     ) { page ->
         AnimatedPageContent(
             orientation = Orientation.Vertical,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScreenScaffold.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScreenScaffold.kt
index 50260db..30552a1 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScreenScaffold.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/ScreenScaffold.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.runtime.Composable
@@ -34,7 +35,9 @@
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.ActiveFocusListener
 import androidx.wear.compose.foundation.ScrollInfoProvider
 import androidx.wear.compose.foundation.lazy.LazyColumnState
@@ -116,6 +119,10 @@
  * Example of using AppScaffold and ScreenScaffold:
  *
  * @sample androidx.wear.compose.material3.samples.ScaffoldSample
+ *
+ * Example of using ScreenScaffold with a [EdgeButton]:
+ *
+ * @sample androidx.wear.compose.material3.samples.EdgeButtonListSample
  * @param scrollState The scroll state for [androidx.wear.compose.foundation.lazy.LazyColumn], used
  *   to drive screen transitions such as [TimeText] scroll away and showing/hiding
  *   [ScrollIndicator].
@@ -376,6 +383,28 @@
     }
 }
 
+/** Contains the default values used by [ScreenScaffold] */
+object ScreenScaffoldDefaults {
+    /**
+     * Creates padding values with extra bottom padding for an EdgeButton.
+     *
+     * @param edgeButtonSize The size of the EdgeButton.
+     * @param start The padding on the start side of the content.
+     * @param top The padding on the top side of the content.
+     * @param end The padding on the end side of the content.
+     * @param extraBottom Additional padding to be added to the bottom padding calculated from the
+     *   edge button size.
+     * @return A [PaddingValues] object with the calculated padding.
+     */
+    fun contentPaddingWithEdgeButton(
+        edgeButtonSize: EdgeButtonSize,
+        start: Dp = 0.dp,
+        top: Dp = 0.dp,
+        end: Dp = 0.dp,
+        extraBottom: Dp = 0.dp,
+    ) = PaddingValues(start, top, end, extraBottom + edgeButtonSize.maximumHeightPlusPadding())
+}
+
 // Sets the height that will be used down the line, using a state as parameter, to avoid
 // recompositions when the height changes.
 internal fun Modifier.dynamicHeight(heightState: () -> Float) =
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Shapes.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Shapes.kt
index 178aba6..4a00aab 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Shapes.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Shapes.kt
@@ -54,9 +54,8 @@
  *   [CornerSize] (used by Cards).
  * @param extraLarge By default, provides [ShapeDefaults.ExtraLarge], a [RoundedCornerShape] with
  *   32dp [CornerSize].
- *
- * TODO(b/273226734) Review documentation with references to components that use the shape themes.
  */
+// TODO(b/273226734) Review documentation with references to components that use the shape themes.
 @Immutable
 class Shapes(
     val extraSmall: CornerBasedShape = ShapeDefaults.ExtraSmall,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Slider.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Slider.kt
index dd4ff33..ccbc467 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Slider.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Slider.kt
@@ -445,7 +445,9 @@
                         containerColor = fromToken(SliderTokens.ContainerColor),
                         buttonIconColor = fromToken(SliderTokens.ButtonIconColor),
                         selectedBarColor = fromToken(SliderTokens.SelectedBarColor),
-                        unselectedBarColor = fromToken(SliderTokens.UnselectedBarColor),
+                        unselectedBarColor =
+                            fromToken(SliderTokens.UnselectedBarColor)
+                                .copy(alpha = SliderTokens.UnselectedBarOpacity),
                         selectedBarSeparatorColor =
                             fromToken(SliderTokens.SelectedBarSeparatorColor),
                         unselectedBarSeparatorColor =
@@ -463,11 +465,13 @@
                                 ),
                         disabledSelectedBarColor = fromToken(SliderTokens.DisabledSelectedBarColor),
                         disabledUnselectedBarColor =
-                            fromToken(SliderTokens.DisabledUnselectedBarColor),
+                            fromToken(SliderTokens.DisabledUnselectedBarColor)
+                                .copy(alpha = SliderTokens.DisabledUnselectedBarOpacity),
                         disabledSelectedBarSeparatorColor =
                             fromToken(SliderTokens.DisabledSelectedBarSeparatorColor),
                         disabledUnselectedBarSeparatorColor =
-                            fromToken(SliderTokens.DisabledUnselectedBarSeparatorColor),
+                            fromToken(SliderTokens.DisabledUnselectedBarSeparatorColor)
+                                .copy(alpha = SliderTokens.DisabledUnselectedBarSeparatorOpacity),
                     )
                     .also { defaultSliderColorsCached = it }
         }
@@ -479,7 +483,9 @@
                         containerColor = fromToken(SliderTokens.ContainerColor),
                         buttonIconColor = fromToken(SliderTokens.ButtonIconColor),
                         selectedBarColor = fromToken(SliderTokens.VariantSelectedBarColor),
-                        unselectedBarColor = fromToken(SliderTokens.UnselectedBarColor),
+                        unselectedBarColor =
+                            fromToken(SliderTokens.UnselectedBarColor)
+                                .copy(alpha = SliderTokens.UnselectedBarOpacity),
                         selectedBarSeparatorColor =
                             fromToken(SliderTokens.SelectedBarSeparatorColor),
                         unselectedBarSeparatorColor =
@@ -497,11 +503,13 @@
                                 ),
                         disabledSelectedBarColor = fromToken(SliderTokens.DisabledSelectedBarColor),
                         disabledUnselectedBarColor =
-                            fromToken(SliderTokens.DisabledUnselectedBarColor),
+                            fromToken(SliderTokens.DisabledUnselectedBarColor)
+                                .copy(alpha = SliderTokens.DisabledUnselectedBarOpacity),
                         disabledSelectedBarSeparatorColor =
                             fromToken(SliderTokens.DisabledSelectedBarSeparatorColor),
                         disabledUnselectedBarSeparatorColor =
-                            fromToken(SliderTokens.DisabledUnselectedBarSeparatorColor),
+                            fromToken(SliderTokens.DisabledUnselectedBarSeparatorColor)
+                                .copy(alpha = SliderTokens.DisabledUnselectedBarSeparatorOpacity),
                     )
                     .also { defaultVariantSliderColorsCached = it }
         }
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
index bc8fa44..63556b6 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TextButton.kt
@@ -90,9 +90,8 @@
  *   preview the button in different states. Note that if `null` is provided, interactions will
  *   still happen internally.
  * @param content The content displayed on the text button, expected to be text or image.
- *
- * TODO(b/261838497) Add Material3 UX guidance links
  */
+// TODO(b/261838497) Add Material3 UX guidance links
 @Composable
 fun TextButton(
     onClick: () -> Unit,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
index bcab35d..04fe78f 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/TimePicker.kt
@@ -332,7 +332,7 @@
                         }
                         .focusRequester(focusRequesterConfirmButton)
                         .focusable(),
-                preferredHeight = ButtonDefaults.EdgeButtonHeightSmall,
+                buttonSize = EdgeButtonSize.Small,
                 colors =
                     buttonColors(
                         contentColor = colors.confirmButtonContentColor,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
index f210918..6e87044f 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Typography.kt
@@ -109,9 +109,8 @@
  * @property numeralExtraSmall NumeralExtraSmall is the smallest role for digits. Numerals use
  *   tabular spacing by default. They are for numbers that need to accommodate longer strings of
  *   digits, where no localization is required like in-workout metrics.
- *
- * TODO(b/273526150) Review documentation for typography, add examples for each size.
  */
+// TODO(b/273526150) Review documentation for typography, add examples for each size.
 @Immutable
 class Typography
 internal constructor(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/LazyColumnScrollTransformModifiers.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/LazyColumnScrollTransformModifiers.kt
index c386e2c..f44fd46 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/LazyColumnScrollTransformModifiers.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/lazy/LazyColumnScrollTransformModifiers.kt
@@ -92,14 +92,13 @@
             remember(backgroundColor, shape) {
                 ScalingMorphingBackgroundPainter(spec, shape, backgroundColor) { scrollProgress }
             }
-
         this@scrollTransform then
-            TargetMorphingHeightConsumerModifierElement { minMorphingHeight = it?.toFloat() } then
-            [email protected](painter) then
-            [email protected] { height, scrollProgress ->
-                with(spec) { scrollProgress.placementHeight(height.toFloat()).fastRoundToInt() }
-            } then
-            [email protected] { contentTransformation(spec) { scrollProgress } }
+            TargetMorphingHeightConsumerModifierElement { minMorphingHeight = it?.toFloat() }
+                .paint(painter)
+                .transformedHeight { height, scrollProgress ->
+                    with(spec) { scrollProgress.placementHeight(height.toFloat()).fastRoundToInt() }
+                }
+                .graphicsLayer { contentTransformation(spec) { scrollProgress } }
     }
 
 /**
@@ -121,11 +120,11 @@
         val spec = remember { LazyColumnScrollTransformBehavior { minMorphingHeight } }
 
         this@scrollTransform then
-            TargetMorphingHeightConsumerModifierElement { minMorphingHeight = it?.toFloat() } then
-            [email protected] { height, scrollProgress ->
-                with(spec) { scrollProgress.placementHeight(height.toFloat()).fastRoundToInt() }
-            } then
-            [email protected] { contentTransformation(spec) { scrollProgress } }
+            TargetMorphingHeightConsumerModifierElement { minMorphingHeight = it?.toFloat() }
+                .transformedHeight { height, scrollProgress ->
+                    with(spec) { scrollProgress.placementHeight(height.toFloat()).fastRoundToInt() }
+                }
+                .graphicsLayer { contentTransformation(spec) { scrollProgress } }
     }
 
 private fun GraphicsLayerScope.contentTransformation(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SliderTokens.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SliderTokens.kt
index 1157ecc..6f30680 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SliderTokens.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/tokens/SliderTokens.kt
@@ -29,10 +29,13 @@
     val DisabledSelectedBarColor = ColorSchemeKeyTokens.OutlineVariant
     val DisabledSelectedBarSeparatorColor = ColorSchemeKeyTokens.SurfaceContainer
     val DisabledUnselectedBarColor = ColorSchemeKeyTokens.Background
+    val DisabledUnselectedBarOpacity = 0.3f
     val DisabledUnselectedBarSeparatorColor = ColorSchemeKeyTokens.OutlineVariant
+    val DisabledUnselectedBarSeparatorOpacity = 0.5f
     val SelectedBarColor = ColorSchemeKeyTokens.Primary
     val SelectedBarSeparatorColor = ColorSchemeKeyTokens.PrimaryContainer
     val UnselectedBarColor = ColorSchemeKeyTokens.Background
+    val UnselectedBarOpacity = 0.3f
     val UnselectedBarSeparatorColor = ColorSchemeKeyTokens.Primary
     val UnselectedBarSeparatorOpacity = 0.5f
     val VariantSelectedBarColor = ColorSchemeKeyTokens.PrimaryDim
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index e1b71d7..d3d4f2a 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
     defaultConfig {
         applicationId "androidx.wear.compose.integration.demos"
         minSdk 25
-        versionCode 43
-        versionName "1.43"
+        versionCode 44
+        versionName "1.44"
     }
 
     buildTypes {
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index f083fc52..b912adf 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -96,6 +96,10 @@
   @kotlin.DslMarker public @interface MaterialScopeMarker {
   }
 
+  public final class PrimaryLayoutKt {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement primaryLayout(androidx.wear.protolayout.material3.MaterialScope, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> mainSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? titleSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? bottomSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? labelForBottomSlot, optional androidx.wear.protolayout.ModifiersBuilders.Clickable? onClick);
+  }
+
   public final class Shape {
     field public static final int CORNER_EXTRA_LARGE = 0; // 0x0
     field public static final int CORNER_EXTRA_SMALL = 1; // 0x1
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index f083fc52..b912adf 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -96,6 +96,10 @@
   @kotlin.DslMarker public @interface MaterialScopeMarker {
   }
 
+  public final class PrimaryLayoutKt {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement primaryLayout(androidx.wear.protolayout.material3.MaterialScope, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> mainSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? titleSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? bottomSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? labelForBottomSlot, optional androidx.wear.protolayout.ModifiersBuilders.Clickable? onClick);
+  }
+
   public final class Shape {
     field public static final int CORNER_EXTRA_LARGE = 0; // 0x0
     field public static final int CORNER_EXTRA_SMALL = 1; // 0x1
diff --git a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
index 1103e2c..37b25a2 100644
--- a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
+++ b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
@@ -26,10 +26,12 @@
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import androidx.wear.protolayout.material3.ColorTokens
 import androidx.wear.protolayout.material3.Typography
+import androidx.wear.protolayout.material3.buttonGroup
 import androidx.wear.protolayout.material3.getColorProp
 import androidx.wear.protolayout.material3.icon
 import androidx.wear.protolayout.material3.iconEdgeButton
 import androidx.wear.protolayout.material3.materialScope
+import androidx.wear.protolayout.material3.primaryLayout
 import androidx.wear.protolayout.material3.text
 import androidx.wear.protolayout.material3.textEdgeButton
 
@@ -84,4 +86,22 @@
         }
     }
 
+@Sampled
+fun topLeveLayout(
+    context: Context,
+    deviceConfiguration: DeviceParameters,
+    clickable: Clickable
+): LayoutElement =
+    materialScope(context, deviceConfiguration) {
+        primaryLayout(
+            titleSlot = { text("App title".prop()) },
+            mainSlot = {
+                buttonGroup {
+                    // To be populated
+                }
+            },
+            bottomSlot = { iconEdgeButton(clickable, "Description".prop()) { icon("id") } }
+        )
+    }
+
 fun String.prop() = StringProp.Builder(this).build()
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
index c34e8ca..d0caa30 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
@@ -291,3 +291,6 @@
     /** The color or icon tint color to be used for all content within a button. */
     public val content: ColorProp
 )
+
+internal fun LayoutElement.isSlotEdgeButton(): Boolean =
+    this is Box && METADATA_TAG == this.modifiers?.metadata?.toTagName()
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
index 9f088c4..32f5e8c 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
@@ -19,8 +19,11 @@
 import android.os.Build.VERSION.SDK_INT
 import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
 import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
 import androidx.annotation.Dimension.Companion.SP
 import androidx.wear.protolayout.DimensionBuilders.dp
+import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.LayoutElementBuilders.Spacer
 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
 import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
 import androidx.wear.protolayout.ModifiersBuilders.Semantics
@@ -31,6 +34,9 @@
 /** Returns byte array representation of tag from String. */
 internal fun String.toTagBytes(): ByteArray = toByteArray(StandardCharsets.UTF_8)
 
+/** Returns String representation of tag from Metadata. */
+internal fun ElementMetadata.toTagName(): String = String(tagData, StandardCharsets.UTF_8)
+
 internal fun <T> Iterable<T>.addBetween(newItem: T): Sequence<T> = sequence {
     var isFirst = true
     for (element in this@addBetween) {
@@ -59,3 +65,8 @@
 internal fun Int.toDp() = dp(this.toFloat())
 
 internal fun String.toElementMetadata() = ElementMetadata.Builder().setTagData(toTagBytes()).build()
+
+/** Builds a horizontal Spacer, with width set to expand and height set to the given value. */
+internal fun horizontalSpacer(@Dimension(unit = DP) heightDp: Int): Spacer {
+    return Spacer.Builder().setWidth(expand()).setHeight(dp(heightDp.toFloat())).build()
+}
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt
new file mode 100644
index 0000000..a9c41a0
--- /dev/null
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material3
+
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.annotation.VisibleForTesting
+import androidx.wear.protolayout.DimensionBuilders
+import androidx.wear.protolayout.DimensionBuilders.DpProp
+import androidx.wear.protolayout.DimensionBuilders.dp
+import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.DimensionBuilders.wrap
+import androidx.wear.protolayout.LayoutElementBuilders.Box
+import androidx.wear.protolayout.LayoutElementBuilders.Column
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
+import androidx.wear.protolayout.ModifiersBuilders
+import androidx.wear.protolayout.ModifiersBuilders.Clickable
+import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers
+import androidx.wear.protolayout.ModifiersBuilders.Padding
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_EDGE_BUTTON_TOP_MARGIN_DP
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_EMPTY_MARGIN_BOTTOM_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_MARGIN_SIDE_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_BOTTOM_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_TOP_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_BOTTOM_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_TOP_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.FOOTER_LABEL_SLOT_MARGIN_SIDE_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.FOOTER_LABEL_TO_BOTTOM_SLOT_SPACER_HEIGHT_DP
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_ICON_SIZE_DP
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_ICON_TITLE_SPACER_HEIGHT_DP
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_MARGIN_BOTTOM_DP
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_MARGIN_SIDE_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_MARGIN_TOP_DP
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.MAIN_SLOT_WITH_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.MAIN_SLOT_WITH_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.METADATA_TAG
+
+/**
+ * ProtoLayout Material3 full screen layout that represents a suggested Material3 layout style that
+ * is responsive and takes care of the elements placement, together with the recommended margin and
+ * padding applied.
+ *
+ * This layout is meant to occupy the whole screen, so nothing else should be added on top of it.
+ *
+ * On the top, there is an icon that will be automatically placed by the system, followed by the
+ * optional title slot. The icon slot needs to be reserved for the whole ProtoLayout Layout and no
+ * other content should be added at the top of the screen as it will be overlapped with the system
+ * placed icon.
+ *
+ * At the bottom, there is an optional fixed slot for either {@link EdgeButton} as a main action or
+ * small non tappable content.
+ *
+ * The middle of the layout is main content, that will fill the available space. For the best
+ * results across different screen sizes, it's recommended that this content's dimension are also
+ * [DimensionBuilders.expand] or [DimensionBuilders.weight]. Additional content in the main one can
+ * be added after a `225dp` breakpoint.
+ *
+ * @param mainSlot The main, central content for this layout. It's recommended for this content to
+ *   fill the available width and height for the best result across different screen size. This
+ *   layout places proper padding to prevent content from being cropped by the screen. Note that
+ *   depending on the corner shapes and different elements on the screen, there might be a need to
+ *   change padding on some of the elements in this slot. The content passed here can also have an
+ *   additional content value added to it, after `225dp` breakpoint. Some of the examples of content
+ *   that can be passed in here are:
+ *     * [buttonGroup] with buttons or cards
+ *     * two [buttonGroup
+ *     * Expanded card
+ *
+ * @param titleSlot The app title in the top slot, just below the icon. This should be one line of
+ *   [text] with [Typography.TITLE_SMALL] typography, describing the main purpose of this layout.
+ *   Title is an optional slot which can be omitted to make space for other elements. Defaults to
+ *   [ColorTokens.ON_BACKGROUND] color.
+ * @param bottomSlot The content for bottom slot in this layout, that will be anchored to the bottom
+ *   edge of the screen. This should be either a small non tappable content such as Text with
+ *   optional label for it or tappable main action with [textEdgeButton] or [iconEdgeButton] which
+ *   is designed to have its bottom following the screen's curvature. This bottom slot is optional,
+ *   if unset the main content will expand more towards the edge of the screen.
+ * @param labelForBottomSlot The label displayed just above the [bottomSlot]. Default will be one
+ *   line of [text] with [Typography.TITLE_SMALL] typography, [ColorTokens.ON_SURFACE] color that
+ *   should contain additional description of this layout. When the [bottomSlot] is not provided or
+ *   it an edge button, the given label will be ignored.
+ * @param onClick The clickable action for whole layout. If any area (outside of other added
+ *   tappable components) is clicked, it will fire the associated action.
+ * @sample androidx.wear.protolayout.material3.samples.topLeveLayout
+ */
+// TODO: b/356568440 - Add sample above and put it in a proper samples file and link with @sample
+// TODO: b/346958146 - Link visuals once they are available.
+// TODO: b/353247528 - Handle the icon.
+// TODO: b/369162409 -Allow side and bottom margins in PrimaryLayout to be customizable.
+// TODO: b/370976767 - Specify that this should be used with MaterialTileService.
+public fun MaterialScope.primaryLayout(
+    mainSlot: (MaterialScope.() -> LayoutElement),
+    titleSlot: (MaterialScope.() -> LayoutElement)? = null,
+    bottomSlot: (MaterialScope.() -> LayoutElement)? = null,
+    labelForBottomSlot: (MaterialScope.() -> LayoutElement)? = null,
+    onClick: Clickable? = null
+): LayoutElement =
+    primaryLayoutWithOverrideIcon(
+        overrideIcon = false,
+        titleSlot = titleSlot,
+        mainSlot = mainSlot,
+        bottomSlot = bottomSlot,
+        labelForBottomSlot = labelForBottomSlot,
+        onClick = onClick
+    )
+
+/**
+ * Overrides the icon slot by showing colors circle. For the rest, see [primaryLayout]. This should
+ * only be used for testing or building internal samples to validate the UI.
+ */
+// TODO: b/353247528 - Set as @VisibleForTesting only.
+internal fun MaterialScope.primaryLayoutWithOverrideIcon(
+    overrideIcon: Boolean,
+    titleSlot: (MaterialScope.() -> LayoutElement)? = null,
+    mainSlot: (MaterialScope.() -> LayoutElement)? = null,
+    bottomSlot: (MaterialScope.() -> LayoutElement)? = null,
+    labelForBottomSlot: (MaterialScope.() -> LayoutElement)? = null,
+    onClick: Clickable? = null,
+): LayoutElement {
+    val screenWidth = deviceConfiguration.screenWidthDp
+    val screenHeight = deviceConfiguration.screenHeightDp
+    val labelSlot: LayoutElement? =
+        labelForBottomSlot?.let {
+            withStyle(
+                    defaultTextElementStyle =
+                        TextElementStyle(
+                            typography = Typography.TITLE_SMALL,
+                            color = getColorProp(ColorTokens.ON_SURFACE)
+                        )
+                )
+                .labelForBottomSlot()
+        }
+
+    val modifiers =
+        Modifiers.Builder()
+            .setMetadata(ElementMetadata.Builder().setTagData(METADATA_TAG.toTagBytes()).build())
+
+    onClick?.apply { modifiers.setClickable(this) }
+
+    val mainSlotSideMargin: DpProp =
+        dp(
+            screenWidth *
+                if (bottomSlot != null)
+                    (if (titleSlot != null)
+                        MAIN_SLOT_WITH_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE
+                    else MAIN_SLOT_WITH_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE)
+                else
+                    (if (titleSlot != null)
+                        MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE
+                    else MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE)
+        )
+
+    val mainLayout =
+        Column.Builder()
+            .setModifiers(modifiers.build())
+            .setWidth(screenWidth.toDp())
+            .setHeight(screenHeight.toDp())
+            // Contains icon and optional title.
+            .addContent(
+                getHeaderContent(
+                    titleSlot?.let {
+                        withStyle(
+                                defaultTextElementStyle =
+                                    TextElementStyle(
+                                        typography = Typography.TITLE_SMALL,
+                                        color = getColorProp(ColorTokens.ON_BACKGROUND)
+                                    )
+                            )
+                            .titleSlot()
+                    },
+                    overrideIcon
+                )
+            )
+    // Contains main content. This Box is needed to set to expand, even if empty so it
+    // fills the empty space until bottom content.
+
+    mainSlot?.let { mainLayout.addContent(mainSlot().getMainContentBox(mainSlotSideMargin)) }
+
+    bottomSlot?.let {
+        // Contains bottom slot, optional label or needed padding if empty.
+        mainLayout.addContent(getFooterContent(bottomSlot(), labelSlot))
+    }
+
+    return mainLayout.build()
+}
+
+private fun MaterialScope.getIconPlaceholder(overrideIcon: Boolean): LayoutElement {
+    val iconSlot =
+        Box.Builder().setWidth(HEADER_ICON_SIZE_DP.toDp()).setHeight(HEADER_ICON_SIZE_DP.toDp())
+    if (overrideIcon) {
+        iconSlot.setModifiers(
+            Modifiers.Builder()
+                .setBackground(
+                    ModifiersBuilders.Background.Builder()
+                        .setCorner(getCorner(Shape.CORNER_FULL))
+                        .setColor(getColorProp(ColorTokens.ON_BACKGROUND))
+                        .build()
+                )
+                .build()
+        )
+    }
+    return iconSlot.build()
+}
+
+/** Returns header content with the mandatory icon and optional title. */
+private fun MaterialScope.getHeaderContent(
+    titleSlot: LayoutElement?,
+    overrideIcon: Boolean
+): Column {
+    val headerBuilder =
+        Column.Builder()
+            .setWidth(wrap())
+            .setHeight(wrap())
+            .setModifiers(Modifiers.Builder().setPadding(getMarginForHeader()).build())
+            .addContent(getIconPlaceholder(overrideIcon))
+
+    titleSlot?.apply {
+        headerBuilder
+            .addContent(horizontalSpacer(HEADER_ICON_TITLE_SPACER_HEIGHT_DP))
+            .addContent(titleSlot)
+    }
+
+    return headerBuilder.build()
+}
+
+/** Returns central slot with the optional main content. It expands to fill the available space. */
+private fun LayoutElement.getMainContentBox(sideMargin: DpProp): Box =
+    Box.Builder()
+        .setWidth(expand())
+        .setHeight(expand())
+        .setModifiers(
+            Modifiers.Builder()
+                .setPadding(
+                    Padding.Builder() // Top and bottom space has been added to other elements.
+                        .setStart(sideMargin)
+                        .setEnd(sideMargin)
+                        .build()
+                )
+                .build()
+        )
+        .addContent(this)
+        .build()
+
+/**
+ * Returns the footer content, containing bottom slot and optional label with the corresponding
+ * spacing and margins depending on what is that content, or Box with padding if there's no bottom
+ * slot.
+ */
+private fun MaterialScope.getFooterContent(
+    bottomSlot: LayoutElement?,
+    labelSlot: LayoutElement?
+): LayoutElement {
+    val footer = Box.Builder().setWidth(wrap()).setHeight(wrap())
+
+    if (bottomSlot == null) {
+        footer.setWidth(expand())
+        footer.setHeight(
+            dp(BOTTOM_SLOT_EMPTY_MARGIN_BOTTOM_PERCENTAGE * deviceConfiguration.screenHeightDp)
+        )
+    } else if (bottomSlot.isSlotEdgeButton()) {
+        // Label shouldn't be used with EdgeButton.
+        footer.setModifiers(
+            Modifiers.Builder()
+                .setPadding(
+                    Padding.Builder().setTop(BOTTOM_EDGE_BUTTON_TOP_MARGIN_DP.toDp()).build()
+                )
+                .build()
+        )
+
+        footer.addContent(bottomSlot)
+    } else {
+        val otherBottomSlot = Column.Builder().setWidth(wrap()).setHeight(wrap())
+
+        footer.setModifiers(
+            Modifiers.Builder()
+                .setPadding(
+                    Padding.Builder()
+                        .setTop(
+                            dp(
+                                (if (labelSlot == null)
+                                    BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_TOP_PERCENTAGE
+                                else BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_TOP_PERCENTAGE) *
+                                    deviceConfiguration.screenHeightDp
+                            )
+                        )
+                        .setBottom(
+                            dp(
+                                (if (labelSlot == null)
+                                    BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_BOTTOM_PERCENTAGE
+                                else BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_BOTTOM_PERCENTAGE) *
+                                    deviceConfiguration.screenHeightDp
+                            )
+                        )
+                        .build()
+                )
+                .build()
+        )
+
+        labelSlot?.apply {
+            otherBottomSlot
+                .addContent(
+                    generateLabelContent(
+                        dp(
+                            FOOTER_LABEL_SLOT_MARGIN_SIDE_PERCENTAGE *
+                                deviceConfiguration.screenWidthDp
+                        )
+                    )
+                )
+                .addContent(horizontalSpacer(FOOTER_LABEL_TO_BOTTOM_SLOT_SPACER_HEIGHT_DP))
+        }
+
+        footer.addContent(
+            otherBottomSlot
+                .addContent(
+                    bottomSlot.generateBottomSlotContent(
+                        dp(
+                            BOTTOM_SLOT_OTHER_MARGIN_SIDE_PERCENTAGE *
+                                deviceConfiguration.screenWidthDp
+                        )
+                    )
+                )
+                .build()
+        )
+    }
+
+    return footer.build()
+}
+
+private fun LayoutElement.generateBottomSlotContent(sidePadding: DpProp): LayoutElement =
+    Box.Builder()
+        .setModifiers(
+            Modifiers.Builder()
+                .setPadding(Padding.Builder().setStart(sidePadding).setEnd(sidePadding).build())
+                .build()
+        )
+        .addContent(this)
+        .build()
+
+private fun LayoutElement.generateLabelContent(sidePadding: DpProp): LayoutElement =
+    Box.Builder()
+        .setModifiers(
+            Modifiers.Builder()
+                .setPadding(Padding.Builder().setStart(sidePadding).setEnd(sidePadding).build())
+                .build()
+        )
+        .addContent(this)
+        .build()
+
+private fun MaterialScope.getMarginForHeader(): Padding {
+    return Padding.Builder()
+        .setTop(HEADER_MARGIN_TOP_DP.toDp())
+        .setBottom(HEADER_MARGIN_BOTTOM_DP.toDp())
+        .setStart(dp(HEADER_MARGIN_SIDE_PERCENTAGE * deviceConfiguration.screenWidthDp))
+        .setEnd(dp(HEADER_MARGIN_SIDE_PERCENTAGE * deviceConfiguration.screenWidthDp))
+        .build()
+}
+
+/** Contains the default values used by Material layout. */
+internal object PrimaryLayoutDefaults {
+    /** Tool tag for Metadata in Modifiers, so we know that Row is actually a PrimaryLayout. */
+    @VisibleForTesting const val METADATA_TAG: String = "PL"
+
+    @Dimension(unit = DP) const val HEADER_MARGIN_TOP_DP: Int = 3
+
+    @Dimension(unit = DP) const val HEADER_MARGIN_BOTTOM_DP: Int = 6
+
+    const val HEADER_MARGIN_SIDE_PERCENTAGE: Float = 14.5f / 100
+
+    @Dimension(unit = DP) const val HEADER_ICON_SIZE_DP: Int = 24
+
+    @Dimension(unit = DP) const val HEADER_ICON_TITLE_SPACER_HEIGHT_DP: Int = 2
+
+    // The remaining margins around EdgeButton are within the component itself.
+    @Dimension(unit = DP) const val BOTTOM_EDGE_BUTTON_TOP_MARGIN_DP: Int = 4
+
+    const val BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_TOP_PERCENTAGE: Float = 4f / 100
+    const val BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_BOTTOM_PERCENTAGE: Float = 8.3f / 100
+
+    const val BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_TOP_PERCENTAGE: Float = 3f / 100
+    const val BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_BOTTOM_PERCENTAGE: Float = 5f / 100
+    const val BOTTOM_SLOT_OTHER_MARGIN_SIDE_PERCENTAGE: Float = 26f / 100
+
+    @Dimension(unit = DP) const val FOOTER_LABEL_TO_BOTTOM_SLOT_SPACER_HEIGHT_DP: Int = 2
+
+    const val FOOTER_LABEL_SLOT_MARGIN_SIDE_PERCENTAGE: Float = 16.64f / 100
+
+    const val BOTTOM_SLOT_EMPTY_MARGIN_BOTTOM_PERCENTAGE: Float = 14f / 100
+    const val MAIN_SLOT_WITH_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE: Float = 3f / 100
+    const val MAIN_SLOT_WITH_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE: Float = 6f / 100
+    const val MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE: Float = 7.3f / 100
+    const val MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE: Float = 8.3f / 100
+}