Merge "Remove BenchmarkRule requirement to be used each test" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index e954274..5d0169f 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -40,6 +40,7 @@
 
     androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
     androidTestImplementation projectOrArtifact(":compose:material:material")
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(ANDROIDX_TEST_EXT_KTX)
     androidTestImplementation(JUNIT)
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
index 6beff5a..734c7f2 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
@@ -16,14 +16,20 @@
 
 package androidx.activity.compose
 
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.activity.addCallback
 import androidx.compose.material.Button
 import androidx.compose.material.Text
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
@@ -100,4 +106,41 @@
             assertThat(results).isEqualTo(listOf("initial", "changed"))
         }
     }
+
+    /**
+     * Test to ensure that the callback from the BackHandler remains in the correct order though
+     * lifecycle changes
+     */
+    @Test
+    fun testBackHandlerLifecycle() {
+        var inteceptedBack = false
+        val lifecycleOwner = TestLifecycleOwner()
+
+        composeTestRule.setContent {
+            val dispatcher = LocalOnBackPressedDispatcherOwner.current.onBackPressedDispatcher
+            val dispatcherOwner = object : OnBackPressedDispatcherOwner {
+                override fun getLifecycle() = lifecycleOwner.lifecycle
+
+                override fun getOnBackPressedDispatcher() = dispatcher
+            }
+            dispatcher.addCallback(lifecycleOwner) { }
+            CompositionLocalProvider(
+                LocalOnBackPressedDispatcherOwner provides dispatcherOwner,
+                LocalLifecycleOwner provides lifecycleOwner
+            ) {
+                BackHandler { inteceptedBack = true }
+            }
+            Button(onClick = { dispatcher.onBackPressed() }) {
+                Text(text = "Press Back")
+            }
+        }
+
+        lifecycleOwner.currentState = Lifecycle.State.CREATED
+        lifecycleOwner.currentState = Lifecycle.State.RESUMED
+
+        composeTestRule.onNodeWithText("Press Back").performClick()
+        composeTestRule.runOnIdle {
+            assertThat(inteceptedBack).isEqualTo(true)
+        }
+    }
 }
diff --git a/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt b/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt
index 86f2967..0e890e7 100644
--- a/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt
+++ b/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt
@@ -28,6 +28,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
 
 /**
  * Provides a [OnBackPressedDispatcher] that can be used by Composables hosted in a
@@ -86,10 +87,10 @@
         backCallback.isEnabled = enabled
     }
     val backDispatcher = LocalOnBackPressedDispatcherOwner.current.onBackPressedDispatcher
-    // If `backDispatcher` changes, dispose and reset the effect
-    DisposableEffect(backDispatcher) {
+    val lifecycleOwner = LocalLifecycleOwner.current
+    DisposableEffect(lifecycleOwner, backDispatcher) {
         // Add callback to the backDispatcher
-        backDispatcher.addCallback(backCallback)
+        backDispatcher.addCallback(lifecycleOwner, backCallback)
         // When the effect leaves the Composition, remove the callback
         onDispose {
             backCallback.remove()
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 279d455..33394d1 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -30,16 +30,16 @@
     api("androidx.core:core-ktx:1.1.0") {
         because "Mirror activity dependency graph for -ktx artifacts"
     }
-    api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")) {
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
-    api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"))
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
     api("androidx.savedstate:savedstate-ktx:1.1.0") {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
     api(KOTLIN_STDLIB)
 
-    androidTestImplementation(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-runtime-testing:2.3.1"))
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
     androidTestImplementation(JUNIT)
     androidTestImplementation(TRUTH)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/activity/activity-ktx/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt b/activity/activity-ktx/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
new file mode 100644
index 0000000..5f110c5
--- /dev/null
+++ b/activity/activity-ktx/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.activity.result
+
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.app.ActivityOptionsCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ActivityResultLauncherTest {
+
+    @Test
+    fun testUnitLaunch() {
+        val expectedResult = "result"
+        val registry = object : ActivityResultRegistry() {
+            override fun <I : Any?, O : Any?> onLaunch(
+                requestCode: Int,
+                contract: ActivityResultContract<I, O>,
+                input: I,
+                options: ActivityOptionsCompat?
+            ) {
+                contract.createIntent(InstrumentationRegistry.getInstrumentation().context, input)
+                dispatchResult(requestCode, expectedResult)
+            }
+        }
+
+        val contract = object : ActivityResultContract<Unit, String?>() {
+            override fun createIntent(context: Context, input: Unit) = Intent()
+            override fun parseResult(resultCode: Int, intent: Intent?) = ""
+        }
+
+        var actualResult: String? = null
+
+        val launcher = registry.register("key", contract) {
+            actualResult = it
+        }
+
+        launcher.launch()
+        assertThat(actualResult).isEqualTo(expectedResult)
+    }
+}
\ No newline at end of file
diff --git a/activity/activity-ktx/src/main/java/androidx/activity/result/ActivityResultLauncher.kt b/activity/activity-ktx/src/main/java/androidx/activity/result/ActivityResultLauncher.kt
index 0b3d759..69ec900 100644
--- a/activity/activity-ktx/src/main/java/androidx/activity/result/ActivityResultLauncher.kt
+++ b/activity/activity-ktx/src/main/java/androidx/activity/result/ActivityResultLauncher.kt
@@ -30,5 +30,5 @@
  */
 @JvmName("launchUnit")
 public fun ActivityResultLauncher<Unit>.launch(options: ActivityOptionsCompat? = null) {
-    launch(null, options)
+    launch(Unit, options)
 }
\ No newline at end of file
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 9be5f88..3f497dc 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -1,8 +1,8 @@
-import static androidx.build.dependencies.DependenciesKt.*
 import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
 import androidx.build.Publish
 
+import static androidx.build.dependencies.DependenciesKt.*
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -23,13 +23,13 @@
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.core:core:1.1.0")
-    api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-runtime:2.3.1"))
-    api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-viewmodel:2.3.1"))
+    api("androidx.lifecycle:lifecycle-runtime:2.3.1")
+    api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
     api("androidx.savedstate:savedstate:1.1.0")
-    api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1"))
+    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
     implementation("androidx.tracing:tracing:1.0.0")
 
-    androidTestImplementation(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-runtime-testing:2.3.1"))
+    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
     androidTestImplementation(KOTLIN_STDLIB)
     androidTestImplementation(LEAKCANARY)
     androidTestImplementation(LEAKCANARY_INSTRUMENTATION)
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchUtils.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchUtils.kt
new file mode 100644
index 0000000..8a3b2aa
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchUtils.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.isSupportedWithVmSettings
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+
+const val TARGET_PACKAGE = "androidx.benchmark.integration.macrobenchmark.target"
+
+fun MacrobenchmarkRule.measureStartup(
+    compilationMode: CompilationMode,
+    startupMode: StartupMode,
+    iterations: Int = 3,
+    setupIntent: Intent.() -> Unit = {}
+) = measureRepeated(
+    packageName = TARGET_PACKAGE,
+    metrics = listOf(StartupTimingMetric()),
+    compilationMode = compilationMode,
+    iterations = iterations,
+    startupMode = startupMode
+) {
+    pressHome()
+    val intent = Intent()
+    intent.setPackage(TARGET_PACKAGE)
+    setupIntent(intent)
+    startActivityAndWait(intent)
+}
+
+fun createStartupCompilationParams(
+    startupModes: List<StartupMode> = listOf(StartupMode.HOT, StartupMode.WARM, StartupMode.COLD),
+    compilationModes: List<CompilationMode> = listOf(
+        CompilationMode.None,
+        CompilationMode.Interpreted,
+        CompilationMode.SpeedProfile()
+    )
+): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+    for (startupMode in startupModes) {
+        for (compilationMode in compilationModes) {
+            // Skip configs that can't run, so they don't clutter Studio benchmark
+            // output with AssumptionViolatedException dumps
+            if (compilationMode.isSupportedWithVmSettings()) {
+                add(arrayOf(startupMode, compilationMode))
+            }
+        }
+    }
+}
+
+fun createCompilationParams(
+    compilationModes: List<CompilationMode> = listOf(
+        CompilationMode.None,
+        CompilationMode.Interpreted,
+        CompilationMode.SpeedProfile()
+    )
+): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+    for (compilationMode in compilationModes) {
+        // Skip configs that can't run, so they don't clutter Studio benchmark
+        // output with AssumptionViolatedException dumps
+        if (compilationMode.isSupportedWithVmSettings()) {
+            add(arrayOf(compilationMode))
+        }
+    }
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
deleted file mode 100644
index d4be8d52..0000000
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.integration.macrobenchmark
-
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(Parameterized::class)
-class ProcessSpeedProfileValidation(
-    private val compilationMode: CompilationMode,
-    private val startupMode: StartupMode
-) {
-    @get:Rule
-    val benchmarkRule = MacrobenchmarkRule()
-
-    @Test
-    fun start() = benchmarkRule.measureRepeated(
-        packageName = PACKAGE_NAME,
-        metrics = listOf(StartupTimingMetric()),
-        compilationMode = compilationMode,
-        iterations = 3,
-        startupMode = startupMode
-    ) {
-        pressHome()
-        startActivityAndWait()
-    }
-
-    companion object {
-        private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
-
-        @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
-        @JvmStatic
-        fun kilProcessParameters(): List<Array<Any>> {
-            val compilationModes = listOf(
-                CompilationMode.None,
-                CompilationMode.SpeedProfile(warmupIterations = 3)
-            )
-            val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
-            return compilationModes.zip(processKillOptions).map {
-                arrayOf(it.first, it.second)
-            }
-        }
-    }
-}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
index 742d7dc..553c577 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -16,6 +16,7 @@
 
 package androidx.benchmark.integration.macrobenchmark
 
+import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 import androidx.test.filters.LargeTest
@@ -26,13 +27,16 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-class SmallListStartupBenchmark(private val startupMode: StartupMode) {
+class SmallListStartupBenchmark(
+    private val startupMode: StartupMode,
+    private val compilationMode: CompilationMode
+) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
     fun startup() = benchmarkRule.measureStartup(
-        profileCompiled = true,
+        compilationMode = compilationMode,
         startupMode = startupMode
     ) {
         action = "androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
@@ -40,11 +44,8 @@
     }
 
     companion object {
-        @Parameterized.Parameters(name = "mode={0}")
+        @Parameterized.Parameters(name = "startup={0},compilation={1}")
         @JvmStatic
-        fun parameters(): List<Array<Any>> {
-            return listOf(StartupMode.COLD, StartupMode.WARM)
-                .map { arrayOf(it) }
-        }
+        fun parameters() = createStartupCompilationParams()
     }
 }
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
deleted file mode 100644
index f07501f..0000000
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.integration.macrobenchmark
-
-import android.content.Intent
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-
-const val TARGET_PACKAGE = "androidx.benchmark.integration.macrobenchmark.target"
-
-fun MacrobenchmarkRule.measureStartup(
-    profileCompiled: Boolean,
-    startupMode: StartupMode,
-    iterations: Int = 3,
-    setupIntent: Intent.() -> Unit = {}
-) = measureRepeated(
-    packageName = "androidx.benchmark.integration.macrobenchmark.target",
-    metrics = listOf(StartupTimingMetric()),
-    compilationMode = if (profileCompiled) {
-        CompilationMode.SpeedProfile(warmupIterations = 3)
-    } else {
-        CompilationMode.None
-    },
-    iterations = iterations,
-    startupMode = startupMode
-) {
-    pressHome()
-    val intent = Intent()
-    intent.setPackage(TARGET_PACKAGE)
-    setupIntent(intent)
-    startActivityAndWait(intent)
-}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
similarity index 89%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt
rename to benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
index b9ce5d4..62ede92 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
@@ -35,7 +35,7 @@
 @LargeTest
 @SdkSuppress(minSdkVersion = 29)
 @RunWith(Parameterized::class)
-class FrameTimingMetricValidation(
+class TrivialListScrollBenchmark(
     private val compilationMode: CompilationMode
 ) {
     @get:Rule
@@ -79,13 +79,8 @@
             "androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
         private const val RESOURCE_ID = "recycler"
 
-        @Parameterized.Parameters(name = "compilation_mode={0}")
+        @Parameterized.Parameters(name = "compilation={0}")
         @JvmStatic
-        fun jankParameters(): List<Array<Any>> {
-            return listOf(
-                CompilationMode.None,
-                CompilationMode.SpeedProfile(warmupIterations = 3)
-            ).map { arrayOf(it) }
-        }
+        fun parameters() = createCompilationParams()
     }
 }
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
index e62457a..29d9be3 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -16,6 +16,7 @@
 
 package androidx.benchmark.integration.macrobenchmark
 
+import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 import androidx.test.filters.LargeTest
@@ -26,24 +27,24 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-class TrivialStartupBenchmark(private val startupMode: StartupMode) {
+class TrivialStartupBenchmark(
+    private val startupMode: StartupMode,
+    private val compilationMode: CompilationMode
+) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
     fun startup() = benchmarkRule.measureStartup(
-        profileCompiled = true,
+        compilationMode = compilationMode,
         startupMode = startupMode
     ) {
         action = "androidx.benchmark.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY"
     }
 
     companion object {
-        @Parameterized.Parameters(name = "mode={0}")
+        @Parameterized.Parameters(name = "startup={0},compilation={1}")
         @JvmStatic
-        fun parameters(): List<Array<Any>> {
-            return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT)
-                .map { arrayOf(it) }
-        }
+        fun parameters() = createStartupCompilationParams()
     }
 }
diff --git a/benchmark/macro/build.gradle b/benchmark/macro/build.gradle
index 72c0ac1..1c61043 100644
--- a/benchmark/macro/build.gradle
+++ b/benchmark/macro/build.gradle
@@ -47,11 +47,11 @@
     implementation(project(":benchmark:benchmark-common"))
     implementation(ANDROIDX_TEST_CORE)
     implementation(ANDROIDX_TEST_UIAUTOMATOR)
-    implementation(ANDROIDX_TEST_RULES)
 
     androidTestImplementation(project(":internal-testutils-ktx"))
     androidTestImplementation(project(":tracing:tracing-ktx"))
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(KOTLIN_TEST)
 }
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
new file mode 100644
index 0000000..11ae6ab
--- /dev/null
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CompilationModeTest {
+    private val vmRunningInterpretedOnly: Boolean
+
+    init {
+        val device = InstrumentationRegistry.getInstrumentation().device()
+        val getProp = device.executeShellCommand("getprop dalvik.vm.extra-opts")
+        vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
+    }
+
+    @Test
+    fun names() {
+        // We test these names, as they're likely built into parameterized
+        // test strings, so stability/brevity are important
+        assertEquals("None", CompilationMode.None.toString())
+        assertEquals("SpeedProfile(iterations=123)", CompilationMode.SpeedProfile(123).toString())
+        assertEquals("Speed", CompilationMode.Speed.toString())
+        assertEquals("Interpreted", CompilationMode.Interpreted.toString())
+    }
+
+    @Test
+    fun isSupportedWithVmSettings_jitEnabled() {
+        assumeFalse(vmRunningInterpretedOnly)
+
+        assertTrue(CompilationMode.None.isSupportedWithVmSettings())
+        assertTrue(CompilationMode.SpeedProfile().isSupportedWithVmSettings())
+        assertTrue(CompilationMode.Speed.isSupportedWithVmSettings())
+        assertFalse(CompilationMode.Interpreted.isSupportedWithVmSettings())
+    }
+
+    @Test
+    fun isSupportedWithVmSettings_jitDisabled() {
+        assumeTrue(vmRunningInterpretedOnly)
+
+        assertFalse(CompilationMode.None.isSupportedWithVmSettings())
+        assertFalse(CompilationMode.SpeedProfile().isSupportedWithVmSettings())
+        assertFalse(CompilationMode.Speed.isSupportedWithVmSettings())
+        assertTrue(CompilationMode.Interpreted.isSupportedWithVmSettings())
+    }
+}
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 30529d6..b52e20e 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -14,23 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.benchmark.macro/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+package androidx.benchmark.macro
 
 import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.AssumptionViolatedException
 
 sealed class CompilationMode(
     // for modes other than [None], is argument passed `cmd package compile`
@@ -44,16 +31,19 @@
     }
 
     object None : CompilationMode(null) {
-        override fun toString() = "androidx.benchmark.macro.CompilationMode.None"
+        override fun toString() = "None"
     }
 
     class SpeedProfile(val warmupIterations: Int = 3) : CompilationMode("speed-profile") {
-        override fun toString() =
-            "androidx.benchmark.macro.CompilationMode.SpeedProfile(iterations=$warmupIterations)"
+        override fun toString() = "SpeedProfile(iterations=$warmupIterations)"
     }
 
     object Speed : CompilationMode("speed") {
-        override fun toString() = "androidx.benchmark.macro.CompilationMode.Speed"
+        override fun toString() = "Speed"
+    }
+
+    object Interpreted : CompilationMode(null) {
+        override fun toString() = "Interpreted"
     }
 }
 
@@ -61,7 +51,7 @@
     val instrumentation = InstrumentationRegistry.getInstrumentation()
     // Clear profile between runs.
     clearProfile(instrumentation, packageName)
-    if (this == CompilationMode.None) {
+    if (this == CompilationMode.None || this == CompilationMode.Interpreted) {
         return // nothing to do
     }
     if (this is CompilationMode.SpeedProfile) {
@@ -75,4 +65,38 @@
         packageName,
         compileArgument()
     )
+}
+
+fun CompilationMode.isSupportedWithVmSettings(): Boolean {
+    val device = InstrumentationRegistry.getInstrumentation().device()
+    val getProp = device.executeShellCommand("getprop dalvik.vm.extra-opts")
+    val vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
+
+    // true if requires interpreted, false otherwise
+    val interpreted = this == CompilationMode.Interpreted
+    return vmRunningInterpretedOnly == interpreted
+}
+
+internal fun CompilationMode.assumeSupportedWithVmSettings() {
+    if (!isSupportedWithVmSettings()) {
+        throw AssumptionViolatedException(
+            when {
+                DeviceInfo.isRooted && this == CompilationMode.Interpreted ->
+                    """
+                        To run benchmarks with CompilationMode $this,
+                        you must disable jit on your device with the following command:
+                        `adb shell setprop dalvik.vm.extra-opts -Xusejit:false; adb shell stop; adb shell start`                         
+                    """.trimIndent()
+                DeviceInfo.isRooted && this != CompilationMode.Interpreted ->
+                    """
+                        To run benchmarks with CompilationMode $this,
+                        you must enable jit on your device with the following command:
+                        `adb shell setprop dalvik.vm.extra-opts \"\"; adb shell stop; adb shell start` 
+                    """.trimIndent()
+                else ->
+                    "You must toggle usejit on the VM to use CompilationMode $this, this requires" +
+                        "rooting your device."
+            }
+        )
+    }
 }
\ No newline at end of file
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/DeviceInfo.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/DeviceInfo.kt
index 062ad0f..a93a8b7 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/DeviceInfo.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/DeviceInfo.kt
@@ -21,6 +21,7 @@
 import android.os.BatteryManager
 import android.os.Build
 import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
 
 internal object DeviceInfo {
     val isEmulator = Build.FINGERPRINT.startsWith("generic") ||
@@ -34,6 +35,20 @@
 
     val isEngBuild = Build.FINGERPRINT.contains(":eng/")
 
+    val isRooted =
+        arrayOf(
+            "/system/app/Superuser.apk",
+            "/sbin/su",
+            "/system/bin/su",
+            "/system/xbin/su",
+            "/data/local/xbin/su",
+            "/data/local/bin/su",
+            "/system/sd/xbin/su",
+            "/system/bin/failsafe/su",
+            "/data/local/su",
+            "/su/bin/su"
+        ).any { File(it).exists() }
+
     /**
      * Battery percentage required to avoid low battery warning.
      *
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 021bcee..f2b1952 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -111,6 +111,9 @@
         "Macrobenchmark currently requires Android 10 (API 29) or greater."
     }
 
+    // skip benchmark if not supported by vm settings
+    compilationMode.assumeSupportedWithVmSettings()
+
     val suppressionState = checkErrors(packageName)
     var warningMessage = suppressionState?.warningMessage ?: ""
 
diff --git a/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java b/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
index a7fc625..a1439a7 100644
--- a/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
+++ b/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
@@ -36,6 +36,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -78,6 +79,7 @@
         mManager.unbindAllConnections();
     }
 
+    @Ignore("Test disabled due to flakiness, see b/182415874")
     @Test
     public void testConnection() {
         final AtomicBoolean connected = new AtomicBoolean();
diff --git a/buildSrc/OWNERS b/buildSrc/OWNERS
index f852f60..7674fe0 100644
--- a/buildSrc/OWNERS
+++ b/buildSrc/OWNERS
@@ -9,5 +9,6 @@
 per-file *Dependencies.kt=file://OWNERS
 per-file *LibraryVersions.kt=file://OWNERS
 per-file *PublishDocsRules.kt=file://OWNERS
+per-file *build_dependencies.gradle=file://OWNERS
 
 per-file *AndroidXPlaygroundRootPlugin.kt = [email protected], [email protected], [email protected]
diff --git a/buildSrc/build_dependencies.gradle b/buildSrc/build_dependencies.gradle
index 5065107..935497b 100644
--- a/buildSrc/build_dependencies.gradle
+++ b/buildSrc/build_dependencies.gradle
@@ -23,7 +23,7 @@
 build_versions.lint = build_versions.studio["lint"]
 
 build_versions.kotlin = "1.4.31"
-build_versions.kotlin_coroutines = "1.4.1"
+build_versions.kotlin_coroutines = "1.4.3"
 build_versions.ksp = "1.4.30-1.0.0-alpha05"
 
 build_versions.hilt = "2.33-beta"
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
index 365c7c9..ebf8a0b 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
@@ -16,7 +16,6 @@
 
 package androidx.build
 
-import androidx.build.AndroidXRootPlugin.Companion.PREBUILT_OR_SNAPSHOT_EXT_NAME
 import androidx.build.AndroidXRootPlugin.Companion.PROJECT_OR_ARTIFACT_EXT_NAME
 import androidx.build.gradle.isRoot
 import groovy.xml.DOMBuilder
@@ -55,12 +54,6 @@
         }
     )
 
-    private val prebuiltOrSnapshotClosure = KotlinClosure1<String, String>(
-        function = {
-            prebuiltOrSnapshot(this)
-        }
-    )
-
     override fun apply(target: Project) {
         if (!target.isRoot) {
             throw GradleException("This plugin should only be applied to root project")
@@ -82,7 +75,6 @@
     private fun configureSubProject(project: Project) {
         project.repositories.addPlaygroundRepositories()
         project.extra.set(PROJECT_OR_ARTIFACT_EXT_NAME, projectOrArtifactClosure)
-        project.extra.set(PREBUILT_OR_SNAPSHOT_EXT_NAME, prebuiltOrSnapshotClosure)
         project.configurations.all { configuration ->
             configuration.resolutionStrategy.dependencySubstitution.all { substitution ->
                 substitution.replaceIfSnapshot()
@@ -127,21 +119,6 @@
         }
     }
 
-    private fun prebuiltOrSnapshot(path: String): String {
-        val sections = path.split(":")
-
-        if (sections.size != 3) {
-            throw GradleException(
-                "Expected prebuiltOrSnapshot path to be of the form " +
-                    "<group>:<artifact>:<version>, but was $path"
-            )
-        }
-
-        val group = sections[0]
-        val artifact = sections[1]
-        return "$group:$artifact:$SNAPSHOT_MARKER"
-    }
-
     private fun DependencySubstitution.replaceIfSnapshot() {
         val requested = this.requested
         if (requested is ModuleComponentSelector && requested.version == SNAPSHOT_MARKER) {
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
index ce0b8d1..471adb5 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
@@ -101,15 +101,6 @@
                     }
                 )
             )
-            project.extra.set(
-                PREBUILT_OR_SNAPSHOT_EXT_NAME,
-                KotlinClosure1<String, String>(
-                    function = {
-                        // this refers to the first parameter of the closure.
-                        this
-                    }
-                )
-            )
             project.plugins.withType(AndroidBasePlugin::class.java) {
                 buildOnServerTask.dependsOn("${project.path}:assembleDebug")
                 buildOnServerTask.dependsOn("${project.path}:assembleAndroidTest")
@@ -245,6 +236,5 @@
 
     companion object {
         const val PROJECT_OR_ARTIFACT_EXT_NAME = "projectOrArtifact"
-        const val PREBUILT_OR_SNAPSHOT_EXT_NAME = "prebuiltOrSnapshot"
     }
 }
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index a099596..eee159c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -74,14 +74,14 @@
     val INSPECTION = Version("1.0.0")
     val INTERPOLATOR = Version("1.1.0-alpha01")
     val JETIFIER = Version("1.0.0-beta10")
-    val LEANBACK = Version("1.1.0-beta01")
+    val LEANBACK = Version("1.2.0-alpha01")
     val LEANBACK_PAGING = Version("1.1.0-beta01")
-    val LEANBACK_PREFERENCE = Version("1.1.0-beta01")
+    val LEANBACK_PREFERENCE = Version("1.2.0-alpha01")
     val LEANBACK_TAB = Version("1.1.0-beta01")
     val LEGACY = Version("1.1.0-alpha01")
     val LOCALBROADCASTMANAGER = Version("1.1.0-alpha02")
     val LIFECYCLE = Version("2.4.0-alpha01")
-    val LIFECYCLE_COMPOSE = Version("1.0.0-alpha04")
+    val LIFECYCLE_VIEWMODEL_COMPOSE = Version("1.0.0-alpha04")
     val LIFECYCLE_EXTENSIONS = Version("2.2.0")
     val LOADER = Version("1.2.0-alpha01")
     val MEDIA = Version("1.3.0-rc01")
@@ -89,7 +89,7 @@
     val MEDIAROUTER = Version("1.3.0-alpha01")
     val NAVIGATION = Version("2.4.0-alpha01")
     val NAVIGATION_COMPOSE = Version("1.0.0-alpha10")
-    val PAGING = Version("3.0.0-beta03")
+    val PAGING = Version("3.1.0-alpha01")
     val PAGING_COMPOSE = Version("1.0.0-alpha08")
     val PALETTE = Version("1.1.0-alpha01")
     val PRINT = Version("1.1.0-beta01")
@@ -112,7 +112,7 @@
     val SLICE_BUILDERS_KTX = Version("1.0.0-alpha08")
     val SLICE_REMOTECALLBACK = Version("1.0.0-alpha01")
     val SLIDINGPANELAYOUT = Version("1.2.0-alpha01")
-    val STARTUP = Version("1.0.0")
+    val STARTUP = Version("1.1.0-alpha01")
     val SQLITE = Version("2.2.0-alpha01")
     val SQLITE_INSPECTOR = Version("2.1.0-alpha01")
     val SWIPEREFRESHLAYOUT = Version("1.2.0-alpha01")
@@ -128,20 +128,25 @@
     val VERSIONED_PARCELABLE = Version("1.2.0-alpha01")
     val VIEWPAGER = Version("1.1.0-alpha01")
     val VIEWPAGER2 = Version("1.1.0-alpha02")
-    val WEAR = Version("1.2.0-alpha07")
-    val WEAR_COMPLICATIONS = Version("1.0.0-alpha10")
+    val WEAR = Version("1.2.0-alpha08")
+    val WEAR_COMPLICATIONS_DATA = Version("1.0.0-alpha11")
+    val WEAR_COMPLICATIONS_PROVIDER = Version("1.0.0-alpha11")
     val WEAR_INPUT = Version("1.1.0-alpha02")
+    val WEAR_INPUT_TESTING = WEAR_INPUT
     val WEAR_ONGOING = Version("1.0.0-alpha04")
     val WEAR_PHONE_INTERACTIONS = Version("1.0.0-alpha04")
     val WEAR_REMOTE_INTERACTIONS = Version("1.0.0-alpha03")
     val WEAR_TILES = Version("1.0.0-alpha02")
-    val WEAR_WATCHFACE = Version("1.0.0-alpha10")
-    val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha10")
-    val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha10")
-    val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha10")
-    val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha10")
+    val WEAR_WATCHFACE = Version("1.0.0-alpha11")
+    val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha11")
+    val WEAR_WATCHFACE_CLIENT_GUAVA = WEAR_WATCHFACE_CLIENT
+    val WEAR_WATCHFACE_COMPLICATIONS_RENDERING = Version("1.0.0-alpha11")
+    val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha11")
+    val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha11")
+    val WEAR_WATCHFACE_EDITOR_GUAVA = WEAR_WATCHFACE_EDITOR
+    val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha11")
     val WEBKIT = Version("1.5.0-alpha01")
-    val WINDOW = Version("1.0.0-alpha05")
+    val WINDOW = Version("1.0.0-alpha06")
     val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
     val WINDOW_SIDECAR = Version("0.1.0-alpha01")
     val WORK = Version("2.6.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index dda6d40..0d5b532 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -27,7 +27,7 @@
 const val ANDROIDX_TEST_RULES = "androidx.test:rules:1.3.0"
 const val ANDROIDX_TEST_RUNNER = "androidx.test:runner:1.3.0"
 const val ANDROIDX_TEST_UIAUTOMATOR = "androidx.test.uiautomator:uiautomator:2.2.0"
-const val AUTO_COMMON = "com.google.auto:auto-common:0.10"
+const val AUTO_COMMON = "com.google.auto:auto-common:0.11"
 const val AUTO_SERVICE_ANNOTATIONS = "com.google.auto.service:auto-service-annotations:1.0-rc6"
 const val AUTO_SERVICE_PROCESSOR = "com.google.auto.service:auto-service:1.0-rc6"
 const val AUTO_VALUE = "com.google.auto.value:auto-value:1.6.3"
@@ -108,7 +108,7 @@
 const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
 const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
 const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.18"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.21"
 val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
 val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
 val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
@@ -127,6 +127,7 @@
     }
 }
 const val TRUTH = "com.google.truth:truth:1.0.1"
+const val VIEW_BINDING = "androidx.databinding:viewbinding:4.1.2"
 const val XERIAL = "org.xerial:sqlite-jdbc:3.25.2"
 const val XPP3 = "xpp3:xpp3:1.1.4c"
 const val XMLPULL = "xmlpull:xmlpull:1.1.3.1"
diff --git a/busytown/androidx-studio-integration.sh b/busytown/androidx-studio-integration.sh
index 22d6b80..1bb624c 100755
--- a/busytown/androidx-studio-integration.sh
+++ b/busytown/androidx-studio-integration.sh
@@ -59,11 +59,11 @@
   zip -r "$DIST_DIR/transforms.zip" "$OUT_DIR/.gradle/caches/transforms-2/files-2.1"
 }
 
-if buildAndroidx; then
-  echo build succeeded
-else
-  # b/162260809 export transforms directory to help identify cause of corrupt/missing files
-  exportTransformsDir
-  exit 1
-fi
+#if buildAndroidx; then
+#  echo build succeeded
+#else
+#  # b/162260809 export transforms directory to help identify cause of corrupt/missing files
+#  exportTransformsDir
+#  exit 1
+#fi
 echo "Completing $0 at $(date)"
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index 02ad9bb..e10908c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -52,7 +52,7 @@
  * well as providing access to other utility methods. The primary purpose of this class it to
  * forward these interactions to the currently configured [UseCaseCamera].
  */
-@SuppressLint("UnsafeExperimentalUsageError")
+@SuppressLint("UnsafeOptInUsageError")
 @CameraScope
 @OptIn(ExperimentalCoroutinesApi::class)
 class CameraControlAdapter @Inject constructor(
@@ -157,7 +157,7 @@
         warn { "TODO: cancelAfAeTrigger is not yet supported" }
     }
 
-    @SuppressLint("UnsafeExperimentalUsageError")
+    @SuppressLint("UnsafeOptInUsageError")
     override fun setExposureCompensationIndex(exposure: Int): ListenableFuture<Int> {
         return threads.scope.async(start = CoroutineStart.UNDISPATCHED) {
             useCaseManager.camera?.let {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 311be3f..01a351e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -43,7 +43,7 @@
  * Adapt the [CameraInfoInternal] interface to [CameraPipe].
  */
 @SuppressLint(
-    "UnsafeExperimentalUsageError" // Suppressed due to experimental ExposureState
+    "UnsafeOptInUsageError" // Suppressed due to experimental ExposureState
 )
 @CameraScope
 class CameraInfoAdapter @Inject constructor(
@@ -82,7 +82,7 @@
     override fun getZoomState(): LiveData<ZoomState> = cameraState.zoomStateLiveData
     override fun getTorchState(): LiveData<Int> = cameraState.torchStateLiveData
 
-    @SuppressLint("UnsafeExperimentalUsageError")
+    @SuppressLint("UnsafeOptInUsageError")
     override fun getExposureState(): ExposureState = cameraState.exposureStateLiveData.value!!
 
     override fun addSessionCaptureCallback(executor: Executor, callback: CameraCaptureCallback) =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
index 22ed619..3ddc4ff0 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
@@ -31,7 +31,7 @@
 /**
  * [CameraStateAdapter] caches and updates based on callbacks from the active CameraGraph.
  */
-@SuppressLint("UnsafeExperimentalUsageError")
+@SuppressLint("UnsafeOptInUsageError")
 @CameraScope
 class CameraStateAdapter @Inject constructor(
     private val zoomControl: ZoomControl,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EvCompValue.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EvCompValue.kt
index 2ea0afa..2837159 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EvCompValue.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EvCompValue.kt
@@ -24,7 +24,7 @@
 /**
  * Immutable adaptor to the ExposureState interface.
  */
-@SuppressLint("UnsafeExperimentalUsageError")
+@SuppressLint("UnsafeOptInUsageError")
 data class EvCompValue(
     private val supported: Boolean,
     private val index: Int,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
index 98b6e81..487b34b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
@@ -27,7 +27,7 @@
 internal val EMPTY_RANGE = Range(0, 0)
 
 /** Adapt [ExposureState] to a [CameraMetadata] instance. */
-@SuppressLint("UnsafeExperimentalUsageError")
+@SuppressLint("UnsafeOptInUsageError")
 class ExposureStateAdapter(
     private val cameraProperties: CameraProperties,
     private val exposureCompensation: Int
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
index 7f66dc9..168ca3146 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
@@ -17,6 +17,8 @@
 package androidx.camera.camera2.pipe
 
 import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
 
 // Public controls and enums used to interact with a CameraGraph.
 
@@ -130,11 +132,14 @@
 /**
  * Return type for a 3A method.
  *
- * @param frameNumber the latest [FrameNumber] at which the method succeeded or was aborted.
  * @param status [Status] of the 3A operation at the time of return.
+ * @param frameMetadata [FrameMetadata] of the latest frame at which the method succeeded or was
+ * aborted. The metadata reflects CaptureResult or TotalCaptureResult for that frame. It can so
+ * happen that the [CaptureResult] itself has all the key-value pairs needed to determine the
+ * completion of the method, in that case this frameMetadata may not contain all the kay value pairs
+ * associated with the final result i.e [TotalCaptureResult] of this frame.
  */
-public data class Result3A(val frameNumber: FrameNumber, val status: Status) {
-
+public data class Result3A(val status: Status, val frameMetadata: FrameMetadata? = null) {
     /**
      * Enum to know the status of 3A operation in case the method returns before the desired
      * operation is complete. The reason could be that the operation was talking a lot longer and an
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 2154f40..2c09227 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -24,15 +24,12 @@
 import android.hardware.camera2.params.MeteringRectangle
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_FRAME_LIMIT
-import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_MS
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
 import kotlinx.coroutines.Deferred
 import java.io.Closeable
 
 /**
  * A [CameraGraph] represents the combined configuration and state of a camera.
- *
- *
  */
 public interface CameraGraph : Closeable {
     public val streams: StreamGraph
@@ -237,7 +234,15 @@
 
         /**
          * Locks the auto-exposure, auto-focus and auto-whitebalance as per the given desired
-         * behaviors.
+         * behaviors. This given 3A parameters are applied before the lock is obtained. If 'null'
+         * value is passed for a parameter, that parameter is ignored, and the current value for
+         * that parameter continues to be applied.
+         *
+         * TODO(sushilnath@): Add support for specifying the AE, AF and AWB modes as well. The
+         * update of modes require special care if the desired lock behavior is immediate. In
+         * that case we have to submit a combination of repeating and single requests so that the
+         * AF skips the initial state of the new mode's state machine and stays locks in the new
+         * mode as well.
          *
          * @param frameLimit the maximum number of frames to wait before we give up waiting for
          * this operation to complete.
@@ -249,28 +254,6 @@
          * or time limit was reached.
          */
         public suspend fun lock3A(
-            aeLockBehavior: Lock3ABehavior? = null,
-            afLockBehavior: Lock3ABehavior? = null,
-            awbLockBehavior: Lock3ABehavior? = null,
-            frameLimit: Int = DEFAULT_FRAME_LIMIT,
-            timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
-        ): Deferred<Result3A>
-
-        /**
-         * Locks the auto-exposure, auto-focus and auto-whitebalance as per the given desired
-         * behaviors. This method is similar to the earlier [lock3A] method with additional
-         * capability of applying the given 3A parameters before the lock is obtained.
-         *
-         * @param frameLimit the maximum number of frames to wait before we give up waiting for
-         * this operation to complete.
-         * @param timeLimitMs the maximum time limit in ms we wait before we give up waiting for
-         * this operation to complete.
-         *
-         * @return [Result3A], which will contain the latest frame number at which the locks were
-         * applied or the frame number at which the method returned early because either frame limit
-         * or time limit was reached.
-         */
-        public fun lock3A(
             aeMode: AeMode? = null,
             afMode: AfMode? = null,
             awbMode: AwbMode? = null,
@@ -281,7 +264,7 @@
             afLockBehavior: Lock3ABehavior? = null,
             awbLockBehavior: Lock3ABehavior? = null,
             frameLimit: Int = DEFAULT_FRAME_LIMIT,
-            timeLimitMs: Int = DEFAULT_TIME_LIMIT_MS
+            timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
         ): Deferred<Result3A>
 
         /**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
index d1d4e13..469a17d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
@@ -110,6 +110,12 @@
     }
 
     override suspend fun lock3A(
+        aeMode: AeMode?,
+        afMode: AfMode?,
+        awbMode: AwbMode?,
+        aeRegions: List<MeteringRectangle>?,
+        afRegions: List<MeteringRectangle>?,
+        awbRegions: List<MeteringRectangle>?,
         aeLockBehavior: Lock3ABehavior?,
         afLockBehavior: Lock3ABehavior?,
         awbLockBehavior: Lock3ABehavior?,
@@ -121,28 +127,17 @@
         // ae, af and awb respectively. If not supported return an exception or return early with
         // the right status code.
         return controller3A.lock3A(
-            aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit,
+            aeRegions,
+            afRegions,
+            awbRegions,
+            aeLockBehavior,
+            afLockBehavior,
+            awbLockBehavior,
+            frameLimit,
             timeLimitNs
         )
     }
 
-    override fun lock3A(
-        aeMode: AeMode?,
-        afMode: AfMode?,
-        awbMode: AwbMode?,
-        aeRegions: List<MeteringRectangle>?,
-        afRegions: List<MeteringRectangle>?,
-        awbRegions: List<MeteringRectangle>?,
-        aeLockBehavior: Lock3ABehavior?,
-        afLockBehavior: Lock3ABehavior?,
-        awbLockBehavior: Lock3ABehavior?,
-        frameLimit: Int,
-        timeLimitMs: Int
-    ): Deferred<Result3A> {
-        check(!closed.value) { "Cannot call lock3A on $this after close." }
-        TODO("Implement lock3A")
-    }
-
     override fun unlock3A(ae: Boolean?, af: Boolean?, awb: Boolean?): Deferred<FrameNumber> {
         check(!closed.value) { "Cannot call unlock3A on $this after close." }
         throw UnsupportedOperationException()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 190b10f..47a94c0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -33,7 +33,6 @@
 import androidx.camera.camera2.pipe.AwbMode
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_FRAME_LIMIT
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
-import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
 import androidx.camera.camera2.pipe.FlashMode
 import androidx.camera.camera2.pipe.Lock3ABehavior
 import androidx.camera.camera2.pipe.Result3A
@@ -99,7 +98,7 @@
             CONTROL_AE_PRECAPTURE_TRIGGER to CONTROL_AE_PRECAPTURE_TRIGGER_START
         )
 
-        private val result3ASubmitFailed = Result3A(FRAME_NUMBER_INVALID, Status.SUBMIT_FAILED)
+        private val result3ASubmitFailed = Result3A(Status.SUBMIT_FAILED)
 
         private val aeUnlockedStateList = listOf(
             CaptureResult.CONTROL_AE_STATE_INACTIVE,
@@ -202,8 +201,9 @@
     }
 
     /**
-     * Given the desired lock behaviors for ae, af and awb, this method, (a) first unlocks them and
-     * wait for them to converge, and then (b) locks them.
+     * Given the desired metering regions and lock behaviors for ae, af and awb, this method
+     * updates the metering regions and then, (a) unlocks ae, af, awb and wait for them to converge,
+     * and then (b) locks them.
      *
      * (a) In this step, as needed, we first send a single request with 'af trigger = cancel' to
      * unlock af, and then a repeating request to unlock ae and awb. We suspend till we receive a
@@ -222,12 +222,20 @@
      * timeLimit) to complete
      */
     suspend fun lock3A(
+        aeRegions: List<MeteringRectangle>? = null,
+        afRegions: List<MeteringRectangle>? = null,
+        awbRegions: List<MeteringRectangle>? = null,
         aeLockBehavior: Lock3ABehavior? = null,
         afLockBehavior: Lock3ABehavior? = null,
         awbLockBehavior: Lock3ABehavior? = null,
         frameLimit: Int = DEFAULT_FRAME_LIMIT,
         timeLimitNs: Long? = DEFAULT_TIME_LIMIT_NS
     ): Deferred<Result3A> {
+        // Update the 3A state of camera graph with the given metering regions. If metering regions
+        // are given as null then they are ignored and the current metering regions continue to be
+        // applied in subsequent requests to the camera device.
+        graphState3A.update(aeRegions = aeRegions, afRegions = afRegions, awbRegions = awbRegions)
+
         // If we explicitly need to unlock af first before proceeding to lock it, we need to send
         // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
         if (afLockBehavior.shouldUnlockAf()) {
@@ -279,8 +287,8 @@
             }
             val result = listener.result.await()
             debug {
-                "lock3A - converged at frame number=${result.frameNumber.value}, status=${result
-                    .status}"
+                "lock3A - converged at frame number=${result.frameMetadata?.frameNumber?.value}, " +
+                    "status=${result.status}"
             }
             // Return immediately if we encounter an error when unlocking and waiting for
             // convergence.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
index cc813c6..9641f64 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
@@ -96,9 +96,7 @@
             currentTimestampNs != null &&
             currentTimestampNs - timestampOfFirstUpdateNs > timeLimitNs
         ) {
-            _result.complete(
-                Result3A(frameMetadata.frameNumber, Result3A.Status.TIME_LIMIT_REACHED)
-            )
+            _result.complete(Result3A(Result3A.Status.TIME_LIMIT_REACHED, frameMetadata))
             return true
         }
 
@@ -110,9 +108,7 @@
         if (frameNumberOfFirstUpdate != null && frameLimit != null &&
             currentFrameNumber.value - frameNumberOfFirstUpdate.value > frameLimit
         ) {
-            _result.complete(
-                Result3A(frameMetadata.frameNumber, Result3A.Status.FRAME_LIMIT_REACHED)
-            )
+            _result.complete(Result3A(Result3A.Status.FRAME_LIMIT_REACHED, frameMetadata))
             return true
         }
 
@@ -122,7 +118,7 @@
                 return false
             }
         }
-        _result.complete(Result3A(frameMetadata.frameNumber, Result3A.Status.OK))
+        _result.complete(Result3A(Result3A.Status.OK, frameMetadata))
         return true
     }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index 33c4844..aae32a7 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -103,7 +103,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // We now check if the correct sequence of requests were submitted by lock3AForCapture call.
@@ -180,7 +180,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
@@ -217,7 +217,7 @@
 
         cameraResponse.await()
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
index ba01b9c..89e5ebb 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
@@ -18,6 +18,7 @@
 
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.params.MeteringRectangle
 import android.os.Build
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.Lock3ABehavior
@@ -109,7 +110,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // We not check if the correct sequence of requests were submitted by lock3A call. The
@@ -199,7 +200,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // A single request to lock AF must have been used as well.
@@ -275,7 +276,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // There should be one more request to lock AE after new scan is done.
@@ -353,7 +354,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // There should be one request to monitor AF to finish it's scan.
@@ -433,7 +434,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // One request to cancel AF to start a new scan.
@@ -519,7 +520,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // There should be one request to monitor AF to finish it's scan.
@@ -613,7 +614,7 @@
         }
 
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
 
         // One request to cancel AF to start a new scan.
@@ -643,6 +644,98 @@
         )
     }
 
+    @Test
+    fun testLock3AWithRegions(): Unit = runBlocking {
+        initGraphProcessor()
+
+        val afMeteringRegion = MeteringRectangle(1, 1, 100, 100, 2)
+        val aeMeteringRegion = MeteringRectangle(10, 15, 140, 140, 3)
+        val result = controller3A.lock3A(
+            aeRegions = listOf(aeMeteringRegion),
+            afRegions = listOf(afMeteringRegion),
+            afLockBehavior = Lock3ABehavior.IMMEDIATE,
+            aeLockBehavior = Lock3ABehavior.IMMEDIATE
+        )
+        assertThat(result.isCompleted).isFalse()
+
+        // Since requirement of to lock both AE and AF immediately, the requests to lock AE and AF
+        // are sent right away. The result of lock3A call will complete once AE and AF have reached
+        // their desired states. In this response i.e cameraResponse1, AF is still scanning so the
+        // result won't be complete.
+        val cameraResponse = GlobalScope.async {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_PASSIVE_SCAN,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        cameraResponse.await()
+        assertThat(result.isCompleted).isFalse()
+
+        // One we we are notified that the AE and AF are in locked state, the result of lock3A call
+        // will complete.
+        GlobalScope.launch {
+            listener3A.onRequestSequenceCreated(
+                FakeRequestMetadata(
+                    requestNumber = RequestNumber(1)
+                )
+            )
+            listener3A.onPartialCaptureResult(
+                FakeRequestMetadata(requestNumber = RequestNumber(1)),
+                FrameNumber(101L),
+                FakeFrameMetadata(
+                    frameNumber = FrameNumber(101L),
+                    resultMetadata = mapOf(
+                        CaptureResult.CONTROL_AF_STATE to CaptureResult
+                            .CONTROL_AF_STATE_FOCUSED_LOCKED,
+                        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+                    )
+                )
+            )
+        }
+
+        val result3A = result.await()
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+
+        val aeRegions = graphState3A.aeRegions!!
+        assertThat(aeRegions.size).isEqualTo(1)
+        assertThat(aeRegions[0]).isEqualTo(aeMeteringRegion)
+
+        val afRegions = graphState3A.afRegions!!
+        assertThat(afRegions.size).isEqualTo(1)
+        assertThat(afRegions[0]).isEqualTo(afMeteringRegion)
+
+        // We not check if the correct sequence of requests were submitted by lock3A call. The
+        // request should be a repeating request to lock AE.
+        val request1 = requestProcessor.nextEvent().requestSequence
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+
+        // The second request should be a single request to lock AF.
+        val request2 = requestProcessor.nextEvent().requestSequence
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+            CaptureRequest.CONTROL_AF_TRIGGER_START
+        )
+        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+            true
+        )
+    }
+
     private fun initGraphProcessor() {
         graphProcessor.onGraphStarted(requestProcessor)
         graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
index 2e0c850..df382c3 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
@@ -76,7 +76,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -108,7 +108,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -144,7 +144,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
index 659d3f6..2a57464 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
@@ -23,7 +23,6 @@
 import androidx.camera.camera2.pipe.AeMode
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.AwbMode
-import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestNumber
@@ -83,7 +82,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -111,7 +110,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -139,7 +138,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -167,7 +166,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -195,7 +194,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -228,7 +227,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -240,7 +239,7 @@
 
         // Since the request processor is closed the submit3A method call will fail.
         val result = controller3A.submit3A(aeMode = AeMode.ON_ALWAYS_FLASH).await()
-        assertThat(result.frameNumber).isEqualTo(FRAME_NUMBER_INVALID)
+        assertThat(result.frameMetadata).isNull()
         assertThat(result.status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
     }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
index 6f2e791..2e145aa 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
@@ -107,7 +107,7 @@
         }
 
         val result3A = result.await()
-        Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        Truth.assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         Truth.assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -168,7 +168,7 @@
         }
 
         val result3A = result.await()
-        Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        Truth.assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         Truth.assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -231,7 +231,7 @@
         }
 
         val result3A = result.await()
-        Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        Truth.assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         Truth.assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -298,7 +298,7 @@
         }
 
         val result3A = result.await()
-        Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        Truth.assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         Truth.assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
index 990529b..54d025f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
@@ -96,7 +96,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -124,7 +124,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -152,7 +152,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -180,7 +180,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -208,7 +208,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
@@ -240,7 +240,7 @@
             )
         }
         val result3A = result.await()
-        assertThat(result3A.frameNumber.value).isEqualTo(101L)
+        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
         assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
     }
 
diff --git a/camera/camera-camera2/api/public_plus_experimental_current.txt b/camera/camera-camera2/api/public_plus_experimental_current.txt
index ddf3bf4..4d34724 100644
--- a/camera/camera-camera2/api/public_plus_experimental_current.txt
+++ b/camera/camera-camera2/api/public_plus_experimental_current.txt
@@ -55,7 +55,7 @@
     method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCamera2Interop {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCamera2Interop {
   }
 
 }
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 7f670ad9..5b1bc69 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryVersions
+
 import androidx.build.LibraryGroups
 import androidx.build.Publish
 
+import static androidx.build.dependencies.DependenciesKt.*
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -27,10 +28,8 @@
 
 dependencies {
     api(project(":camera:camera-core"))
-
-    implementation("androidx.core:core:1.1.0")
     api("androidx.annotation:annotation:1.0.0")
-    api("androidx.annotation:annotation-experimental:1.0.0-rc01")
+    implementation("androidx.core:core:1.1.0")
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
     implementation(GUAVA_LISTENABLE_FUTURE)
     implementation(AUTO_VALUE_ANNOTATIONS)
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
deleted file mode 100644
index 1406fb1..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2;
-
-import android.content.Context;
-
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraX;
-import androidx.camera.core.CameraXConfig;
-import androidx.camera.core.ImageAnalysis;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Preview;
-import androidx.camera.core.internal.CameraUseCaseAdapter;
-import androidx.camera.testing.CameraUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.Arrays;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Contains tests for {@link androidx.camera.core.CameraX} which varies use case combinations to
- * run.
- */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public final class UseCaseCombinationTest {
-    private static final CameraSelector DEFAULT_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA;
-
-    @Rule
-    public TestRule mCameraRule = CameraUtil.grantCameraPermissionAndPreTest();
-
-    private Context mContext;
-
-    @Before
-    public void setUp() {
-        mContext = ApplicationProvider.getApplicationContext();
-        final CameraXConfig config = Camera2Config.defaultConfig();
-        CameraX.initialize(mContext, config);
-    }
-
-    @After
-    public void tearDown() throws InterruptedException, ExecutionException, TimeoutException {
-        CameraX.shutdown().get(10000, TimeUnit.MILLISECONDS);
-    }
-
-    /**
-     * Test Combination: Preview + ImageCapture
-     */
-    @Test
-    public void previewCombinesImageCapture()  {
-        final Preview preview = initPreview();
-        final ImageCapture imageCapture = initImageCapture();
-
-        CameraUseCaseAdapter camera = CameraUtil.createCameraUseCaseAdapter(mContext,
-                DEFAULT_SELECTOR);
-        camera.detachUseCases();
-
-        // TODO(b/160249108) move off of main thread once UseCases can be attached on any
-        //  thread
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            // This should not throw CameraUseCaseAdapter.CameraException
-            try {
-                camera.addUseCases(Arrays.asList(preview, imageCapture));
-            } catch (CameraUseCaseAdapter.CameraException e) {
-                throw new IllegalArgumentException(e);
-            }
-        });
-    }
-
-    /**
-     * Test Combination: Preview + ImageAnalysis
-     */
-    @Test
-    public void previewCombinesImageAnalysis()  {
-        final Preview preview = initPreview();
-        final ImageAnalysis imageAnalysis = initImageAnalysis();
-
-        CameraUseCaseAdapter camera = CameraUtil.createCameraUseCaseAdapter(mContext,
-                DEFAULT_SELECTOR);
-        camera.detachUseCases();
-
-        // TODO(b/160249108) move off of main thread once UseCases can be attached on any
-        //  thread
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            // This should not throw CameraUseCaseAdapter.CameraException
-            try {
-                camera.addUseCases(Arrays.asList(preview, imageAnalysis));
-            } catch (CameraUseCaseAdapter.CameraException e) {
-                throw new IllegalArgumentException(e);
-            }
-        });
-    }
-
-    /** Test Combination: Preview + ImageAnalysis + ImageCapture */
-    @Test
-    public void previewCombinesImageAnalysisAndImageCapture() {
-        final Preview preview = initPreview();
-        final ImageAnalysis imageAnalysis = initImageAnalysis();
-        final ImageCapture imageCapture = initImageCapture();
-
-        CameraUseCaseAdapter camera = CameraUtil.createCameraUseCaseAdapter(mContext,
-                DEFAULT_SELECTOR);
-        camera.detachUseCases();
-
-        // TODO(b/160249108) move off of main thread once UseCases can be attached on any
-        //  thread
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            // This should not throw CameraUseCaseAdapter.CameraException
-            try {
-                camera.addUseCases(Arrays.asList(preview, imageAnalysis, imageCapture));
-            } catch (CameraUseCaseAdapter.CameraException e) {
-                throw new IllegalArgumentException(e);
-            }
-        });
-    }
-
-    private Preview initPreview() {
-        return new Preview.Builder().setTargetName("Preview").build();
-    }
-
-    private ImageAnalysis initImageAnalysis() {
-        return new ImageAnalysis.Builder()
-                .setTargetName("ImageAnalysis")
-                .build();
-    }
-
-    private ImageCapture initImageCapture() {
-        return new ImageCapture.Builder().build();
-    }
-}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
index b0debf9..dbe9b8e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
@@ -22,9 +22,9 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.camera2.interop.CaptureRequestOptions;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.ExtendableBuilder;
@@ -36,7 +36,7 @@
 /**
  * Internal shared implementation details for camera 2 interop.
  */
-@UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
 public final class Camera2ImplConfig extends CaptureRequestOptions {
 
     /** @hide */
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index bb39cdb..afa5645 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -33,8 +33,8 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.internal.annotation.CameraExecutor;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
@@ -102,7 +102,7 @@
  * requests end in {@code ImmediateFailedFuture}. Any cached requests are dropped.</li>
  * </ul>
  */
-@UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
 public class Camera2CameraControlImpl implements CameraControlInternal {
     private static final String TAG = "Camera2CameraControlImp";
     @VisibleForTesting
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 69c20f1..41f1219 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -24,7 +24,7 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
 import androidx.camera.camera2.interop.Camera2CameraInfo;
@@ -61,7 +61,7 @@
  * CameraCaptureCallbacks added before this link will also be added
  * to the {@link Camera2CameraControlImpl}.
  */
-@UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
 public final class Camera2CameraInfoImpl implements CameraInfoInternal {
 
     private static final String TAG = "Camera2CameraInfo";
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureOptionUnpacker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureOptionUnpacker.java
index 99611c7..e1e385a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureOptionUnpacker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureOptionUnpacker.java
@@ -17,7 +17,7 @@
 package androidx.camera.camera2.internal;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.impl.CaptureConfig;
@@ -33,7 +33,7 @@
 
     static final Camera2CaptureOptionUnpacker INSTANCE = new Camera2CaptureOptionUnpacker();
 
-    @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     @Override
     public void unpack(@NonNull UseCaseConfig<?> config,
             @NonNull final CaptureConfig.Builder builder) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
index 74b2b20..59f815e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
@@ -23,7 +23,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.interop.CaptureRequestOptions;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.Logger;
@@ -69,7 +69,7 @@
         return surfaceList;
     }
 
-    @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     private static void applyImplementationOptionToCaptureBuilder(
             CaptureRequest.Builder builder, Config config) {
         CaptureRequestOptions bundle = CaptureRequestOptions.Builder.from(config).build();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
index 577ad8c..3132339 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
@@ -17,7 +17,7 @@
 package androidx.camera.camera2.internal;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.impl.CameraEventCallbacks;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -35,7 +35,7 @@
 
     static final Camera2SessionOptionUnpacker INSTANCE = new Camera2SessionOptionUnpacker();
 
-    @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     @Override
     public void unpack(@NonNull UseCaseConfig<?> config,
             @NonNull final SessionConfig.Builder builder) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index a07b42b..3fa60a5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -27,7 +27,7 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.impl.CameraEventCallbacks;
 import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
@@ -261,7 +261,7 @@
         }
     }
 
-    @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     @NonNull
     private ListenableFuture<Void> openCaptureSession(@NonNull List<Surface> configuredSurfaces,
             @NonNull SessionConfig sessionConfig, @NonNull CameraDevice cameraDevice) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
index 12e6f7f..8b87d66 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
@@ -23,7 +23,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.internal.annotation.CameraExecutor;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
@@ -53,7 +53,7 @@
  * The task will fails with {@link CameraControl.OperationCanceledException} if the camera is
  * closed.
  */
-@UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+@OptIn(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
 public class ExposureControl {
 
     private static final int DEFAULT_EXPOSURE_COMPENSATION = 0;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/ExperimentalCamera2Interop.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/ExperimentalCamera2Interop.java
index f9df834..84441ee 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/ExperimentalCamera2Interop.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/ExperimentalCamera2Interop.java
@@ -18,6 +18,8 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
+import androidx.annotation.RequiresOptIn;
+
 import java.lang.annotation.Retention;
 
 /**
@@ -36,10 +38,7 @@
  *
  * <p>These will be changed in future release possibly, hence add @Experimental annotation.
  */
-// TODO(b/170599666): Experimental/UseExperimental is deprecated and has to be replaced with
-//  RequiresOptIn/OptIn.
-@SuppressWarnings("deprecation")
 @Retention(CLASS)
[email protected]
+@RequiresOptIn
 public @interface ExperimentalCamera2Interop {
 }
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index b10368c..32b510d 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -90,25 +90,25 @@
     ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAvailableCamerasLimiter {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAvailableCamerasLimiter {
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraFilter {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraFilter {
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCustomizableThreads {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCustomizableThreads {
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalExposureCompensation {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalExposureCompensation {
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLogging {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLogging {
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroup {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroup {
   }
 
   @androidx.camera.core.ExperimentalExposureCompensation public interface ExposureState {
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 4d18631..4a8792b 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -14,10 +14,12 @@
  * limitations under the License.
  */
 
-import static androidx.build.dependencies.DependenciesKt.*
+
 import androidx.build.LibraryGroups
 import androidx.build.Publish
 
+import static androidx.build.dependencies.DependenciesKt.*
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -26,9 +28,10 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.0.0")
-    api("androidx.annotation:annotation-experimental:1.0.0-rc01")
     api("androidx.lifecycle:lifecycle-livedata:2.1.0")
     api(GUAVA_LISTENABLE_FUTURE)
+    api("androidx.annotation:annotation-experimental:1.1.0-rc01")
+    api(KOTLIN_STDLIB) // Added for annotation-experimental
     implementation("androidx.exifinterface:exifinterface:1.0.0")
     implementation("androidx.core:core:1.1.0")
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java
index b1a20e5..0013339 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java
@@ -26,7 +26,7 @@
 import android.graphics.Rect;
 import android.media.Image;
 
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -129,7 +129,7 @@
     }
 
     @Test
-    @UseExperimental(markerClass = ExperimentalGetImage.class)
+    @OptIn(markerClass = ExperimentalGetImage.class)
     public void getImage_returnsWrappedImage() {
         assertThat(mImageProxy.getImage()).isEqualTo(mImage);
     }
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java
index e40d2933..aaa9ed5 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java
@@ -25,7 +25,7 @@
 import android.graphics.ImageFormat;
 import android.graphics.Rect;
 
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -129,7 +129,7 @@
     }
 
     @Test
-    @UseExperimental(markerClass = ExperimentalGetImage.class)
+    @OptIn(markerClass = ExperimentalGetImage.class)
     public void getImage_returnsImageForWrappedImage() {
         assertThat(mImageProxy.getImage()).isEqualTo(mBaseImageProxy.getImage());
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
index e4d5954..73e8342 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
@@ -18,9 +18,9 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.LensFacingCameraFilter;
 
@@ -94,7 +94,7 @@
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     @NonNull
     public List<CameraInfo> filter(@NonNull List<CameraInfo> cameraInfos) {
         List<CameraInfo> input = new ArrayList<>(cameraInfos);
@@ -168,7 +168,7 @@
      * @hide
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     @Nullable
     public Integer getLensFacing() {
         Integer currentLensFacing = null;
@@ -212,7 +212,7 @@
          * <p>If lens facing is already set, this will add extra requirement for lens facing
          * instead of replacing the previous setting.
          */
-        @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+        @OptIn(markerClass = ExperimentalCameraFilter.class)
         @NonNull
         public Builder requireLensFacing(@LensFacing int lensFacing) {
             mCameraFilterSet.add(new LensFacingCameraFilter(lensFacing));
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index 8bcdb97..0ee2ec9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -29,9 +29,9 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.impl.CameraDeviceSurfaceManager;
 import androidx.camera.core.impl.CameraFactory;
 import androidx.camera.core.impl.CameraInternal;
@@ -539,7 +539,7 @@
     /**
      * Initializes camera stack on the given thread and retry recursively until timeout.
      */
-    @UseExperimental(markerClass = ExperimentalAvailableCamerasLimiter.class)
+    @OptIn(markerClass = ExperimentalAvailableCamerasLimiter.class)
     private void initAndRetryRecursively(
             @NonNull Executor cameraExecutor,
             long startMs,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
index 99fc74f..7d03a531 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
 
 import java.lang.annotation.Retention;
 
@@ -40,6 +40,6 @@
  * reduce the latency.
  */
 @Retention(CLASS)
-@Experimental
+@RequiresOptIn
 public @interface ExperimentalAvailableCamerasLimiter {
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCameraFilter.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCameraFilter.java
index 1bdf1b6..7ce851b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCameraFilter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCameraFilter.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
 
 import java.lang.annotation.Retention;
 
@@ -32,6 +32,6 @@
  * available camera, the camera selector will thrown an IllegalArgumentException.
  */
 @Retention(CLASS)
-@Experimental
+@RequiresOptIn
 public @interface ExperimentalCameraFilter {
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCustomizableThreads.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCustomizableThreads.java
index cc9772d..028ef58 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCustomizableThreads.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCustomizableThreads.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
 
 import java.lang.annotation.Retention;
 
@@ -36,6 +36,6 @@
  * requirements.
  */
 @Retention(CLASS)
-@Experimental
+@RequiresOptIn
 public @interface ExperimentalCustomizableThreads {
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java
index b7c7ce4..6e22ab7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java
@@ -19,7 +19,7 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
 
 import java.lang.annotation.Retention;
 
@@ -32,6 +32,6 @@
  * {@link androidx.camera.core.CameraInfo}.
  */
 @Retention(CLASS)
-@Experimental
+@RequiresOptIn
 public @interface ExperimentalExposureCompensation {
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalGetImage.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalGetImage.java
index 0aedde3..b84dc050 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalGetImage.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalGetImage.java
@@ -20,7 +20,7 @@
 
 import android.media.Image;
 
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
 
 import java.lang.annotation.Retention;
 
@@ -37,6 +37,6 @@
  * should be called on the ImageProxy from which the Image was retrieved.
  */
 @Retention(CLASS)
-@Experimental
+@RequiresOptIn
 public @interface ExperimentalGetImage {
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalLogging.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalLogging.java
index 3de235b..e2b415b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalLogging.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalLogging.java
@@ -16,7 +16,7 @@
 
 package androidx.camera.core;
 
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -27,6 +27,6 @@
  * level to use inside CameraX.
  */
 @Retention(RetentionPolicy.CLASS)
-@Experimental
+@RequiresOptIn
 public @interface ExperimentalLogging {
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalUseCaseGroup.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalUseCaseGroup.java
index 216a281..eaa2bd6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalUseCaseGroup.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalUseCaseGroup.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
 
 import java.lang.annotation.Retention;
 
@@ -33,6 +33,6 @@
  * all the use cases.
  */
 @Retention(CLASS)
-@Experimental
+@RequiresOptIn
 public @interface ExperimentalUseCaseGroup {
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index faa196b..e9af877 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -54,11 +54,11 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
 import androidx.camera.core.impl.CameraInfoInternal;
@@ -192,7 +192,7 @@
     }
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     SessionConfig.Builder createPipeline(@NonNull String cameraId, @NonNull PreviewConfig config,
             @NonNull Size resolution) {
         Threads.checkMainThread();
@@ -351,7 +351,7 @@
      *                        {@link #setSurfaceProvider(SurfaceProvider)}.
      */
     @UiThread
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     public void setSurfaceProvider(@NonNull Executor executor,
             @Nullable SurfaceProvider surfaceProvider) {
         Threads.checkMainThread();
@@ -528,7 +528,7 @@
      * @hide
      */
     @Override
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     @RestrictTo(Scope.LIBRARY)
     public void setViewPortCropRect(@NonNull Rect viewPortCropRect) {
         super.setViewPortCropRect(viewPortCropRect);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/LensFacingCameraFilter.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/LensFacingCameraFilter.java
index eb062bd..55c247b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/LensFacingCameraFilter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/LensFacingCameraFilter.java
@@ -17,7 +17,7 @@
 package androidx.camera.core.impl;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.core.CameraFilter;
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
@@ -30,7 +30,7 @@
 /**
  * A filter that filters camera based on lens facing.
  */
-@UseExperimental(markerClass = ExperimentalCameraFilter.class)
+@OptIn(markerClass = ExperimentalCameraFilter.class)
 public class LensFacingCameraFilter implements CameraFilter {
     @CameraSelector.LensFacing
     private int mLensFacing;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 79fbc5d..0619dfc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -22,7 +22,7 @@
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraFilter;
@@ -175,7 +175,7 @@
      * @throws CameraException Thrown if the combination of newly added UseCases and the
      *                         currently added UseCases exceed the capability of the camera.
      */
-    @UseExperimental(markerClass = androidx.camera.core.ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = androidx.camera.core.ExperimentalUseCaseGroup.class)
     public void addUseCases(@NonNull Collection<UseCase> useCases) throws CameraException {
         synchronized (mLock) {
             List<UseCase> newUseCases = new ArrayList<>();
@@ -366,7 +366,7 @@
         return suggestedResolutions;
     }
 
-    @UseExperimental(markerClass = androidx.camera.core.ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = androidx.camera.core.ExperimentalUseCaseGroup.class)
     private void updateViewPort(@NonNull Map<UseCase, Size> suggestedResolutionsMap,
             @NonNull Collection<UseCase> useCases) {
         synchronized (mLock) {
@@ -492,7 +492,7 @@
     }
 
     @Override
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     public void setExtendedConfig(@Nullable CameraConfig cameraConfig) throws CameraException {
         synchronized (mLock) {
             CameraConfig newCameraConfig = cameraConfig == null ? CameraConfigs.emptyConfig() :
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/CameraSelectorTest.java b/camera/camera-core/src/test/java/androidx/camera/core/CameraSelectorTest.java
index 02f4bc5..afd96b6 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/CameraSelectorTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/CameraSelectorTest.java
@@ -21,7 +21,7 @@
 
 import android.os.Build;
 
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.core.impl.CameraControlInternal;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.testing.fakes.FakeCamera;
@@ -98,7 +98,7 @@
         cameraSelectorBuilder.build().getLensFacing();
     }
 
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     @Test
     public void canAppendFilters() {
         CameraFilter filter0 = mock(CameraFilter.class);
@@ -124,7 +124,7 @@
         assertThat(CameraSelector.DEFAULT_FRONT_CAMERA.select(mCameras)).isEqualTo(mFrontCamera);
     }
 
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     @Test(expected = IllegalArgumentException.class)
     public void exception_extraOutputCamera() {
         CameraSelector.Builder cameraSelectorBuilder = new CameraSelector.Builder();
@@ -137,7 +137,7 @@
         cameraSelectorBuilder.build().select(mCameras);
     }
 
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     @Test(expected = UnsupportedOperationException.class)
     public void exception_extraInputCamera() {
         CameraSelector.Builder cameraSelectorBuilder = new CameraSelector.Builder();
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
index a42275e..90de15a 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.extensions.impl;
 
+import android.graphics.ImageFormat;
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
 import android.util.Pair;
@@ -35,9 +36,9 @@
      * <p> The result of the processing step should be written to the {@link Surface} that was
      * received by {@link #onOutputSurface(Surface, int)}.
      *
-     * @param results The map of images and metadata to process. The {@link Image} that are
-     *                contained within the map will become invalid after this method completes,
-     *                so no references to them should be kept.
+     * @param results The map of {@link ImageFormat#YUV_420_888} format images and metadata to
+     *                process. The {@link Image} that are contained within the map will become
+     *                invalid after this method completes, so no references to them should be kept.
      */
     void process(Map<Integer, Pair<Image, TotalCaptureResult>> results);
 }
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
index f1817a7..2879568 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
@@ -20,6 +20,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
 
 /**
  * Provides interfaces that the OEM needs to implement to handle the state change.
@@ -51,8 +52,9 @@
      * This will be invoked before creating a
      * {@link android.hardware.camera2.CameraCaptureSession}. The {@link CaptureRequest}
      * parameters returned via {@link CaptureStageImpl} will be passed to the camera device as
-     * part of the capture session initialization via setSessionParameters(). The valid parameter
-     * is a subset of the available capture request parameters.
+     * part of the capture session initialization via
+     * {@link SessionConfiguration#setSessionParameters(CaptureRequest)} which only supported from
+     * API level 28. The valid parameter is a subset of the available capture request parameters.
      *
      * @return The request information to set the session wide camera parameters.
      */
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
index 629f13c..e7ecaa1 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.extensions.impl;
 
+import android.graphics.ImageFormat;
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
 
@@ -32,8 +33,8 @@
      * <p> The result of the processing step should be written to the {@link android.view.Surface}
      * that was received by {@link ProcessorImpl#onOutputSurface(android.view.Surface, int)}.
      *
-     * @param image The image to process. This will be invalid after the method completes so no
-     *              reference to it should be kept.
+     * @param image  The {@link ImageFormat#YUV_420_888} format image to process. This will be
+     *               invalid after the method completes so no reference to it should be kept.
      * @param result The metadata associated with the image to process.
      */
     void process(Image image, TotalCaptureResult result);
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.java
deleted file mode 100644
index 0a3e099..0000000
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.extensions;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Context;
-import android.hardware.camera2.CameraAccessException;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.core.CameraInfoUnavailableException;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraX;
-import androidx.camera.extensions.impl.ImageCaptureExtenderImpl;
-import androidx.camera.extensions.util.ExtensionsTestUtil;
-import androidx.camera.testing.CameraUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@RunWith(AndroidJUnit4.class)
-public class ImageCaptureExtenderValidationTest {
-    @Rule
-    public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest();
-    private final Context mContext = ApplicationProvider.getApplicationContext();
-
-    @Before
-    public void setUp() throws InterruptedException, ExecutionException, TimeoutException {
-        assumeTrue(CameraUtil.deviceHasCamera());
-        CameraX.initialize(mContext, Camera2Config.defaultConfig());
-
-        assumeTrue(ExtensionsTestUtil.initExtensions(mContext));
-    }
-
-    @After
-    public void tearDown() throws ExecutionException, InterruptedException, TimeoutException {
-        CameraX.shutdown().get(10000, TimeUnit.MILLISECONDS);
-        ExtensionsManager.deinit().get();
-    }
-
-    @Test
-    @LargeTest
-    public void getSupportedResolutionsImplementationTest()
-            throws CameraInfoUnavailableException, CameraAccessException {
-        // getSupportedResolutions supported since version 1.1
-        assumeTrue(ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_1) >= 0);
-
-        // Uses for-loop to check all possible effect/lens facing combinations
-        for (Object[] EffectLensFacingPair :
-                ExtensionsTestUtil.getAllEffectLensFacingCombinations()) {
-            ExtensionsManager.EffectMode effectMode =
-                    (ExtensionsManager.EffectMode) EffectLensFacingPair[0];
-            @CameraSelector.LensFacing int lensFacing = (int) EffectLensFacingPair[1];
-
-            assumeTrue(CameraUtil.hasCameraWithLensFacing(lensFacing));
-            assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing));
-
-            // Retrieves the target format/resolutions pair list from vendor library for the
-            // target effect mode.
-            ImageCaptureExtenderImpl impl = ExtensionsTestUtil.createImageCaptureExtenderImpl(
-                    effectMode, lensFacing);
-
-            // NoSuchMethodError will be thrown if getSupportedResolutions is not
-            // implemented in vendor library, and then the test will fail.
-            impl.getSupportedResolutions();
-        }
-    }
-}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.kt
new file mode 100644
index 0000000..04abfb93
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions
+
+import android.content.Context
+import android.hardware.camera2.CameraAccessException
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraInfoUnavailableException
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.extensions.util.ExtensionsTestUtil
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+@SmallTest
+@RunWith(Parameterized::class)
+class ImageCaptureExtenderValidationTest(
+    @field:Extensions.ExtensionMode @param:Extensions.ExtensionMode private val extensionMode: Int,
+    @field:CameraSelector.LensFacing @param:CameraSelector.LensFacing private val lensFacing: Int
+) {
+    private val context =
+        ApplicationProvider.getApplicationContext<Context>()
+
+    private val effectMode: ExtensionsManager.EffectMode =
+        ExtensionsTestUtil.extensionModeToEffectMode(extensionMode)
+
+    private lateinit var extensions: Extensions
+
+    @Before
+    @Throws(Exception::class)
+    fun setUp() {
+        Assume.assumeTrue(CameraUtil.deviceHasCamera())
+        CameraX.initialize(context, Camera2Config.defaultConfig()).get()
+        Assume.assumeTrue(
+            CameraUtil.hasCameraWithLensFacing(
+                lensFacing
+            )
+        )
+        Assume.assumeTrue(ExtensionsTestUtil.initExtensions(context))
+        extensions = ExtensionsManager.getExtensions(context)
+    }
+
+    @After
+    @Throws(
+        InterruptedException::class,
+        ExecutionException::class,
+        TimeoutException::class
+    )
+    fun cleanUp() {
+        CameraX.shutdown()[10000, TimeUnit.MILLISECONDS]
+        ExtensionsManager.deinit().get()
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "extension = {0}, facing = {1}")
+        fun initParameters(): Collection<Array<Any>> =
+            ExtensionsTestUtil.getAllExtensionsLensFacingCombinations()
+    }
+
+    @Test
+    @Throws(
+        CameraInfoUnavailableException::class,
+        CameraAccessException::class
+    )
+    fun getSupportedResolutionsImplementationTest() {
+        // getSupportedResolutions supported since version 1.1
+        Assume.assumeTrue(ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_1) >= 0)
+        Assume.assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing))
+
+        // Creates the ImageCaptureExtenderImpl to retrieve the target format/resolutions pair list
+        // from vendor library for the target effect mode.
+        val impl = ExtensionsTestUtil.createImageCaptureExtenderImpl(effectMode, lensFacing)
+
+        // NoSuchMethodError will be thrown if getSupportedResolutions is not implemented in
+        // vendor library, and then the test will fail.
+        impl.supportedResolutions
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.O_MR1)
+    @Throws(
+        CameraInfoUnavailableException::class,
+        CameraAccessException::class
+    )
+    fun returnsNullFromOnPresetSession_whenAPILevelOlderThan28() {
+        Assume.assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing))
+
+        // Creates the ImageCaptureExtenderImpl to check that onPresetSession() returns null when
+        // API level is older than 28.
+        val impl = ExtensionsTestUtil.createImageCaptureExtenderImpl(effectMode, lensFacing)
+        assertThat(impl.onPresetSession()).isNull()
+    }
+}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.java
deleted file mode 100644
index 48fa332..0000000
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.extensions;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Context;
-import android.hardware.camera2.CameraAccessException;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.core.CameraInfoUnavailableException;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraX;
-import androidx.camera.extensions.impl.PreviewExtenderImpl;
-import androidx.camera.extensions.util.ExtensionsTestUtil;
-import androidx.camera.testing.CameraUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@RunWith(AndroidJUnit4.class)
-public class PreviewExtenderValidationTest {
-    @Rule
-    public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest();
-    private final Context mContext = ApplicationProvider.getApplicationContext();
-
-    @Before
-    public void setUp() throws InterruptedException, ExecutionException, TimeoutException {
-        assumeTrue(CameraUtil.deviceHasCamera());
-        CameraX.initialize(mContext, Camera2Config.defaultConfig());
-
-        assumeTrue(ExtensionsTestUtil.initExtensions(mContext));
-    }
-
-    @After
-    public void tearDown() throws ExecutionException, InterruptedException, TimeoutException {
-        CameraX.shutdown().get(10000, TimeUnit.MILLISECONDS);
-        ExtensionsManager.deinit().get();
-    }
-
-    @Test
-    @LargeTest
-    public void getSupportedResolutionsImplementationTest()
-            throws CameraInfoUnavailableException, CameraAccessException {
-        // getSupportedResolutions supported since version 1.1
-        assumeTrue(ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_1) >= 0);
-
-        // Uses for-loop to check all possible effect/lens facing combinations
-        for (Object[] EffectLensFacingPair :
-                ExtensionsTestUtil.getAllEffectLensFacingCombinations()) {
-            ExtensionsManager.EffectMode effectMode =
-                    (ExtensionsManager.EffectMode) EffectLensFacingPair[0];
-            @CameraSelector.LensFacing int lensFacing = (int) EffectLensFacingPair[1];
-
-            assumeTrue(CameraUtil.hasCameraWithLensFacing(lensFacing));
-            assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing));
-
-            // Retrieves the target format/resolutions pair list from vendor library for the
-            // target effect mode.
-            PreviewExtenderImpl impl = ExtensionsTestUtil.createPreviewExtenderImpl(effectMode,
-                    lensFacing);
-
-            // NoSuchMethodError will be thrown if getSupportedResolutions is not
-            // implemented in vendor library, and then the test will fail.
-            impl.getSupportedResolutions();
-        }
-    }
-}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.kt
new file mode 100644
index 0000000..a94b9ce
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions
+
+import android.content.Context
+import android.hardware.camera2.CameraAccessException
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraInfoUnavailableException
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.extensions.util.ExtensionsTestUtil
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+@SmallTest
+@RunWith(Parameterized::class)
+class PreviewExtenderValidationTest(
+    @field:Extensions.ExtensionMode @param:Extensions.ExtensionMode private val extensionMode: Int,
+    @field:CameraSelector.LensFacing @param:CameraSelector.LensFacing private val lensFacing: Int
+) {
+    private val context =
+        ApplicationProvider.getApplicationContext<Context>()
+
+    private val effectMode: ExtensionsManager.EffectMode =
+        ExtensionsTestUtil.extensionModeToEffectMode(extensionMode)
+
+    private lateinit var extensions: Extensions
+
+    @Before
+    @Throws(Exception::class)
+    fun setUp() {
+        Assume.assumeTrue(CameraUtil.deviceHasCamera())
+        CameraX.initialize(context, Camera2Config.defaultConfig()).get()
+        Assume.assumeTrue(
+            CameraUtil.hasCameraWithLensFacing(
+                lensFacing
+            )
+        )
+        Assume.assumeTrue(ExtensionsTestUtil.initExtensions(context))
+        extensions = ExtensionsManager.getExtensions(context)
+    }
+
+    @After
+    @Throws(
+        InterruptedException::class,
+        ExecutionException::class,
+        TimeoutException::class
+    )
+    fun cleanUp() {
+        CameraX.shutdown()[10000, TimeUnit.MILLISECONDS]
+        ExtensionsManager.deinit().get()
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "extension = {0}, facing = {1}")
+        fun initParameters(): Collection<Array<Any>> =
+            ExtensionsTestUtil.getAllExtensionsLensFacingCombinations()
+    }
+
+    @Test
+    @Throws(
+        CameraInfoUnavailableException::class,
+        CameraAccessException::class
+    )
+    fun getSupportedResolutionsImplementationTest() {
+        // getSupportedResolutions supported since version 1.1
+        Assume.assumeTrue(ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_1) >= 0)
+        Assume.assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing))
+
+        // Creates the ImageCaptureExtenderImpl to retrieve the target format/resolutions pair list
+        // from vendor library for the target effect mode.
+        val impl = ExtensionsTestUtil.createPreviewExtenderImpl(effectMode, lensFacing)
+
+        // NoSuchMethodError will be thrown if getSupportedResolutions is not implemented in
+        // vendor library, and then the test will fail.
+        impl.supportedResolutions
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.O_MR1)
+    @Throws(
+        CameraInfoUnavailableException::class,
+        CameraAccessException::class
+    )
+    fun returnsNullFromOnPresetSession_whenAPILevelOlderThan28() {
+        Assume.assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing))
+
+        // Creates the ImageCaptureExtenderImpl to check that onPresetSession() returns null when
+        // API level is older than 28.
+        val impl = ExtensionsTestUtil.createPreviewExtenderImpl(effectMode, lensFacing)
+        Truth.assertThat(impl.onPresetSession()).isNull()
+    }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionCameraFilter.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionCameraFilter.java
index 6b1924d..06237ee 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionCameraFilter.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionCameraFilter.java
@@ -20,7 +20,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.interop.Camera2CameraInfo;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.CameraFilter;
@@ -38,7 +38,7 @@
  * A filter that filters camera based on extender implementation. If the implementation is
  * unavailable, the camera will be considered available.
  */
-@UseExperimental(markerClass = ExperimentalCameraFilter.class)
+@OptIn(markerClass = ExperimentalCameraFilter.class)
 public final class ExtensionCameraFilter implements CameraFilter {
     private final PreviewExtenderImpl mPreviewExtenderImpl;
     private final ImageCaptureExtenderImpl mImageCaptureExtenderImpl;
@@ -59,7 +59,7 @@
         mImageCaptureExtenderImpl = imageCaptureExtenderImpl;
     }
 
-    @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     @NonNull
     @Override
     public List<CameraInfo> filter(@NonNull List<CameraInfo> cameraInfos) {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/Extensions.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/Extensions.java
index 9c6f01d..f70b0b3 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/Extensions.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/Extensions.java
@@ -20,8 +20,8 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraFilter;
 import androidx.camera.core.CameraInfo;
@@ -133,7 +133,7 @@
      *                                  Camera can support the list of
      *                                  UseCases for the extension.
      */
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     public void setExtension(@NonNull Camera camera, @ExtensionMode int mode) {
         if (!isExtensionAvailable(camera, mode)) {
             throw new IllegalArgumentException("Extension mode not supported on camera: " + mode);
@@ -189,7 +189,7 @@
      * @param camera The Camera to check if it supports the extension.
      * @param mode   The extension mode to check
      */
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     public boolean isExtensionAvailable(@NonNull Camera camera, @ExtensionMode int mode) {
         CameraSelector cameraSelector =
                 new CameraSelector.Builder().addCameraFilter(getFilter(mode)).build();
@@ -204,7 +204,7 @@
         return true;
     }
 
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     private CameraFilter getFilter(@ExtensionMode int mode) {
         CameraFilter filter;
         try {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ImageCaptureExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ImageCaptureExtender.java
index 6605fce..5a57893 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ImageCaptureExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ImageCaptureExtender.java
@@ -18,14 +18,15 @@
 
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.impl.CameraEventCallback;
 import androidx.camera.camera2.impl.CameraEventCallbacks;
@@ -69,7 +70,7 @@
     private int mEffectMode;
     private ExtensionCameraFilter mExtensionCameraFilter;
 
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     void init(ImageCapture.Builder builder, ImageCaptureExtenderImpl implementation,
             @Extensions.ExtensionMode int effectMode) {
         mBuilder = builder;
@@ -93,7 +94,7 @@
      * Returns the camera specified with the given camera selector and this extension, null if
      * there's no available can be found.
      */
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     private String getCameraWithExtension(@NonNull CameraSelector cameraSelector) {
         CameraSelector.Builder extensionCameraSelectorBuilder =
                 CameraSelector.Builder.fromSelector(cameraSelector);
@@ -118,7 +119,7 @@
      * @param cameraSelector The selector used to determine the camera for which to enable
      *                       extensions.
      */
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     public void enableExtension(@NonNull CameraSelector cameraSelector) {
         String cameraId = getCameraWithExtension(cameraSelector);
         if (cameraId == null) {
@@ -267,7 +268,7 @@
             mContext = context;
         }
 
-        @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+        @OptIn(markerClass = ExperimentalCamera2Interop.class)
         @Override
         public void onAttach(@NonNull CameraInfo cameraInfo) {
             if (mActive.get()) {
@@ -301,7 +302,15 @@
             if (mActive.get()) {
                 CaptureStageImpl captureStageImpl = mImpl.onPresetSession();
                 if (captureStageImpl != null) {
-                    return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
+                    if (Build.VERSION.SDK_INT >= 28) {
+                        return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
+                    } else {
+                        Logger.w(TAG, "The CaptureRequest parameters returned from "
+                                + "onPresetSession() will be passed to the camera device as part "
+                                + "of the capture session via "
+                                + "SessionConfiguration#setSessionParameters(CaptureRequest) "
+                                + "which only supported from API level 28!");
+                    }
                 }
             }
             return null;
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
index 4a40b8a..19826ba 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
@@ -18,14 +18,15 @@
 
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
 import androidx.camera.camera2.impl.CameraEventCallback;
 import androidx.camera.camera2.impl.CameraEventCallbacks;
@@ -66,7 +67,7 @@
     private int mEffectMode;
     private ExtensionCameraFilter mExtensionCameraFilter;
 
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     void init(Preview.Builder builder, PreviewExtenderImpl implementation,
             @Extensions.ExtensionMode int effectMode) {
         mBuilder = builder;
@@ -90,7 +91,7 @@
      * Returns the camera specified with the given camera selector and this extension, null if
      * there's no available can be found.
      */
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     private String getCameraWithExtension(@NonNull CameraSelector cameraSelector) {
         CameraSelector.Builder extensionCameraSelectorBuilder =
                 CameraSelector.Builder.fromSelector(cameraSelector);
@@ -115,7 +116,7 @@
      * @param cameraSelector The selector used to determine the camera for which to enable
      *                       extensions.
      */
-    @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     public void enableExtension(@NonNull CameraSelector cameraSelector) {
         String cameraId = getCameraWithExtension(cameraSelector);
         if (cameraId == null) {
@@ -279,7 +280,7 @@
             mCloseableProcessor = closeableProcessor;
         }
 
-        @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+        @OptIn(markerClass = ExperimentalCamera2Interop.class)
         @Override
         public void onAttach(@NonNull CameraInfo cameraInfo) {
             synchronized (mLock) {
@@ -320,7 +321,15 @@
             synchronized (mLock) {
                 CaptureStageImpl captureStageImpl = mImpl.onPresetSession();
                 if (captureStageImpl != null) {
-                    return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
+                    if (Build.VERSION.SDK_INT >= 28) {
+                        return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
+                    } else {
+                        Logger.w(TAG, "The CaptureRequest parameters returned from "
+                                + "onPresetSession() will be passed to the camera device as part "
+                                + "of the capture session via "
+                                + "SessionConfiguration#setSessionParameters(CaptureRequest) "
+                                + "which only supported from API level 28!");
+                    }
                 }
             }
 
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index 2e0c377..c653ed5 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -20,7 +20,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.interop.Camera2CameraInfo;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.CameraInfo;
@@ -46,7 +46,7 @@
     @Extensions.ExtensionMode
     private int mEffectMode;
 
-    @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     public ImageCaptureConfigProvider(@Extensions.ExtensionMode int mode,
             @NonNull CameraInfo cameraInfo, @NonNull Context context) {
         try {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
index e70da40..384f169 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
@@ -20,7 +20,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.interop.Camera2CameraInfo;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.CameraInfo;
@@ -46,7 +46,7 @@
     @Extensions.ExtensionMode
     private int mEffectMode;
 
-    @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     public PreviewConfigProvider(@Extensions.ExtensionMode int mode,
             @NonNull CameraInfo cameraInfo, @NonNull Context context) {
         try {
diff --git a/camera/camera-lifecycle/api/public_plus_experimental_current.txt b/camera/camera-lifecycle/api/public_plus_experimental_current.txt
index 47b2878..cdd3299 100644
--- a/camera/camera-lifecycle/api/public_plus_experimental_current.txt
+++ b/camera/camera-lifecycle/api/public_plus_experimental_current.txt
@@ -1,10 +1,10 @@
 // Signature format: 4.0
 package androidx.camera.lifecycle {
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraProviderConfiguration {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraProviderConfiguration {
   }
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroupLifecycle {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroupLifecycle {
   }
 
   public final class ProcessCameraProvider implements androidx.camera.core.CameraProvider {
diff --git a/camera/camera-lifecycle/build.gradle b/camera/camera-lifecycle/build.gradle
index 57fe2aa..69a63fd 100644
--- a/camera/camera-lifecycle/build.gradle
+++ b/camera/camera-lifecycle/build.gradle
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryVersions
+
 import androidx.build.LibraryGroups
 import androidx.build.Publish
 
+import static androidx.build.dependencies.DependenciesKt.*
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -31,6 +32,7 @@
     api(project(":camera:camera-core"))
     implementation("androidx.core:core:1.1.0")
     implementation(AUTO_VALUE_ANNOTATIONS)
+
     annotationProcessor(AUTO_VALUE)
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalCameraProviderConfiguration.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalCameraProviderConfiguration.java
index b15354d..bedde47 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalCameraProviderConfiguration.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalCameraProviderConfiguration.java
@@ -18,6 +18,8 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
+import androidx.annotation.RequiresOptIn;
+
 import java.lang.annotation.Retention;
 
 /**
@@ -33,8 +35,7 @@
  *
  * @see androidx.camera.core.CameraXConfig.Provider
  */
-@SuppressWarnings("deprecation")
 @Retention(CLASS)
[email protected]
+@RequiresOptIn
 public @interface ExperimentalCameraProviderConfiguration {
 }
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalUseCaseGroupLifecycle.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalUseCaseGroupLifecycle.java
index 6c88ceb..dfc880d 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalUseCaseGroupLifecycle.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalUseCaseGroupLifecycle.java
@@ -18,6 +18,8 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
+import androidx.annotation.RequiresOptIn;
+
 import java.lang.annotation.Retention;
 
 /**
@@ -32,8 +34,7 @@
  *
  * TODO(b/159033688): Remove after the bug is fixed.
  */
-@SuppressWarnings("deprecation")
 @Retention(CLASS)
[email protected]
+@RequiresOptIn
 public @interface ExperimentalUseCaseGroupLifecycle {
 }
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
index 702ba88..9b2455d 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
@@ -23,6 +23,7 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.camera.core.Camera;
@@ -272,7 +273,7 @@
     @SuppressWarnings({"lambdaLast", "deprecation"})
     @MainThread
     @NonNull
-    @androidx.annotation.experimental.UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     public Camera bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner,
             @NonNull CameraSelector cameraSelector,
             @NonNull UseCase... useCases) {
@@ -295,7 +296,7 @@
     @SuppressWarnings({"lambdaLast", "deprecation"})
     @MainThread
     @NonNull
-    @androidx.annotation.experimental.UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     public Camera bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner,
             @NonNull CameraSelector cameraSelector,
             @NonNull UseCaseGroup useCaseGroup) {
@@ -364,7 +365,7 @@
     @SuppressWarnings({"lambdaLast", "unused", "deprecation"})
     @RestrictTo(Scope.LIBRARY_GROUP)
     @ExperimentalUseCaseGroup
-    @androidx.annotation.experimental.UseExperimental(markerClass = ExperimentalCameraFilter.class)
+    @OptIn(markerClass = ExperimentalCameraFilter.class)
     @NonNull
     public Camera bindToLifecycle(
             @NonNull LifecycleOwner lifecycleOwner,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 2f31568..997def5 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -18,8 +18,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraXConfig;
 import androidx.camera.core.ExperimentalAvailableCamerasLimiter;
@@ -39,7 +39,7 @@
     private static final String CAMERA_ID_1 = "1";
 
     /** Generates a fake {@link CameraXConfig}. */
-    @UseExperimental(markerClass = ExperimentalAvailableCamerasLimiter.class)
+    @OptIn(markerClass = ExperimentalAvailableCamerasLimiter.class)
     @NonNull
     public static CameraXConfig create() {
         return create(null);
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
new file mode 100644
index 0000000..694c4b8
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.content.Context
+import android.os.ParcelFileDescriptor
+import android.provider.MediaStore
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.FileDescriptor
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class OutputOptionsTest {
+
+    @Test
+    fun canBuildFileOutputOptions() {
+        val savedFile = File.createTempFile("CameraX", ".tmp")
+        savedFile.deleteOnExit()
+
+        val fileOutputOptions = FileOutputOptions.builder()
+            .setFile(savedFile)
+            .setFileSizeLimit(0)
+            .build()
+
+        assertThat(fileOutputOptions).isNotNull()
+        assertThat(fileOutputOptions.type).isEqualTo(OutputOptions.Type.FILE)
+        assertThat(fileOutputOptions.file).isNotNull()
+        assertThat(fileOutputOptions.fileSizeLimit).isEqualTo(0)
+        savedFile.delete()
+    }
+
+    @Test
+    fun canBuildMediaStoreOutputOptions() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        val contentResolver: ContentResolver = context.contentResolver
+        val fileName = "OutputOptionTest"
+        val contentValues = ContentValues().apply {
+            put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+            put(MediaStore.Video.Media.TITLE, fileName)
+            put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
+        }
+
+        val uri = contentResolver.insert(
+            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+            contentValues
+        )
+
+        assertThat(uri).isNotNull()
+
+        val mediaStoreOutputOptions = MediaStoreOutputOptions.builder()
+            .setContentResolver(contentResolver)
+            .setFileSizeLimit(0)
+            .setUri(uri!!)
+            .build()
+
+        assertThat(mediaStoreOutputOptions.uri).isNotNull()
+        assertThat(mediaStoreOutputOptions.type).isEqualTo(OutputOptions.Type.MEDIA_STORE)
+        assertThat(mediaStoreOutputOptions.fileSizeLimit).isEqualTo(0)
+        contentResolver.delete(uri, null, null)
+    }
+
+    @Test
+    fun canBuildFileDescriptorOutputOptions() {
+        val savedFile = File.createTempFile("CameraX", ".tmp")
+        savedFile.deleteOnExit()
+        val pfd: ParcelFileDescriptor = ParcelFileDescriptor.open(
+            savedFile,
+            ParcelFileDescriptor.MODE_READ_WRITE
+        )
+        val fd: FileDescriptor = pfd.fileDescriptor
+
+        val fileOutputOptions = FileDescriptorOutputOptions.builder()
+            .setFileDescriptor(fd)
+            .setFileSizeLimit(0)
+            .build()
+
+        assertThat(fileOutputOptions).isNotNull()
+        assertThat(fileOutputOptions.type).isEqualTo(OutputOptions.Type.FILE_DESCRIPTOR)
+        assertThat(fileOutputOptions.fileDescriptor).isNotNull()
+        assertThat(fileOutputOptions.fileSizeLimit).isEqualTo(0)
+        pfd.close()
+        savedFile.delete()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java
new file mode 100644
index 0000000..99663f9
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.io.FileDescriptor;
+
+/**
+ * A class to store the result to a given file descriptor.
+ *
+ * <p> The file descriptor must be seekable and writable. And the caller should be responsible for
+ * closing the file descriptor.
+ */
+@AutoValue
+public abstract class FileDescriptorOutputOptions extends OutputOptions {
+
+    FileDescriptorOutputOptions() {
+        super(Type.FILE_DESCRIPTOR);
+    }
+
+    /** Returns a builder for this FileDescriptorOutputOptions. */
+    @NonNull
+    public static Builder builder() {
+        return new AutoValue_FileDescriptorOutputOptions.Builder();
+    }
+
+    /**
+     * Gets the limit for the file length in bytes.
+     */
+    @Override
+    public abstract int getFileSizeLimit();
+
+    /** Gets the FileDescriptor instance */
+    @NonNull
+    public abstract FileDescriptor getFileDescriptor();
+
+    /** The builder of the {@link FileDescriptorOutputOptions}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        Builder() {
+        }
+
+        /** Defines how to store the result. */
+        @NonNull
+        public abstract Builder setFileDescriptor(
+                @NonNull FileDescriptor fileDescriptor);
+
+        /** Sets the limit for the file length in bytes. */
+        @NonNull
+        public abstract Builder setFileSizeLimit(int bytes);
+
+        /** Builds the FileDescriptorOutputOptions instance. */
+        @NonNull
+        public abstract FileDescriptorOutputOptions build();
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java
new file mode 100644
index 0000000..5c208ba
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.io.File;
+
+/**
+ * A class to store the result to a given file.
+ *
+ * <p> The file could be in a path where the application has permission to write in.
+ */
+@AutoValue
+public abstract class FileOutputOptions extends OutputOptions {
+
+    FileOutputOptions() {
+        super(Type.FILE);
+    }
+
+    /** Returns a builder for this FileOutputOptions. */
+    @NonNull
+    public static Builder builder() {
+        return new AutoValue_FileOutputOptions.Builder();
+    }
+
+    /**
+     * Gets the limit for the file length in bytes.
+     */
+    @Override
+    public abstract int getFileSizeLimit();
+
+    /** Gets the File instance */
+    @NonNull
+    public abstract File getFile();
+
+    /** The builder of the {@link FileOutputOptions}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        Builder() {
+        }
+
+        /** Defines how to store the result. */
+        @NonNull
+        public abstract Builder setFile(@NonNull File file);
+
+        /** Sets the limit for the file length in bytes. */
+        @NonNull
+        public abstract Builder setFileSizeLimit(int bytes);
+
+        /** Builds the FileOutputOptions instance. */
+        @NonNull
+        public abstract FileOutputOptions build();
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java
new file mode 100644
index 0000000..fb08bdb
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video;
+
+import static androidx.camera.video.OutputOptions.Type.MEDIA_STORE;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * A class provides a option for storing output to MediaStore.
+ *
+ * <p> The result could be saved to a shared storage. The results will remain on the device after
+ * the app is uninstalled.
+ */
+@AutoValue
+public abstract class MediaStoreOutputOptions extends OutputOptions {
+
+    MediaStoreOutputOptions() {
+        super(MEDIA_STORE);
+    }
+
+    /** Returns a builder for this MediaStoreOutputOptions. */
+    @NonNull
+    public static Builder builder() {
+        return new AutoValue_MediaStoreOutputOptions.Builder();
+    }
+
+    /**
+     * Gets the ContentResolver instance in order to convert Uri to a file path.
+     */
+    @NonNull
+    public abstract ContentResolver getContentResolver();
+
+    /**
+     * Gets the limit for the file length in bytes.
+     */
+    @Override
+    public abstract int getFileSizeLimit();
+
+    /** Gets the Uri instance */
+    @NonNull
+    public abstract Uri getUri();
+
+    /** The builder of the {@link MediaStoreOutputOptions}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        Builder() {
+        }
+
+        /** Sets the ContentResolver instance. */
+        @NonNull
+        public abstract Builder setContentResolver(@NonNull ContentResolver contentResolver);
+
+        /** Defines how to store the result. */
+        @NonNull
+        public abstract Builder setUri(@NonNull Uri uri);
+
+        /** Sets the limit for the file length in bytes. */
+        @NonNull
+        public abstract Builder setFileSizeLimit(int bytes);
+
+        /** Builds the MediaStoreOutputOptions instance. */
+        @NonNull
+        public abstract MediaStoreOutputOptions build();
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
new file mode 100644
index 0000000..62e3dee
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Options for configuring output destination.
+ */
+public abstract class OutputOptions {
+
+    Type mType;
+
+    public OutputOptions(@NonNull Type type) {
+        mType = type;
+    }
+
+    /**
+     * To be used to cast OutputOptions to subtype.
+     */
+    Type getType() {
+        return mType;
+    }
+
+    /**
+     * Gets the limit for the file length in bytes.
+     */
+    public abstract int getFileSizeLimit();
+
+    /**
+     * Types of the output options.
+     */
+    enum Type {
+        FILE, FILE_DESCRIPTOR, MEDIA_STORE
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
index 9e69832..20e5387 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
@@ -31,7 +31,7 @@
      *
      * <p>For example, "video/avc" for a video encoder and "audio/mp4a-latm" for an audio encoder.
      *
-     * @See {@link MediaFormat}
+     * @see {@link MediaFormat}
      */
     @NonNull
     String getMimeType();
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/OutputUtil.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/OutputUtil.java
new file mode 100644
index 0000000..fa975f8
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/OutputUtil.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.utils;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+
+/**
+ * Utility class for output related operations.
+ */
+public final class OutputUtil {
+    private static final String TAG = "OutputUtil";
+
+    private OutputUtil(){}
+
+    /** Gets the absolute path from a Uri. */
+    @Nullable
+    public static String getAbsolutePathFromUri(@NonNull ContentResolver resolver,
+            @NonNull Uri contentUri, @NonNull String mediaStoreColumn) {
+        Cursor cursor = null;
+        try {
+            String[] proj;
+            int columnIndex;
+            proj = new String[]{mediaStoreColumn};
+            cursor = resolver.query(contentUri, proj, null, null, null);
+
+            if (cursor == null) {
+                return null;
+            }
+
+            columnIndex = cursor.getColumnIndexOrThrow(mediaStoreColumn);
+            cursor.moveToFirst();
+            return cursor.getString(columnIndex);
+        } catch (RuntimeException e) {
+            Logger.e(TAG, String.format(
+                    "Failed in getting absolute path for Uri %s with Exception %s",
+                    contentUri.toString(), e.toString()));
+            return null;
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+}
diff --git a/camera/camera-view/api/public_plus_experimental_current.txt b/camera/camera-view/api/public_plus_experimental_current.txt
index be9d4ae..6dbb420 100644
--- a/camera/camera-view/api/public_plus_experimental_current.txt
+++ b/camera/camera-view/api/public_plus_experimental_current.txt
@@ -127,7 +127,7 @@
 
 package androidx.camera.view.video {
 
-  @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalVideo {
+  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalVideo {
   }
 
   @androidx.camera.view.video.ExperimentalVideo @com.google.auto.value.AutoValue public abstract class Metadata {
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index 1ad6270..aef81d0 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
-import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryVersions
+
 import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
 import androidx.build.Publish
 
+import static androidx.build.dependencies.DependenciesKt.*
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -28,15 +30,19 @@
 apply(from: "dependencies.gradle")
 
 dependencies {
-    implementation("androidx.appcompat:appcompat:1.1.0")
     api("androidx.lifecycle:lifecycle-common:2.0.0")
+    api("androidx.annotation:annotation:1.0.0")
     api("androidx.camera:camera-core:${VIEW_ATOMIC_GROUP_PINNED_VER}")
     implementation("androidx.camera:camera-lifecycle:${VIEW_ATOMIC_GROUP_PINNED_VER}")
-    api("androidx.annotation:annotation:1.0.0")
+    implementation("androidx.annotation:annotation-experimental:1.1.0-rc01")
     implementation(GUAVA_LISTENABLE_FUTURE)
     implementation("androidx.core:core:1.1.0")
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
     implementation(AUTO_VALUE_ANNOTATIONS)
+    implementation("androidx.appcompat:appcompat:1.1.0")
+    // Added for annotation-experimental
+    compileOnly(KOTLIN_STDLIB)
+
     annotationProcessor(AUTO_VALUE)
 
     testImplementation(ANDROIDX_TEST_RUNNER)
diff --git a/camera/camera-view/dependencies.gradle b/camera/camera-view/dependencies.gradle
index 5795377..ce7090a 100644
--- a/camera/camera-view/dependencies.gradle
+++ b/camera/camera-view/dependencies.gradle
@@ -16,5 +16,5 @@
 
 ext {
     // camera-view temporarily pins same-group depenencies to RC/stable until beta
-    VIEW_ATOMIC_GROUP_PINNED_VER = "1.0.0-rc03"
+    VIEW_ATOMIC_GROUP_PINNED_VER = "1.0.0-rc04"
 }
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
index ba1c76f..9bc5023 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
@@ -113,7 +113,7 @@
             SURFACE_SIZE,
             BACK_CAMERA
         )
-        return mPreviewTransform.isCropRectAspectRatioMatchPreviewView(PREVIEW_VIEW_SIZE)
+        return mPreviewTransform.isViewportAspectRatioMatchPreviewView(PREVIEW_VIEW_SIZE)
     }
 
     @Test
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index a5f703d..87a66489 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -28,9 +28,9 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraInfo;
@@ -113,7 +113,7 @@
      *
      * @hide
      */
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @IntDef(flag = true, value = {IMAGE_CAPTURE, IMAGE_ANALYSIS, VIDEO_CAPTURE})
@@ -330,7 +330,7 @@
      * @see ImageAnalysis
      */
     @MainThread
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     public void setEnabledUseCases(@UseCases int enabledUseCases) {
         Threads.checkMainThread();
         if (enabledUseCases == mEnabledUseCases) {
@@ -359,9 +359,9 @@
      * Same as {@link #isVideoCaptureEnabled()}.
      *
      * <p> This wrapper method is to workaround the limitation that currently only one
-     * {@link UseExperimental} mark class is allowed per method.
+     * UseExperimental mark class is allowed per method.
      */
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     private boolean isVideoCaptureEnabledInternal() {
         return isVideoCaptureEnabled();
     }
@@ -375,7 +375,7 @@
      */
     @SuppressLint({"MissingPermission", "WrongConstant"})
     @MainThread
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     void attachPreviewSurface(@NonNull Preview.SurfaceProvider surfaceProvider,
             @NonNull ViewPort viewPort, @NonNull Display display) {
         Threads.checkMainThread();
@@ -1125,7 +1125,7 @@
      */
     @Nullable
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     protected UseCaseGroup createUseCaseGroup() {
         if (!isCameraInitialized()) {
             Logger.d(TAG, CAMERA_NOT_INITIALIZED);
@@ -1182,7 +1182,7 @@
 
         @SuppressLint("WrongConstant")
         @Override
-        @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+        @OptIn(markerClass = ExperimentalUseCaseGroup.class)
         public void onDisplayChanged(int displayId) {
             if (mPreviewDisplay != null && mPreviewDisplay.getDisplayId() == displayId) {
                 mPreview.setTargetRotation(mPreviewDisplay.getRotation());
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraXModule.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraXModule.java
index 757c27ee..c1ad936 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraXModule.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraXModule.java
@@ -26,8 +26,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresPermission;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.AspectRatio;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraInfoUnavailableException;
@@ -162,7 +162,7 @@
         }
     }
 
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     @RequiresPermission(permission.CAMERA)
     void bindToLifecycleAfterViewMeasured() {
         if (mNewLifecycle == null) {
@@ -270,7 +270,7 @@
                 "Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
     }
 
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     public void takePicture(Executor executor, OnImageCapturedCallback callback) {
         if (mImageCapture == null) {
             return;
@@ -287,7 +287,7 @@
         mImageCapture.takePicture(executor, callback);
     }
 
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
             @NonNull Executor executor, OnImageSavedCallback callback) {
         if (mImageCapture == null) {
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
index e468fb2..b686559 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
@@ -26,9 +26,9 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.Camera;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.UseCaseGroup;
@@ -110,7 +110,7 @@
      *
      * @return null if failed to start camera.
      */
-    @UseExperimental(markerClass = ExperimentalUseCaseGroupLifecycle.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroupLifecycle.class)
     @RequiresPermission(Manifest.permission.CAMERA)
     @Override
     @Nullable
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index ce02f39..7944a06 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -48,14 +48,14 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.ExperimentalUseCaseGroup;
 import androidx.camera.core.Logger;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.ViewPort;
 import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.view.internal.compat.quirk.PreviewStretchedQuirk;
+import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
 import androidx.core.util.Preconditions;
 
 /**
@@ -104,8 +104,12 @@
 
     // SurfaceRequest.getResolution().
     private Size mResolution;
-    // TransformationInfo.getCropRect().
+    // This represents the area of the Surface that should be visible to end users. The value
+    // is based on TransformationInfo.getCropRect() with possible corrections due to device quirks.
     private Rect mSurfaceCropRect;
+    // This rect represents the size of the viewport in preview. It's always the same as
+    // TransformationInfo.getCropRect().
+    private Rect mViewportRect;
     // TransformationInfo.getRotationDegrees().
     private int mPreviewRotationDegrees;
     // TransformationInfo.getTargetRotation.
@@ -123,12 +127,13 @@
      *
      * <p> All the values originally come from a {@link SurfaceRequest}.
      */
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     void setTransformationInfo(@NonNull SurfaceRequest.TransformationInfo transformationInfo,
             Size resolution, boolean isFrontCamera) {
         Logger.d(TAG, "Transformation info set: " + transformationInfo + " " + resolution + " "
                 + isFrontCamera);
-        mSurfaceCropRect = transformationInfo.getCropRect();
+        mSurfaceCropRect = getCorrectedCropRect(transformationInfo.getCropRect());
+        mViewportRect = transformationInfo.getCropRect();
         mPreviewRotationDegrees = transformationInfo.getRotationDegrees();
         mTargetRotation = transformationInfo.getTargetRotation();
         mResolution = resolution;
@@ -237,7 +242,7 @@
 
         // Get the target of the mapping, the vertices of the crop rect in PreviewView.
         float[] previewViewCropRectVertices;
-        if (isCropRectAspectRatioMatchPreviewView(previewViewSize)) {
+        if (isViewportAspectRatioMatchPreviewView(previewViewSize)) {
             // If crop rect has the same aspect ratio as PreviewView, scale the crop rect to fill
             // the entire PreviewView. This happens if the scale type is FILL_* AND a
             // PreviewView-based viewport is used.
@@ -245,7 +250,7 @@
         } else {
             // If the aspect ratios don't match, it could be 1) scale type is FIT_*, 2) the
             // Viewport is not based on the PreviewView or 3) both.
-            RectF previewViewCropRect = getPreviewViewCropRectForMismatchedAspectRatios(
+            RectF previewViewCropRect = getPreviewViewViewportRectForMismatchedAspectRatios(
                     previewViewSize, layoutDirection);
             previewViewCropRectVertices = rectToVertices(previewViewCropRect);
         }
@@ -253,7 +258,7 @@
                 previewViewCropRectVertices, mPreviewRotationDegrees);
 
         // Get the source of the mapping, the vertices of the crop rect in Surface.
-        float[] surfaceCropRectVertices = getSurfaceCropRectVertices();
+        float[] surfaceCropRectVertices = rectToVertices(new RectF(mSurfaceCropRect));
 
         // Map source to target.
         matrix.setPolyToPoly(surfaceCropRectVertices, 0, rotatedPreviewViewCropRectVertices, 0, 4);
@@ -282,43 +287,46 @@
     /**
      * Gets the vertices of the crop rect in Surface.
      */
-    private float[] getSurfaceCropRectVertices() {
-        RectF cropRectF = new RectF(mSurfaceCropRect);
-        PreviewStretchedQuirk quirk = DeviceQuirks.get(PreviewStretchedQuirk.class);
+    private Rect getCorrectedCropRect(Rect surfaceCropRect) {
+        PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
         if (quirk != null) {
             // Correct crop rect if the device has a quirk.
+            RectF cropRectF = new RectF(surfaceCropRect);
             Matrix correction = new Matrix();
             correction.setScale(
                     quirk.getCropRectScaleX(),
-                    quirk.getCropRectScaleY(),
-                    mSurfaceCropRect.centerX(),
-                    mSurfaceCropRect.centerY());
+                    1f,
+                    surfaceCropRect.centerX(),
+                    surfaceCropRect.centerY());
             correction.mapRect(cropRectF);
+            Rect correctRect = new Rect();
+            cropRectF.round(correctRect);
+            return correctRect;
         }
-        return rectToVertices(cropRectF);
+        return surfaceCropRect;
     }
 
     /**
-     * Gets the crop rect in {@link PreviewView} coordinates for the case where crop rect's aspect
-     * ratio doesn't match {@link PreviewView}'s aspect ratio.
+     * Gets the viewport rect in {@link PreviewView} coordinates for the case where viewport's
+     * aspect ratio doesn't match {@link PreviewView}'s aspect ratio.
      *
      * <p> When aspect ratios don't match, additional calculation is needed to figure out how to
      * fit crop rect into the{@link PreviewView}.
      */
-    RectF getPreviewViewCropRectForMismatchedAspectRatios(Size previewViewSize,
+    RectF getPreviewViewViewportRectForMismatchedAspectRatios(Size previewViewSize,
             int layoutDirection) {
         RectF previewViewRect = new RectF(0, 0, previewViewSize.getWidth(),
                 previewViewSize.getHeight());
-        Size rotatedCropRectSize = getRotatedCropRectSize();
-        RectF rotatedSurfaceCropRect = new RectF(0, 0, rotatedCropRectSize.getWidth(),
-                rotatedCropRectSize.getHeight());
+        Size rotatedViewportSize = getRotatedViewportSize();
+        RectF rotatedViewportRect = new RectF(0, 0, rotatedViewportSize.getWidth(),
+                rotatedViewportSize.getHeight());
         Matrix matrix = new Matrix();
-        setMatrixRectToRect(matrix, rotatedSurfaceCropRect, previewViewRect, mScaleType);
-        matrix.mapRect(rotatedSurfaceCropRect);
+        setMatrixRectToRect(matrix, rotatedViewportRect, previewViewRect, mScaleType);
+        matrix.mapRect(rotatedViewportRect);
         if (layoutDirection == LayoutDirection.RTL) {
-            return flipHorizontally(rotatedSurfaceCropRect, (float) previewViewSize.getWidth() / 2);
+            return flipHorizontally(rotatedViewportRect, (float) previewViewSize.getWidth() / 2);
         }
-        return rotatedSurfaceCropRect;
+        return rotatedViewportRect;
     }
 
     /**
@@ -374,29 +382,29 @@
     }
 
     /**
-     * Returns crop rect size with target rotation applied.
+     * Returns viewport size with target rotation applied.
      */
-    private Size getRotatedCropRectSize() {
-        Preconditions.checkNotNull(mSurfaceCropRect);
+    private Size getRotatedViewportSize() {
         if (is90or270(mPreviewRotationDegrees)) {
-            return new Size(mSurfaceCropRect.height(), mSurfaceCropRect.width());
+            return new Size(mViewportRect.height(), mViewportRect.width());
         }
-        return new Size(mSurfaceCropRect.width(), mSurfaceCropRect.height());
+        return new Size(mViewportRect.width(), mViewportRect.height());
     }
 
     /**
-     * Checks if the crop rect's aspect ratio matches that of the {@link PreviewView}.
+     * Checks if the viewport's aspect ratio matches that of the {@link PreviewView}.
      *
      * <p> The mismatch could happen if the {@link ViewPort} is not based on the
      * {@link PreviewView}, or the {@link PreviewView#getScaleType()} is FIT_*. In this case, we
      * need to calculate how the crop rect should be fitted.
      */
     @VisibleForTesting
-    boolean isCropRectAspectRatioMatchPreviewView(Size previewViewSize) {
-        Size rotatedSize = getRotatedCropRectSize();
+    boolean isViewportAspectRatioMatchPreviewView(Size previewViewSize) {
+        // Using viewport rect to check if the viewport is based on the PreviewView.
+        Size rotatedViewportSize = getRotatedViewportSize();
         return isAspectRatioMatchingWithRoundingError(
                 previewViewSize, /* isAccurate1= */ true,
-                rotatedSize,  /* isAccurate2= */ false);
+                rotatedViewportSize,  /* isAccurate2= */ false);
     }
 
     /**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
index 3b44754..1983460 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
@@ -44,10 +44,10 @@
 import androidx.annotation.ColorRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
@@ -64,6 +64,8 @@
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.utils.Threads;
+import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.view.internal.compat.quirk.SurfaceViewStretchedQuirk;
 import androidx.camera.view.transform.CoordinateTransform;
 import androidx.camera.view.transform.OutputTransform;
 import androidx.core.content.ContextCompat;
@@ -154,7 +156,7 @@
     @SuppressWarnings("WeakerAccess")
     final Preview.SurfaceProvider mSurfaceProvider = new Preview.SurfaceProvider() {
 
-        @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+        @OptIn(markerClass = ExperimentalUseCaseGroup.class)
         @Override
         @AnyThread
         public void onSurfaceRequested(@NonNull SurfaceRequest surfaceRequest) {
@@ -362,7 +364,7 @@
      */
     @UiThread
     @NonNull
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     public Preview.SurfaceProvider getSurfaceProvider() {
         Threads.checkMainThread();
         return mSurfaceProvider;
@@ -594,14 +596,16 @@
 
     // Synthetic access
     @SuppressWarnings("WeakerAccess")
-    boolean shouldUseTextureView(@NonNull SurfaceRequest surfaceRequest,
+    static boolean shouldUseTextureView(@NonNull SurfaceRequest surfaceRequest,
             @NonNull final ImplementationMode implementationMode) {
         // TODO(b/159127402): use TextureView if target rotation is not display rotation.
         boolean isLegacyDevice = surfaceRequest.getCamera().getCameraInfoInternal()
                 .getImplementationType().equals(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
-        if (surfaceRequest.isRGBA8888Required() || Build.VERSION.SDK_INT <= 24 || isLegacyDevice) {
+        boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null;
+        if (surfaceRequest.isRGBA8888Required() || Build.VERSION.SDK_INT <= 24 || isLegacyDevice
+                || hasSurfaceViewQuirk) {
             // Force to use TextureView when the device is running android 7.0 and below, legacy
-            // level or RGBA8888 is required.
+            // level, RGBA8888 is required or SurfaceView has quirks.
             return true;
         }
         switch (implementationMode) {
@@ -884,7 +888,7 @@
                 surfaceCropRect.height()));
     }
 
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     private void attachToControllerIfReady(boolean shouldFailSilently) {
         Display display = getDisplay();
         ViewPort viewPort = getViewPort();
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/TransformExperimental.java b/camera/camera-view/src/main/java/androidx/camera/view/TransformExperimental.java
index 06ec9ef..858827d 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/TransformExperimental.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/TransformExperimental.java
@@ -18,6 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
+import androidx.annotation.RequiresOptIn;
 import androidx.annotation.RestrictTo;
 
 import java.lang.annotation.Retention;
@@ -34,6 +35,6 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @Retention(CLASS)
[email protected]
+@RequiresOptIn
 public @interface TransformExperimental {
 }
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
index 65e35b3..d73a851 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
@@ -39,8 +39,12 @@
         final List<Quirk> quirks = new ArrayList<>();
 
         // Load all device specific quirks
-        if (PreviewStretchedQuirk.load()) {
-            quirks.add(new PreviewStretchedQuirk());
+        if (PreviewOneThirdWiderQuirk.load()) {
+            quirks.add(new PreviewOneThirdWiderQuirk());
+        }
+
+        if (SurfaceViewStretchedQuirk.load()) {
+            quirks.add(new SurfaceViewStretchedQuirk());
         }
 
         return quirks;
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
new file mode 100644
index 0000000..316e4db
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * A quirk where the preview buffer is stretched.
+ *
+ * <p> The symptom is, the preview's FOV is always 1/3 wider than intended. For example, if the
+ * preview Surface is 800x600, it's actually has a FOV of 1066x600 with the same center point,
+ * but squeezed to fit the 800x600 buffer.
+ */
+public class PreviewOneThirdWiderQuirk implements Quirk {
+
+    private static final String SAMSUNG_A3_2017 = "A3Y17LTE"; // b/180121821
+    private static final String SAMSUNG_J5_PRIME = "ON5XELTE"; // b/183329599
+
+    static boolean load() {
+        boolean isSamsungJ5PrimeAndApi26 =
+                SAMSUNG_J5_PRIME.equals(Build.DEVICE.toUpperCase()) && Build.VERSION.SDK_INT >= 26;
+        boolean isSamsungA3 = SAMSUNG_A3_2017.equals(Build.DEVICE.toUpperCase());
+        return isSamsungJ5PrimeAndApi26 || isSamsungA3;
+    }
+
+    /**
+     * The mount that the crop rect needs to be scaled in x.
+     */
+    public float getCropRectScaleX() {
+        return 0.75f;
+    }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
deleted file mode 100644
index fd274fa..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.internal.compat.quirk;
-
-import android.os.Build;
-
-import androidx.camera.core.impl.Quirk;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * A quirk where the preview buffer is stretched.
- *
- * <p> This is similar to the SamsungPreviewTargetAspectRatioQuirk in camera-camera2 artifact.
- * The difference is, the other quirk can be fixed by choosing a different resolution,
- * while for this one the preview is always stretched no matter what resolution is selected.
- */
-public class PreviewStretchedQuirk implements Quirk {
-
-    private static final String SAMSUNG_A3_2017 = "A3Y17LTE"; // b/180121821
-
-    private static final List<String> KNOWN_AFFECTED_DEVICES = Arrays.asList(SAMSUNG_A3_2017);
-
-    static boolean load() {
-        return KNOWN_AFFECTED_DEVICES.contains(Build.DEVICE.toUpperCase());
-    }
-
-    /**
-     * The mount that the crop rect needs to be scaled in x.
-     */
-    public float getCropRectScaleX() {
-        if (SAMSUNG_A3_2017.equals(Build.DEVICE.toUpperCase())) {
-            // For Samsung A3 2017, the symptom seems to be that the preview's FOV is always 1/3
-            // wider than it's supposed to be. For example, if the preview Surface is 800x600, it's
-            // actually has a FOV of 1066x600, but stretched to fit the 800x600 buffer. To correct
-            // the preview, we need to crop out the extra 25% FOV.
-            return 0.75f;
-        }
-        // No scale.
-        return 1;
-    }
-
-    /**
-     * The mount that the crop rect needs to be scaled in y.
-     */
-    public float getCropRectScaleY() {
-        // No scale.
-        return 1;
-    }
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirk.java
new file mode 100644
index 0000000..46b49f5
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirk.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * A quirk where SurfaceView is stretched.
+ *
+ * <p> On Samsung Galaxy Z Fold2, transform APIs (e.g. View#setScaleX) do not work as intended.
+ * b/129403806
+ */
+public class SurfaceViewStretchedQuirk implements Quirk {
+
+    // Samsung Galaxy Z Fold2 b/129403806
+    private static final String SAMSUNG = "SAMSUNG";
+    private static final String GALAXY_Z_FOLD_2 = "F2Q";
+
+    static boolean load() {
+        return SAMSUNG.equals(Build.MANUFACTURER.toUpperCase()) && GALAXY_Z_FOLD_2.equals(
+                Build.DEVICE.toUpperCase());
+    }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/ExperimentalVideo.java b/camera/camera-view/src/main/java/androidx/camera/view/video/ExperimentalVideo.java
index d564532..6e4a804 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/ExperimentalVideo.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/video/ExperimentalVideo.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
 
 import java.lang.annotation.Retention;
 
@@ -30,6 +30,6 @@
  * releases, or may be removed altogether.
  */
 @Retention(CLASS)
-@Experimental
+@RequiresOptIn
 public @interface ExperimentalVideo {
 }
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java
new file mode 100644
index 0000000..803a3a3
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Size;
+
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
+import androidx.camera.view.internal.compat.quirk.QuirkInjector;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/**
+ * Unit tests for {@link PreviewTransformation}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class PreviewTransformationTest {
+
+    private static final Rect CROP_RECT = new Rect(0, 0, 600, 400);
+
+    private final PreviewTransformation mPreviewTransformation = new PreviewTransformation();
+
+    @Test
+    public void withPreviewStretchedQuirk_cropRectIsAdjusted() {
+        // Arrange.
+        QuirkInjector.inject(new PreviewOneThirdWiderQuirk());
+
+        // Act.
+        mPreviewTransformation.setTransformationInfo(
+                SurfaceRequest.TransformationInfo.of(CROP_RECT, 0, 0),
+                new Size(CROP_RECT.width(), CROP_RECT.height()),
+                /*isFrontCamera*/ false);
+
+        // Assert: the crop rect is corrected.
+        assertThat(mPreviewTransformation.getSurfaceCropRect()).isEqualTo(new Rect(75, 0, 525,
+                400));
+    }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
new file mode 100644
index 0000000..82c2e25
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.util.Size;
+
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.fakes.FakeCameraInfoInternal;
+import androidx.camera.view.internal.compat.quirk.QuirkInjector;
+import androidx.camera.view.internal.compat.quirk.SurfaceViewStretchedQuirk;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class PreviewViewTest {
+
+    @After
+    public void tearDown() {
+        QuirkInjector.clear();
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.N_MR1)
+    public void surfaceViewNormal_useSurfaceView() {
+        // Assert: SurfaceView is used.
+        assertThat(PreviewView.shouldUseTextureView(
+                createSurfaceRequestCompatibleWithSurfaceView(),
+                PreviewView.ImplementationMode.PERFORMANCE)).isFalse();
+    }
+
+    @Test
+    public void surfaceViewHasQuirk_useTextureView() {
+        // Arrange:
+        QuirkInjector.inject(new SurfaceViewStretchedQuirk());
+
+        // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
+        assertThat(PreviewView.shouldUseTextureView(
+                createSurfaceRequestCompatibleWithSurfaceView(),
+                PreviewView.ImplementationMode.PERFORMANCE)).isTrue();
+    }
+
+    private SurfaceRequest createSurfaceRequestCompatibleWithSurfaceView() {
+        FakeCameraInfoInternal cameraInfoInternal = new FakeCameraInfoInternal();
+        cameraInfoInternal.setImplementationType(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
+        return new SurfaceRequest(new Size(800, 600),
+                new FakeCamera(null, cameraInfoInternal),
+                /*isRGB8888Required*/ false);
+    }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java
new file mode 100644
index 0000000..d9f5964
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.List;
+
+/**
+ * Tests version of main/.../DeviceQuirks.java, which provides device specific quirks, used for
+ * device specific workarounds.
+ * <p>
+ * In main/.../DeviceQuirks, Device quirks are loaded the first time a device workaround is
+ * encountered, and remain in memory until the process is killed. When running tests, this means
+ * that the same device quirks are used for all the tests. This causes an issue when tests modify
+ * device properties (using Robolectric for instance). Instead of force-reloading the device
+ * quirks in every test that uses a device workaround, this class internally reloads the quirks
+ * every time a device workaround is needed.
+ */
+public class DeviceQuirks {
+
+    private DeviceQuirks() {
+    }
+
+    /**
+     * Retrieves a specific device {@link Quirk} instance given its type.
+     *
+     * @param quirkClass The type of device quirk to retrieve.
+     * @return A device {@link Quirk} instance of the provided type, or {@code null} if it isn't
+     * found.
+     */
+    @SuppressWarnings("unchecked")
+    @Nullable
+    public static <T extends Quirk> T get(@NonNull final Class<T> quirkClass) {
+        final List<Quirk> quirks = DeviceQuirksLoader.loadQuirks();
+        quirks.addAll(QuirkInjector.INJECTED_QUIRKS);
+        for (final Quirk quirk : quirks) {
+            if (quirk.getClass() == quirkClass) {
+                return (T) quirk;
+            }
+        }
+        return null;
+    }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
similarity index 61%
copy from camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
copy to camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
index 0b42944..17c02f5 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
@@ -28,24 +28,36 @@
 import org.robolectric.util.ReflectionHelpers;
 
 /**
- * Unit tests for {@link PreviewStretchedQuirk}.
+ * Unit tests for {@link PreviewOneThirdWiderQuirk}.
  */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class PreviewStretchedQuirkTest {
+public class PreviewOneThirdWiderQuirkTest {
 
     @Test
     public void quirkExistsOnSamsungA3() {
-        // Arrange.
         ReflectionHelpers.setStaticField(Build.class, "DEVICE", "A3Y17LTE");
+        assertPreviewShouldBeCroppedBy25Percent();
+    }
 
-        // Act.
-        final PreviewStretchedQuirk quirk = DeviceQuirks.get(PreviewStretchedQuirk.class);
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.O)
+    public void quirkExistsOnSamsungJ5PrimeApi26AndAbove() {
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
+        assertPreviewShouldBeCroppedBy25Percent();
+    }
 
-        // Assert.
+    @Test
+    @Config(maxSdk = Build.VERSION_CODES.N_MR1)
+    public void quirkDoesNotExistOnSamsungJ5PrimeApi25AndBelow() {
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
+        assertThat(DeviceQuirks.get(PreviewOneThirdWiderQuirk.class)).isNull();
+    }
+
+    private void assertPreviewShouldBeCroppedBy25Percent() {
+        final PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
         assertThat(quirk).isNotNull();
         assertThat(quirk.getCropRectScaleX()).isEqualTo(0.75F);
-        assertThat(quirk.getCropRectScaleY()).isEqualTo(1F);
     }
 }
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/QuirkInjector.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/QuirkInjector.java
new file mode 100644
index 0000000..d7568c3
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/QuirkInjector.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Inject quirks for unit tests.
+ *
+ * <p> Used with the test version of {@link DeviceQuirks} to test the behavior of quirks.
+ */
+public class QuirkInjector {
+
+    static final List<Quirk> INJECTED_QUIRKS = new ArrayList<>();
+
+    /**
+     * Inject a quirk. The injected quirk will be loaded by {@link DeviceQuirks}.
+     */
+    public static void inject(@NonNull Quirk quirk) {
+        INJECTED_QUIRKS.add(quirk);
+    }
+
+    /**
+     * Clears all injected quirks.
+     */
+    public static void clear() {
+        INJECTED_QUIRKS.clear();
+    }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
similarity index 78%
rename from camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
rename to camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
index 0b42944..965357e 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
@@ -28,24 +28,23 @@
 import org.robolectric.util.ReflectionHelpers;
 
 /**
- * Unit tests for {@link PreviewStretchedQuirk}.
+ * Unit test for {@link SurfaceViewStretchedQuirk}.
  */
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class PreviewStretchedQuirkTest {
+public class SurfaceViewStretchedQuirkTest {
 
     @Test
-    public void quirkExistsOnSamsungA3() {
+    public void quirkExistsOnSamsungGalaxyZFold2() {
         // Arrange.
-        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "A3Y17LTE");
+        ReflectionHelpers.setStaticField(Build.class, "DEVICE", "F2Q");
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "SAMSUNG");
 
         // Act.
-        final PreviewStretchedQuirk quirk = DeviceQuirks.get(PreviewStretchedQuirk.class);
+        final SurfaceViewStretchedQuirk quirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class);
 
         // Assert.
         assertThat(quirk).isNotNull();
-        assertThat(quirk.getCropRectScaleX()).isEqualTo(0.75F);
-        assertThat(quirk.getCropRectScaleY()).isEqualTo(1F);
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
index 382ec89..79ad758 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
@@ -18,6 +18,7 @@
 import android.Manifest
 import android.app.Instrumentation
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.lifecycle.Lifecycle.State.CREATED
@@ -35,12 +36,14 @@
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import org.junit.After
+import org.junit.AfterClass
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TestRule
 import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
 
 private const val HOME_TIMEOUT_MS = 3000L
 private const val ROTATE_TIMEOUT_MS = 2000L
@@ -62,6 +65,14 @@
             Manifest.permission.RECORD_AUDIO
         )
 
+    companion object {
+        @AfterClass
+        @JvmStatic
+        fun shutdownCameraX() {
+            CameraX.shutdown().get(10, TimeUnit.SECONDS)
+        }
+    }
+
     @Before
     fun setup() {
         Assume.assumeTrue(CameraUtil.deviceHasCamera())
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/InitializationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/InitializationTest.kt
index 5887576..42dce52 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/InitializationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/InitializationTest.kt
@@ -17,6 +17,7 @@
 package androidx.camera.integration.core
 
 import android.content.Context
+import androidx.camera.core.CameraX
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.concurrent.futures.await
@@ -32,6 +33,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
 import org.junit.After
+import org.junit.AfterClass
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
@@ -39,6 +41,7 @@
 import org.junit.rules.TestRule
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit
 
 @LargeTest
 @RunWith(Parameterized::class)
@@ -70,6 +73,12 @@
                     TestConfig(orientation)
                 }
         }
+
+        @AfterClass
+        @JvmStatic
+        fun shutdownCameraX() {
+            CameraX.shutdown().get(10, TimeUnit.SECONDS)
+        }
     }
 
     private var providerResult: CameraXViewModel.CameraProviderResult? = null
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java
index 050cd6f..b1b8a1b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 import android.content.Intent;
 
+import androidx.camera.core.CameraX;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.CoreAppTestUtil;
 import androidx.test.core.app.ApplicationProvider;
@@ -38,12 +39,17 @@
 import androidx.test.uiautomator.UiDevice;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
 // Test new activity lifecycle when using CameraX.
 @RunWith(AndroidJUnit4.class)
 @LargeTest
@@ -96,6 +102,12 @@
         pressHomeButton();
     }
 
+    @AfterClass
+    public static void shutdownCameraX()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        CameraX.shutdown().get(10, TimeUnit.SECONDS);
+    }
+
     @Test
     public void checkPreviewUpdatedWithNewInstance() {
 
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
index 71cdc95..1c1704e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
@@ -17,6 +17,7 @@
 package androidx.camera.integration.core
 
 import android.Manifest
+import androidx.camera.core.CameraX
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.test.core.app.ActivityScenario
@@ -27,12 +28,14 @@
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.rule.GrantPermissionRule
+import org.junit.AfterClass
 import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TestRule
 import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -47,6 +50,14 @@
             Manifest.permission.RECORD_AUDIO
         )
 
+    companion object {
+        @AfterClass
+        @JvmStatic
+        fun tearDown() {
+            CameraX.shutdown().get(10, TimeUnit.SECONDS)
+        }
+    }
+
     @Before
     fun setUp() {
         assumeTrue(CameraUtil.deviceHasCamera())
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
index 2551bb3..70eccd1 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
@@ -34,6 +34,7 @@
 
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraX;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.TorchState;
 import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
@@ -54,12 +55,17 @@
 import junit.framework.AssertionFailedError;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
 /** Test toggle buttons in CoreTestApp. */
 @RunWith(AndroidJUnit4.class)
 @LargeTest
@@ -118,6 +124,12 @@
         mDevice.waitForIdle(IDLE_TIMEOUT_MS);
     }
 
+    @AfterClass
+    public static void shutdownCameraX()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        CameraX.shutdown().get(10, TimeUnit.SECONDS);
+    }
+
     @Test
     public void testFlashToggleButton() {
         waitFor(new WaitForViewToShow(R.id.constraintLayout));
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
new file mode 100644
index 0000000..9cdff8a
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.core
+
+import android.content.Context
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit
+
+private val DEFAULT_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
+
+/** Contains tests for [CameraX] which varies use case combinations to run. */
+@LargeTest
+@RunWith(Parameterized::class)
+class UseCaseCombinationTest(
+    private val implName: String,
+    private val cameraConfig: CameraXConfig
+) {
+
+    @get:Rule
+    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+            arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+        )
+    }
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Before
+    fun initializeCameraX(): Unit = runBlocking {
+        CameraX.initialize(context, cameraConfig).get(10, TimeUnit.SECONDS)
+    }
+
+    @After
+    fun shutdownCameraX(): Unit = runBlocking {
+        CameraX.shutdown().get(10, TimeUnit.SECONDS)
+    }
+
+    /** Test Combination: Preview + ImageCapture */
+    @Test
+    fun previewCombinesImageCapture() = runBlocking {
+        val preview = initPreview()
+        val imageCapture = initImageCapture()
+
+        val camera = CameraUtil.createCameraUseCaseAdapter(context, DEFAULT_SELECTOR)
+        camera.detachUseCases()
+
+        // TODO(b/160249108) move off of main thread once UseCases can be attached on any thread
+        withContext(Dispatchers.Main) {
+            camera.addUseCases(listOf(preview, imageCapture))
+        }
+    }
+
+    /** Test Combination: Preview + ImageAnalysis */
+    @Test
+    fun previewCombinesImageAnalysis() = runBlocking {
+        val preview = initPreview()
+        val imageAnalysis = initImageAnalysis()
+
+        val camera = CameraUtil.createCameraUseCaseAdapter(context, DEFAULT_SELECTOR)
+        camera.detachUseCases()
+
+        // TODO(b/160249108) move off of main thread once UseCases can be attached on any thread
+        withContext(Dispatchers.Main) {
+            camera.addUseCases(listOf(preview, imageAnalysis))
+        }
+    }
+
+    /** Test Combination: Preview + ImageAnalysis + ImageCapture  */
+    @Test
+    fun previewCombinesImageAnalysisAndImageCapture() = runBlocking {
+        val preview = initPreview()
+        val imageAnalysis = initImageAnalysis()
+        val imageCapture = initImageCapture()
+
+        val camera = CameraUtil.createCameraUseCaseAdapter(context, DEFAULT_SELECTOR)
+        camera.detachUseCases()
+
+        // TODO(b/160249108) move off of main thread once UseCases can be attached on any
+        //  thread
+        withContext(Dispatchers.Main) {
+            camera.addUseCases(listOf(preview, imageAnalysis, imageCapture))
+        }
+    }
+
+    private fun initPreview(): Preview {
+        return Preview.Builder()
+            .setTargetName("Preview")
+            .build()
+    }
+
+    private fun initImageAnalysis(): ImageAnalysis {
+        return ImageAnalysis.Builder()
+            .setTargetName("ImageAnalysis")
+            .build()
+    }
+
+    private fun initImageCapture(): ImageCapture {
+        return ImageCapture.Builder().build()
+    }
+}
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 63937a2..2495047 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
@@ -56,6 +56,7 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.experimental.UseExperimental;
 import androidx.appcompat.app.AppCompatActivity;
@@ -190,7 +191,7 @@
     private FutureCallback<Integer> mEVFutureCallback = new FutureCallback<Integer>() {
 
         @Override
-        @UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+        @OptIn(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
         public void onSuccess(@Nullable Integer result) {
             CameraInfo cameraInfo = getCameraInfo();
             if (cameraInfo != null) {
@@ -305,7 +306,7 @@
         return mPhotoToggle.isChecked() && cameraInfo != null && cameraInfo.hasFlashUnit();
     }
 
-    @UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+    @OptIn(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
     private boolean isExposureCompensationSupported() {
         CameraInfo cameraInfo = getCameraInfo();
         return cameraInfo != null
@@ -445,7 +446,7 @@
         });
     }
 
-    @UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+    @OptIn(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
     private void setUpEVButton() {
         mPlusEV.setOnClickListener(v -> {
             Objects.requireNonNull(getCameraInfo());
@@ -824,7 +825,7 @@
      * Workaround method for an AndroidX issue where {@link UseExperimental} doesn't support 2 or
      * more annotations.
      */
-    @UseExperimental(markerClass = ExperimentalUseCaseGroupLifecycle.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroupLifecycle.class)
     private Camera bindToLifecycleSafely(List<UseCase> useCases) {
         Log.e(TAG, "Binding use cases " + useCases);
         return bindToLifecycleSafelyWithExperimental(useCases);
@@ -833,7 +834,7 @@
     /**
      * Binds use cases to the current lifecycle.
      */
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     @ExperimentalUseCaseGroupLifecycle
     private Camera bindToLifecycleSafelyWithExperimental(List<UseCase> useCases) {
         ViewPort viewPort = new ViewPort.Builder(new Rational(mViewFinder.getWidth(),
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
index 1c7deb2..af83fc9 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
@@ -22,7 +22,7 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.Camera2Config;
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
 import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration;
@@ -91,7 +91,7 @@
         return mProcessCameraProviderLiveData;
     }
 
-    @UseExperimental(markerClass = ExperimentalCameraProviderConfiguration.class)
+    @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
     @MainThread
     private static void tryConfigureCameraProvider() {
         if (sConfiguredCameraXCameraImplementation == null) {
@@ -99,7 +99,7 @@
         }
     }
 
-    @UseExperimental(markerClass = ExperimentalCameraProviderConfiguration.class)
+    @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
     @MainThread
     static void configureCameraProvider(@NonNull String cameraImplementation) {
         if (!cameraImplementation.equals(sConfiguredCameraXCameraImplementation)) {
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
index 53242a6..50646a9 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
@@ -28,8 +28,8 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.WorkerThread;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.ExperimentalUseCaseGroup;
 import androidx.camera.core.Preview;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -101,7 +101,7 @@
         mExecutor.execute(() -> mNativeContext = initContext());
     }
 
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     @MainThread
     void attachInputPreview(@NonNull Preview preview) {
         preview.setSurfaceProvider(
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.java b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.java
index 0aeb641..3823713 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.java
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.java
@@ -33,7 +33,7 @@
 import android.util.Size;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.camera2.Camera2Config;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.CameraSelector;
@@ -136,7 +136,7 @@
     }
 
     @Before
-    @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     public void setUp() throws Exception {
         mProcessingHandlerThread =
                 new HandlerThread("Processing");
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
index 177210e..7c9077f 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
@@ -151,6 +151,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
index fbf70ba..0814b3d 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
 
@@ -96,6 +97,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
index 17b1eaa..e23aeb9 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
@@ -156,6 +156,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
index 0a8a654..4801055 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
@@ -20,6 +20,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
 
@@ -117,6 +118,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index f2d82b1..eff6232 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -152,6 +152,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
index 9136f89..ca6a8da 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
@@ -19,6 +19,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
 import android.hardware.camera2.TotalCaptureResult;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
 import android.view.Surface;
@@ -135,6 +136,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
index f571dd9..564e941 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.extensions.impl;
 
+import android.graphics.ImageFormat;
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
 import android.util.Pair;
@@ -45,9 +46,9 @@
      * <p> The result of the processing step should be written to the {@link Surface} that was
      * received by {@link #onOutputSurface(Surface, int)}.
      *
-     * @param results The map of images and metadata to process. The {@link Image} that are
-     *                contained within the map will become invalid after this method completes,
-     *                so no references to them should be kept.
+     * @param results The map of {@link ImageFormat#YUV_420_888} format images and metadata to
+     *                process. The {@link Image} that are contained within the map will become
+     *                invalid after this method completes, so no references to them should be kept.
      */
     void process(Map<Integer, Pair<Image, TotalCaptureResult>> results);
 
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
index f1817a7..010ce68 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
@@ -20,6 +20,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
 
 /**
  * Provides interfaces that the OEM needs to implement to handle the state change.
@@ -51,8 +52,10 @@
      * This will be invoked before creating a
      * {@link android.hardware.camera2.CameraCaptureSession}. The {@link CaptureRequest}
      * parameters returned via {@link CaptureStageImpl} will be passed to the camera device as
-     * part of the capture session initialization via setSessionParameters(). The valid parameter
-     * is a subset of the available capture request parameters.
+     * part of the capture session initialization via
+     * {@link SessionConfiguration#setSessionParameters(CaptureRequest)} which only supported
+     * from API level 28. The valid parameter is a subset of the available capture request
+     * parameters.
      *
      * @return The request information to set the session wide camera parameters.
      */
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index 6ac0f3e..2ac4548 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -229,6 +229,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
         return captureStage;
     }
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
index 7b0eb61..1434871 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
@@ -151,6 +151,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
index fb4a855..8bb6074 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
 import android.util.Pair;
 import android.util.Size;
 
@@ -96,6 +97,12 @@
 
     @Override
     public CaptureStageImpl onPresetSession() {
+        // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+        // (CaptureRequest) which only supported from API level 28.
+        if (Build.VERSION.SDK_INT < 28) {
+            return null;
+        }
+
         // Set the necessary CaptureRequest parameters via CaptureStage, here we use some
         // placeholder set of CaptureRequest.Key values
         SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
index 0e7879e..7caad1a 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.extensions.impl;
 
+import android.graphics.ImageFormat;
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
 
@@ -32,8 +33,8 @@
      * <p> The result of the processing step should be written to the {@link android.view.Surface}
      * that was received by {@link ProcessorImpl#onOutputSurface(android.view.Surface, int)}.
      *
-     * @param image The image to process. This will be invalid after the method completes so no
-     *              reference to it should be kept.
+     * @param image  The {@link ImageFormat#YUV_420_888} format image to process. This will be
+     *               invalid after the method completes so no reference to it should be kept.
      * @param result The metadata associated with the image to process.
      */
     void process(Image image, TotalCaptureResult result);
diff --git a/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml b/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
index da988fa..8833509 100644
--- a/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
@@ -34,8 +34,10 @@
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
+    <!-- The attribute, requestLegacyExternalStorage, is ignored if app is targeted Android 11
+     (API level 30),. -->
     <application
+        android:requestLegacyExternalStorage="true"
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt
index 19f829c..ab9f1a2 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt
@@ -35,6 +35,7 @@
 import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.ImageProxy
 import androidx.camera.integration.antelope.MainActivity.Companion.PHOTOS_DIR
+import androidx.camera.integration.antelope.MainActivity.Companion.PHOTOS_PATH
 import androidx.camera.integration.antelope.MainActivity.Companion.logd
 import androidx.camera.integration.antelope.cameracontrollers.CameraState
 import androidx.camera.integration.antelope.cameracontrollers.closeCameraX
@@ -186,7 +187,7 @@
  * Actually write a byteArray file to disk. Assume the file is a jpg and use that extension
  */
 fun writeFile(activity: MainActivity, bytes: ByteArray) {
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
         writeFileAfterQ(activity, bytes)
     } else {
         writeFileBeforeQ(activity, bytes)
@@ -194,8 +195,10 @@
 }
 
 /**
- * Original writeFile implementation. It is workable on Pie and Pei lower for
- * Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
+ * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
+ * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
+ * make it workable. Ref:
+ * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
  */
 fun writeFileBeforeQ(activity: MainActivity, bytes: ByteArray) {
     val jpgFile = File(
@@ -255,21 +258,18 @@
 }
 
 /**
- * After Q, change to use MediaStore to access the shared media files.
+ * R and R above, change to use MediaStore to access the shared media files. Ref:
  * https://developer.android.com/training/data-storage/shared
  */
 fun writeFileAfterQ(activity: MainActivity, bytes: ByteArray) {
-    var imageUri: Uri?
     val resolver: ContentResolver = activity.contentResolver
-
-    val relativeLocation = Environment.DIRECTORY_DCIM + File.separatorChar + PHOTOS_DIR
     val contentValues = ContentValues().apply {
         put(MediaStore.MediaColumns.DISPLAY_NAME, generateTimestamp().toString() + ".jpg")
         put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
-        put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation)
+        put(MediaStore.MediaColumns.RELATIVE_PATH, PHOTOS_PATH)
     }
 
-    imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+    val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
     if (imageUri != null) {
         val output = activity.contentResolver.openOutputStream(imageUri)
         try {
@@ -303,9 +303,28 @@
 }
 
 /**
- * Delete all the photos generated by testing from the default Antelope PHOTOS_DIR
+ * Delete all the photos generated by testing
  */
 fun deleteTestPhotos(activity: MainActivity) {
+    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+        deleteTestPhotosAfterQ(activity)
+    } else {
+        deleteTestPhotosBeforeQ(activity)
+    }
+
+    activity.runOnUiThread {
+        Toast.makeText(activity, "All test photos deleted", Toast.LENGTH_SHORT).show()
+    }
+    logd("All photos in storage directory DCIM/" + PHOTOS_DIR + " deleted.")
+}
+
+/**
+ * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
+ * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
+ * make it workable. Ref:
+ * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
+ */
+fun deleteTestPhotosBeforeQ(activity: MainActivity) {
     val photosDir = File(
         Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
         PHOTOS_DIR
@@ -320,15 +339,27 @@
         val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
         scannerIntent.data = Uri.fromFile(photosDir)
         activity.sendBroadcast(scannerIntent)
-
-        activity.runOnUiThread {
-            Toast.makeText(activity, "All test photos deleted", Toast.LENGTH_SHORT).show()
-        }
-        logd("All photos in storage directory DCIM/" + PHOTOS_DIR + " deleted.")
     }
 }
 
 /**
+ * R and R above, change to use MediaStore to delete the photo files. Ref:
+ * https://developer.android.com/training/data-storage/shared
+ */
+fun deleteTestPhotosAfterQ(activity: MainActivity) {
+    val imageDirUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+    val resolver: ContentResolver = activity.contentResolver
+    val selection = MediaStore.MediaColumns.RELATIVE_PATH + " like ?"
+    val selectionArgs = arrayOf("%$PHOTOS_PATH%")
+
+    resolver.delete(
+        imageDirUri,
+        selection,
+        selectionArgs
+    )
+}
+
+/**
  * Try to detect if a saved image file has had HDR effects applied to it by examining the EXIF tag.
  *
  * Note: this does not currently work.
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
index 2027e63..70a7ae4 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
@@ -23,6 +23,7 @@
 import android.content.pm.PackageManager
 import android.content.res.Configuration
 import android.os.Bundle
+import android.os.Environment
 import android.os.Handler
 import android.os.HandlerThread
 import android.util.Log
@@ -42,6 +43,7 @@
 import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
 import androidx.test.espresso.idling.CountingIdlingResource
+import java.io.File
 
 /**
  * Main Antelope Activity
@@ -93,6 +95,8 @@
         /** Idling Resource used for Espresso tests */
         public val antelopeIdlingResource = CountingIdlingResource("AntelopeIdlingResource")
 
+        val PHOTOS_PATH = Environment.DIRECTORY_DCIM + File.separatorChar + PHOTOS_DIR
+        val LOG_PATH = Environment.DIRECTORY_DOCUMENTS + File.separatorChar + LOG_DIR
         /** Convenience wrapper for Log.d that can be toggled on/off */
         fun logd(message: String) {
             if (camViewModel.getShouldOutputLog().value ?: false)
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt
index 5dedacf..03eaa24 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt
@@ -26,7 +26,7 @@
 import android.os.Environment
 import android.provider.MediaStore
 import android.widget.Toast
-import androidx.camera.integration.antelope.MainActivity.Companion.LOG_DIR
+import androidx.camera.integration.antelope.MainActivity.Companion.LOG_PATH
 import androidx.camera.integration.antelope.MainActivity.Companion.logd
 import com.google.common.math.Quantiles
 import com.google.common.math.Stats
@@ -281,7 +281,7 @@
  * @param csv The comma-based csv string
  */
 fun writeCSV(activity: MainActivity, filePrefix: String, csv: String) {
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
         writeCSVAfterQ(activity, filePrefix, csv)
     } else {
         writeCSVBeforeQ(activity, filePrefix, csv)
@@ -289,8 +289,10 @@
 }
 
 /**
-* Original writeFile implementation. It is workable on Pie and Pei lower for
-* Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
+ * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
+ * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
+ * make it workable. Ref:
+ * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
 */
 fun writeCSVBeforeQ(activity: MainActivity, prefix: String, csv: String) {
     val csvFile = File(
@@ -374,7 +376,7 @@
 }
 
 /**
- * After Q, change to use MediaStore to access the shared media files.
+ * R and R above, change to use MediaStore to access the shared media files.
  * https://developer.android.com/training/data-storage/shared
  *
  * @param activity The main activity
@@ -383,17 +385,17 @@
  */
 fun writeCSVAfterQ(activity: MainActivity, prefix: String, csv: String) {
     var output: OutputStream?
-    var csvUri: Uri?
     val resolver: ContentResolver = activity.contentResolver
-
-    val relativePath = Environment.DIRECTORY_DOCUMENTS + File.separatorChar + LOG_DIR
     val contentValues = ContentValues().apply {
         put(MediaStore.MediaColumns.DISPLAY_NAME, prefix + "_" + generateCSVTimestamp() + ".csv")
         put(MediaStore.MediaColumns.MIME_TYPE, "text/comma-separated-values")
-        put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
+        put(MediaStore.MediaColumns.RELATIVE_PATH, LOG_PATH)
     }
 
-    csvUri = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
+    val csvUri = resolver.insert(
+        MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+        contentValues
+    )
     if (csvUri != null) {
         lateinit var bufferWriter: BufferedWriter
         try {
@@ -422,9 +424,45 @@
 }
 
 /**
- * Delete all Antelope .csv files in the documents directory
+ * Delete all Antelope .csv files in the Documents directory
  */
 fun deleteCSVFiles(activity: MainActivity) {
+    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+        deleteCSVFilesAfterQ(activity)
+    } else {
+        deleteCSVFilesBeforeQ(activity)
+    }
+
+    activity.runOnUiThread {
+        Toast.makeText(activity, "CSV logs deleted", Toast.LENGTH_SHORT).show()
+    }
+    logd("All csv logs in directory DOCUMENTS/" + MainActivity.LOG_DIR + " deleted.")
+}
+
+/**
+ * R and R above, change to use MediaStore to delete the log files. It will delete records in media
+ * store and the physical log files.
+ */
+fun deleteCSVFilesAfterQ(activity: MainActivity) {
+    val logDirUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
+    val resolver: ContentResolver = activity.contentResolver
+    val selection = MediaStore.MediaColumns.RELATIVE_PATH + " like ?"
+    val selectionArgs = arrayOf("%$LOG_PATH%")
+
+    resolver.delete(
+        logDirUri,
+        selection,
+        selectionArgs
+    )
+}
+
+/**
+ * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
+ * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
+ * make it workable. Ref:
+ * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
+ */
+fun deleteCSVFilesBeforeQ(activity: MainActivity) {
     val csvDir = File(
         Environment.getExternalStoragePublicDirectory(
             Environment.DIRECTORY_DOCUMENTS
@@ -441,11 +479,6 @@
         val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
         scannerIntent.data = Uri.fromFile(csvDir)
         activity.sendBroadcast(scannerIntent)
-
-        activity.runOnUiThread {
-            Toast.makeText(activity, "CSV logs deleted", Toast.LENGTH_SHORT).show()
-        }
-        logd("All csv logs in directory DOCUMENTS/" + MainActivity.LOG_DIR + " deleted.")
     }
 }
 
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt
index e147a94..f86966c 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt
@@ -22,7 +22,7 @@
 import android.util.Log
 import android.view.Surface
 import android.view.ViewGroup
-import androidx.annotation.experimental.UseExperimental
+import androidx.annotation.OptIn
 import androidx.camera.camera2.interop.Camera2Interop
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
 import androidx.camera.core.CameraSelector
@@ -307,7 +307,7 @@
 /**
  * Setup the Camera X preview use case
  */
-@UseExperimental(markerClass = ExperimentalCamera2Interop::class)
+@OptIn(ExperimentalCamera2Interop::class)
 private fun cameraXPreviewUseCaseBuilder(
     focusMode: FocusMode,
     deviceStateCallback: CameraDevice.StateCallback,
@@ -328,7 +328,7 @@
 /**
  * Setup the Camera X image capture use case
  */
-@UseExperimental(markerClass = ExperimentalCamera2Interop::class)
+@OptIn(ExperimentalCamera2Interop::class)
 private fun cameraXImageCaptureUseCaseBuilder(
     focusMode: FocusMode,
     deviceStateCallback:
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index e833abd..46cb4308 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -37,9 +37,9 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageAnalysis;
 import androidx.camera.core.ImageCapture;
@@ -117,7 +117,7 @@
 
     @NonNull
     @Override
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     public View onCreateView(
             @NonNull LayoutInflater inflater,
             @Nullable ViewGroup container,
@@ -367,7 +367,7 @@
     /**
      * Updates UI text based on the state of {@link #mCameraController}.
      */
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     private void updateUiText() {
         mFlashMode.setText(getFlashModeTextResId());
         final Integer lensFacing = mCameraController.getCameraSelector().getLensFacing();
@@ -424,7 +424,7 @@
         }
     }
 
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     private void onUseCaseToggled(CompoundButton compoundButton, boolean value) {
         if (mCaptureEnabledToggle == null || mAnalysisEnabledToggle == null
                 || mVideoEnabledToggle == null) {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraViewFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraViewFragment.java
index d21b2f5..4cf40a6 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraViewFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraViewFragment.java
@@ -30,7 +30,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.view.CameraView;
 import androidx.camera.view.CameraView.CaptureMode;
@@ -152,7 +152,7 @@
     }
 
     @Override
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
         super.onViewStateRestored(savedInstanceState);
 
@@ -261,7 +261,7 @@
         }
     }
 
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     void updateModeButtonIcon() {
         if (mCameraView.getCaptureMode() == CaptureMode.MIXED) {
             mModeButton.setButtonDrawable(R.drawable.ic_photo_camera);
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java
index c109dfd..0f08848 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java
@@ -31,7 +31,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCapture.OnImageSavedCallback;
 import androidx.camera.core.ImageCaptureException;
@@ -101,7 +101,7 @@
     }
 
     /** Called when the user taps. */
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     private void onTap() {
         if (mCameraView.getCaptureMode() == CaptureMode.IMAGE
                 || mCameraView.getCaptureMode() == CaptureMode.MIXED) {
@@ -142,7 +142,7 @@
     }
 
     /** Called when the user holds (long presses). */
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     private void onHold() {
         if (mCameraView.getCaptureMode() == CaptureMode.VIDEO
                 || mCameraView.getCaptureMode() == CaptureMode.MIXED) {
@@ -167,7 +167,7 @@
     }
 
     /** Called when the user releases. */
-    @UseExperimental(markerClass = ExperimentalVideo.class)
+    @OptIn(markerClass = ExperimentalVideo.class)
     private void onRelease() {
         if (mCameraView.getCaptureMode() == CaptureMode.VIDEO
                 || mCameraView.getCaptureMode() == CaptureMode.MIXED) {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
index 6933449..fce0f3e 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
@@ -37,8 +37,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraInfoUnavailableException;
 import androidx.camera.core.CameraSelector;
@@ -166,7 +166,7 @@
         }
     }
 
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     void setUpTargetRotationButton(@NonNull final ProcessCameraProvider cameraProvider,
             @NonNull final View rootView) {
         Button button = rootView.findViewById(R.id.target_rotation);
@@ -328,8 +328,8 @@
     }
 
     @SuppressWarnings("WeakerAccess")
-    @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
-    @SuppressLint("UnsafeExperimentalUsageError")
+    @OptIn(markerClass = ExperimentalUseCaseGroup.class)
+    @SuppressLint("UnsafeOptInUsageError")
     void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
         if (mPreview == null) {
             return;
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
index d15867e..5b18e9d 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
@@ -161,9 +161,13 @@
         // Loop through the y plane and get the sum of the luminance for each tile.
         byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
         image.getPlanes()[0].getBuffer().get(bytes);
+        int tileX;
+        int tileY;
         for (int x = 0; x < cropRect.width(); x++) {
             for (int y = 0; y < cropRect.height(); y++) {
-                tiles[x / tileWidth][y / tileHeight] +=
+                tileX = Math.min(x / tileWidth, TILE_COUNT - 1);
+                tileY = Math.min(y / tileHeight, TILE_COUNT - 1);
+                tiles[tileX][tileY] +=
                         bytes[(y + cropRect.top) * image.getWidth() + cropRect.left + x] & 0xFF;
             }
         }
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml
index ee0b902..7938a58 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml
@@ -56,8 +56,8 @@
                 style="?android:buttonBarButtonStyle"
                 android:layout_width="match_parent"
                 android:layout_height="0dp"
-                android:layout_marginBottom="50dp"
-                android:layout_marginTop="50dp"
+                android:layout_marginBottom="10dp"
+                android:layout_marginTop="10dp"
                 android:layout_weight="1"
                 android:text="@string/btn_capture" />
 
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
index aebdc61..a8791e7 100644
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
+++ b/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
@@ -335,7 +335,8 @@
             //TODO("b/177083268: Multiple hosts support is not implemented")
         }
 
-        if (!bindService(rendererIntent, mServiceConnectionImpl, Context.BIND_AUTO_CREATE)) {
+        if (!bindService(rendererIntent, mServiceConnectionImpl,
+                Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES)) {
             onServiceConnectionError(
                     "Cannot bind to the renderer host with intent: " + rendererIntent);
         }
@@ -402,7 +403,6 @@
         } catch (RemoteException e) {
             // We are already unbinding (maybe because the host has already cut the connection)
             // Let's not log more errors unnecessarily.
-            //TODO(179506019): Revisit calls to unbindService()
         }
 
         unbindService(mServiceConnectionImpl);
diff --git a/car/app/app-samples/helloworld/build.gradle b/car/app/app-samples/helloworld/build.gradle
index a6b8625..8cb3f37 100644
--- a/car/app/app-samples/helloworld/build.gradle
+++ b/car/app/app-samples/helloworld/build.gradle
@@ -1,7 +1,4 @@
-import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_CORE
-import static androidx.build.dependencies.DependenciesKt.JUNIT
-import static androidx.build.dependencies.DependenciesKt.ROBOLECTRIC
-import static androidx.build.dependencies.DependenciesKt.TRUTH
+import static androidx.build.dependencies.DependenciesKt.*
 
 /*
  * Copyright (C) 2021 The Android Open Source Project
@@ -26,7 +23,7 @@
 
 android {
     defaultConfig {
-        applicationId "androidx.car.app.samples.helloworld"
+        applicationId "androidx.car.app.sample.helloworld"
         minSdkVersion 23
         versionCode 1
         versionName "1.0"
diff --git a/car/app/app-samples/helloworld/github_build.gradle b/car/app/app-samples/helloworld/github_build.gradle
index b6e2004..feb0b85a 100644
--- a/car/app/app-samples/helloworld/github_build.gradle
+++ b/car/app/app-samples/helloworld/github_build.gradle
@@ -20,7 +20,7 @@
     compileSdkVersion 29
 
     defaultConfig {
-        applicationId "androidx.car.app.samples.helloworld"
+        applicationId "androidx.car.app.sample.helloworld"
         minSdkVersion 23
         targetSdkVersion 29
         versionCode 1
@@ -34,7 +34,7 @@
 }
 
 dependencies {
-    implementation "androidx.car.app:app:1.0.0-beta01"
+    implementation "androidx.car.app:app:1.0.0-rc01"
 }
 
 
diff --git a/car/app/app-samples/helloworld/src/main/AndroidManifest.xml b/car/app/app-samples/helloworld/src/main/AndroidManifest.xml
index c06779c..57f9e09 100644
--- a/car/app/app-samples/helloworld/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/helloworld/src/main/AndroidManifest.xml
@@ -16,7 +16,7 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    package="androidx.car.app.samples.helloworld"
+    package="androidx.car.app.sample.helloworld"
     android:versionCode="1"
     android:versionName="1.0">
   <application
@@ -26,7 +26,7 @@
         android:resource="@xml/automotive_app_desc"
         tools:ignore="MetadataTagInsideApplicationTag" />
     <service
-        android:name=".HelloWorldService"
+        android:name="androidx.car.app.sample.helloworld.HelloWorldService"
         android:exported="true">
       <intent-filter>
         <action android:name="androidx.car.app.CarAppService" />
diff --git a/car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldScreen.java b/car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldScreen.java
similarity index 96%
rename from car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldScreen.java
rename to car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldScreen.java
index d1602f5..048e20a 100644
--- a/car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldScreen.java
+++ b/car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldScreen.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.car.app.samples.helloworld;
+package androidx.car.app.sample.helloworld;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldService.java b/car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldService.java
similarity index 97%
rename from car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldService.java
rename to car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldService.java
index 6e71d87..d333abf 100644
--- a/car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldService.java
+++ b/car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldService.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.car.app.samples.helloworld;
+package androidx.car.app.sample.helloworld;
 
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
diff --git a/car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldScreenTest.java b/car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldScreenTest.java
similarity index 97%
rename from car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldScreenTest.java
rename to car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldScreenTest.java
index 0d96725..ec49b05 100644
--- a/car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldScreenTest.java
+++ b/car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldScreenTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.helloworld;
+package androidx.car.app.sample.helloworld;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldSessionTest.java b/car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldSessionTest.java
similarity index 97%
rename from car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldSessionTest.java
rename to car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldSessionTest.java
index 1a126b0..e510ecb 100644
--- a/car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldSessionTest.java
+++ b/car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldSessionTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.helloworld;
+package androidx.car.app.sample.helloworld;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/car/app/app-samples/navigation/build.gradle b/car/app/app-samples/navigation/build.gradle
index 8365d1e..ce1db708 100644
--- a/car/app/app-samples/navigation/build.gradle
+++ b/car/app/app-samples/navigation/build.gradle
@@ -21,7 +21,7 @@
 
 android {
     defaultConfig {
-        applicationId "androidx.car.app.samples.navigation"
+        applicationId "androidx.car.app.sample.navigation"
         minSdkVersion 23
         targetSdkVersion 29
         versionCode 1
diff --git a/car/app/app-samples/navigation/github_build.gradle b/car/app/app-samples/navigation/github_build.gradle
index 41ece82..1ebb356 100644
--- a/car/app/app-samples/navigation/github_build.gradle
+++ b/car/app/app-samples/navigation/github_build.gradle
@@ -20,7 +20,7 @@
     compileSdkVersion 29
 
     defaultConfig {
-        applicationId "androidx.car.app.samples.navigation"
+        applicationId "androidx.car.app.sample.navigation"
         minSdkVersion 23
         targetSdkVersion 29
         versionCode 1
@@ -37,5 +37,5 @@
     implementation "androidx.constraintlayout:constraintlayout:1.1.3"
     implementation "androidx.core:core:1.5.0-alpha01"
 
-    implementation "androidx.car.app:app:1.0.0-beta01"
+    implementation "androidx.car.app:app:1.0.0-rc01"
 }
diff --git a/car/app/app-samples/navigation/src/main/AndroidManifest.xml b/car/app/app-samples/navigation/src/main/AndroidManifest.xml
index d9457f7..9d1e3f4 100644
--- a/car/app/app-samples/navigation/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/navigation/src/main/AndroidManifest.xml
@@ -16,7 +16,7 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    package="androidx.car.app.samples.navigation"
+    package="androidx.car.app.sample.navigation"
     android:versionCode="1"
     android:versionName="1.0">
 
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/app/MainActivity.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/app/MainActivity.java
similarity index 95%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/app/MainActivity.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/app/MainActivity.java
index 4abbdc3..d4a117c 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/app/MainActivity.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/app/MainActivity.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.app;
+package androidx.car.app.sample.navigation.app;
 
 import android.Manifest;
 import android.app.Activity;
@@ -29,8 +29,8 @@
 import android.widget.Button;
 
 import androidx.annotation.NonNull;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.nav.NavigationService;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.nav.NavigationService;
 
 /**
  * The main app activity.
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/FavoritesScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/FavoritesScreen.java
similarity index 95%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/FavoritesScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/FavoritesScreen.java
index 5a84ab5..ee43bb0 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/FavoritesScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/FavoritesScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
 
@@ -36,9 +36,9 @@
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
 import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.model.DemoScripts;
-import androidx.car.app.samples.navigation.model.PlaceInfo;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.model.DemoScripts;
+import androidx.car.app.sample.navigation.model.PlaceInfo;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationCarAppService.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationCarAppService.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationCarAppService.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationCarAppService.java
index 9cdf56f..6c36afb 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationCarAppService.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationCarAppService.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import android.annotation.SuppressLint;
 import android.app.Notification;
@@ -27,7 +27,7 @@
 import androidx.annotation.NonNull;
 import androidx.car.app.CarAppService;
 import androidx.car.app.Session;
-import androidx.car.app.samples.navigation.R;
+import androidx.car.app.sample.navigation.R;
 import androidx.car.app.validation.HostValidator;
 import androidx.core.app.NotificationCompat;
 import androidx.lifecycle.DefaultLifecycleObserver;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationScreen.java
similarity index 98%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationScreen.java
index 0781c43..9779ffe 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -33,8 +33,8 @@
 import androidx.car.app.navigation.model.RoutingInfo;
 import androidx.car.app.navigation.model.Step;
 import androidx.car.app.navigation.model.TravelEstimate;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.model.Instruction;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.model.Instruction;
 import androidx.core.graphics.drawable.IconCompat;
 
 import java.util.ArrayList;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationSession.java
similarity index 93%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationSession.java
index cc41007..713ae63 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationSession.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
@@ -28,6 +28,7 @@
 import android.location.LocationListener;
 import android.location.LocationManager;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.util.Log;
 
@@ -44,10 +45,10 @@
 import androidx.car.app.navigation.model.Destination;
 import androidx.car.app.navigation.model.Step;
 import androidx.car.app.navigation.model.TravelEstimate;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.model.Instruction;
-import androidx.car.app.samples.navigation.nav.DeepLinkNotificationReceiver;
-import androidx.car.app.samples.navigation.nav.NavigationService;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.model.Instruction;
+import androidx.car.app.sample.navigation.nav.DeepLinkNotificationReceiver;
+import androidx.car.app.sample.navigation.nav.NavigationService;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.Lifecycle;
@@ -112,6 +113,20 @@
                 public void onLocationChanged(Location location) {
                     mNavigationCarSurface.updateLocationString(getLocationString(location));
                 }
+
+                /** @deprecated This callback will never be invoked on Android Q and above. */
+                @Override
+                @Deprecated
+                public void onStatusChanged(String provider, int status, Bundle extras) {
+                }
+
+                @Override
+                public void onProviderEnabled(String provider) {
+                }
+
+                @Override
+                public void onProviderDisabled(String provider) {
+                }
             };
 
     // Monitors the state of the connection to the Navigation service.
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RequestPermissionScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RequestPermissionScreen.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RequestPermissionScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RequestPermissionScreen.java
index 626def0..962442b 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RequestPermissionScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RequestPermissionScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -33,7 +33,7 @@
 import androidx.car.app.model.MessageTemplate;
 import androidx.car.app.model.ParkedOnlyOnClickListener;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.navigation.app.MainActivity;
+import androidx.car.app.sample.navigation.app.MainActivity;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RoutePreviewScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RoutePreviewScreen.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RoutePreviewScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RoutePreviewScreen.java
index 94a2472..d35bddd 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RoutePreviewScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RoutePreviewScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import android.text.SpannableString;
 import android.util.Log;
@@ -29,7 +29,7 @@
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
 import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
-import androidx.car.app.samples.navigation.R;
+import androidx.car.app.sample.navigation.R;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchResultsScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchResultsScreen.java
similarity index 96%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchResultsScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchResultsScreen.java
index 7b27774..53dd243 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchResultsScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchResultsScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
 
@@ -35,8 +35,8 @@
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
 import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
-import androidx.car.app.samples.navigation.model.DemoScripts;
-import androidx.car.app.samples.navigation.model.PlaceInfo;
+import androidx.car.app.sample.navigation.model.DemoScripts;
+import androidx.car.app.sample.navigation.model.PlaceInfo;
 
 /** Screen for showing a list of places from a search. */
 public final class SearchResultsScreen extends Screen {
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchScreen.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchScreen.java
index a65e8be..6a45232 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -26,7 +26,7 @@
 import androidx.car.app.model.SearchTemplate;
 import androidx.car.app.model.SearchTemplate.SearchCallback;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.navigation.model.DemoScripts;
+import androidx.car.app.sample.navigation.model.DemoScripts;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SettingsScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SettingsScreen.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SettingsScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SettingsScreen.java
index 559e76e..143ccd8 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SettingsScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SettingsScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -29,7 +29,7 @@
 import androidx.car.app.model.SectionedItemList;
 import androidx.car.app.model.Template;
 import androidx.car.app.model.Toggle;
-import androidx.car.app.samples.navigation.R;
+import androidx.car.app.sample.navigation.R;
 
 /** Settings screen demo. */
 public final class SettingsScreen extends Screen {
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SurfaceRenderer.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SurfaceRenderer.java
similarity index 99%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SurfaceRenderer.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SurfaceRenderer.java
index 3b373d8..c148881 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SurfaceRenderer.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SurfaceRenderer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
 
 import android.graphics.Canvas;
 import android.graphics.Color;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/DemoScripts.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/DemoScripts.java
similarity index 98%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/DemoScripts.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/DemoScripts.java
index 97c4aa0..d7bf70a 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/DemoScripts.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/DemoScripts.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.model;
+package androidx.car.app.sample.navigation.model;
 
 import static androidx.car.app.navigation.model.LaneDirection.SHAPE_NORMAL_RIGHT;
 import static androidx.car.app.navigation.model.LaneDirection.SHAPE_STRAIGHT;
@@ -79,7 +79,7 @@
 import androidx.car.app.navigation.model.Maneuver;
 import androidx.car.app.navigation.model.Step;
 import androidx.car.app.navigation.model.TravelEstimate;
-import androidx.car.app.samples.navigation.R;
+import androidx.car.app.sample.navigation.R;
 import androidx.core.graphics.drawable.IconCompat;
 
 import java.util.ArrayList;
@@ -140,13 +140,7 @@
                 new CarIcon.Builder(
                         IconCompat.createWithResource(
                                 carContext,
-                                androidx.car
-                                        .app
-                                        .samples
-                                        .navigation
-                                        .R
-                                        .drawable
-                                        .junction_image))
+                                R.drawable.junction_image))
                         .build();
 
         Lane straightNormal =
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Instruction.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Instruction.java
similarity index 99%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Instruction.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Instruction.java
index 56f688f..7ddb4ca 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Instruction.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Instruction.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.model;
+package androidx.car.app.sample.navigation.model;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/PlaceInfo.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/PlaceInfo.java
similarity index 95%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/PlaceInfo.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/PlaceInfo.java
index 05a0d8f..237325e 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/PlaceInfo.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/PlaceInfo.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.model;
+package androidx.car.app.sample.navigation.model;
 
 import androidx.annotation.NonNull;
 
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Script.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Script.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Script.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Script.java
index a8ff7e84e..baf9976 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Script.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Script.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.model;
+package androidx.car.app.sample.navigation.model;
 
 import android.os.Handler;
 
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/DeepLinkNotificationReceiver.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/DeepLinkNotificationReceiver.java
similarity index 94%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/DeepLinkNotificationReceiver.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/DeepLinkNotificationReceiver.java
index 67b55a9..13c0758 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/DeepLinkNotificationReceiver.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/DeepLinkNotificationReceiver.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.nav;
+package androidx.car.app.sample.navigation.nav;
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -23,7 +23,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
-import androidx.car.app.samples.navigation.car.NavigationCarAppService;
+import androidx.car.app.sample.navigation.car.NavigationCarAppService;
 
 import java.util.Arrays;
 import java.util.HashSet;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/NavigationService.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/NavigationService.java
index 9d07221..41a5f5ef 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/NavigationService.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.navigation.nav;
+package androidx.car.app.sample.navigation.nav;
 
 import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
 
-import static androidx.car.app.samples.navigation.nav.DeepLinkNotificationReceiver.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP;
+import static androidx.car.app.sample.navigation.nav.DeepLinkNotificationReceiver.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP;
 
 import android.annotation.SuppressLint;
 import android.app.Notification;
@@ -54,10 +54,10 @@
 import androidx.car.app.navigation.model.TravelEstimate;
 import androidx.car.app.navigation.model.Trip;
 import androidx.car.app.notification.CarAppExtender;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.app.MainActivity;
-import androidx.car.app.samples.navigation.model.Instruction;
-import androidx.car.app.samples.navigation.model.Script;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.app.MainActivity;
+import androidx.car.app.sample.navigation.model.Instruction;
+import androidx.car.app.sample.navigation.model.Script;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationManagerCompat;
 
@@ -79,7 +79,7 @@
 
     // Constants for location broadcast
     private static final String PACKAGE_NAME =
-            "androidx.car.app.samples.navigation.nav.navigationservice";
+            "androidx.car.app.sample.navigation.nav.navigationservice";
 
     private static final String EXTRA_STARTED_FROM_NOTIFICATION =
             PACKAGE_NAME + ".started_from_notification";
diff --git a/car/app/app-samples/navigation/src/main/res/layout/activity_main.xml b/car/app/app-samples/navigation/src/main/res/layout/activity_main.xml
index b311504..e84efcf 100644
--- a/car/app/app-samples/navigation/src/main/res/layout/activity_main.xml
+++ b/car/app/app-samples/navigation/src/main/res/layout/activity_main.xml
@@ -15,9 +15,7 @@
      limitations under the License.
 -->
 
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
diff --git a/car/app/app-samples/places/build.gradle b/car/app/app-samples/places/build.gradle
index 01222f5..bc3b2e7 100644
--- a/car/app/app-samples/places/build.gradle
+++ b/car/app/app-samples/places/build.gradle
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import static androidx.build.dependencies.DependenciesKt.*
+
+import static androidx.build.dependencies.DependenciesKt.GUAVA_ANDROID
 
 plugins {
     id("AndroidXPlugin")
@@ -22,7 +23,7 @@
 
 android {
     defaultConfig {
-        applicationId "androidx.car.app.samples.places"
+        applicationId "androidx.car.app.sample.places"
         minSdkVersion 23
         targetSdkVersion 29
         versionCode 1
diff --git a/car/app/app-samples/places/github_build.gradle b/car/app/app-samples/places/github_build.gradle
index 681d3c18..7d94154 100644
--- a/car/app/app-samples/places/github_build.gradle
+++ b/car/app/app-samples/places/github_build.gradle
@@ -19,7 +19,7 @@
 android {
     compileSdkVersion 29
     defaultConfig {
-        applicationId "androidx.car.app.samples.places"
+        applicationId "androidx.car.app.sample.places"
         minSdkVersion 23
         targetSdkVersion 29
         versionCode 1
@@ -36,5 +36,5 @@
     implementation 'com.github.bumptech.glide:glide:4.9.0'
     implementation 'com.google.guava:guava:28.1-jre'
 
-    implementation "androidx.car.app:app:1.0.0-beta01"
+    implementation "androidx.car.app:app:1.0.0-rc01"
 }
\ No newline at end of file
diff --git a/car/app/app-samples/places/src/main/AndroidManifest.xml b/car/app/app-samples/places/src/main/AndroidManifest.xml
index 3e6b254..01fe5dd 100644
--- a/car/app/app-samples/places/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/places/src/main/AndroidManifest.xml
@@ -16,7 +16,7 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    package="androidx.car.app.samples.places"
+    package="androidx.car.app.sample.places"
     android:versionCode="1"
     android:versionName="1.0">
 
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Constants.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Constants.java
similarity index 95%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Constants.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Constants.java
index 00bdf75..f27d43a 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Constants.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Constants.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
 import android.location.Location;
 
-import androidx.car.app.samples.places.places.PlaceCategory;
+import androidx.car.app.sample.places.places.PlaceCategory;
 
 /** App-wide constants */
 class Constants {
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Executors.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Executors.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Executors.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Executors.java
index b33177e..9e672d3 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Executors.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Executors.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/ImageUtil.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/ImageUtil.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/ImageUtil.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/ImageUtil.java
index e838eaa..2bb5cc9 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/ImageUtil.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/ImageUtil.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
 import android.content.Context;
 import android.graphics.Bitmap;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceCategoryListScreen.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceCategoryListScreen.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceCategoryListScreen.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceCategoryListScreen.java
index ee2c806..ab1ad5e 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceCategoryListScreen.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceCategoryListScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
 import android.location.Location;
 
@@ -32,7 +32,7 @@
 import androidx.car.app.model.PlaceMarker;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.places.places.PlaceCategory;
+import androidx.car.app.sample.places.places.PlaceCategory;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceDetailsScreen.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceDetailsScreen.java
similarity index 95%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceDetailsScreen.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceDetailsScreen.java
index 40d20ef..44abe3b 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceDetailsScreen.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceDetailsScreen.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
-import static androidx.car.app.samples.places.Executors.BACKGROUND_EXECUTOR;
-import static androidx.car.app.samples.places.Executors.UI_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.BACKGROUND_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.UI_EXECUTOR;
 
 import android.content.Context;
 import android.content.Intent;
@@ -40,9 +40,9 @@
 import androidx.car.app.model.PaneTemplate;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.places.places.PlaceDetails;
-import androidx.car.app.samples.places.places.PlaceFinder;
-import androidx.car.app.samples.places.places.PlaceInfo;
+import androidx.car.app.sample.places.places.PlaceDetails;
+import androidx.car.app.sample.places.places.PlaceFinder;
+import androidx.car.app.sample.places.places.PlaceInfo;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceListScreen.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceListScreen.java
similarity index 95%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceListScreen.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceListScreen.java
index 8bebc62..9440a31 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceListScreen.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceListScreen.java
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
 import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
 
-import static androidx.car.app.samples.places.Executors.BACKGROUND_EXECUTOR;
-import static androidx.car.app.samples.places.Executors.UI_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.BACKGROUND_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.UI_EXECUTOR;
 
 import android.location.Geocoder;
 import android.location.Location;
@@ -41,9 +41,9 @@
 import androidx.car.app.model.PlaceMarker;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.places.places.PlaceCategory;
-import androidx.car.app.samples.places.places.PlaceFinder;
-import androidx.car.app.samples.places.places.PlaceInfo;
+import androidx.car.app.sample.places.places.PlaceCategory;
+import androidx.car.app.sample.places.places.PlaceFinder;
+import androidx.car.app.sample.places.places.PlaceInfo;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlacesCarAppService.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlacesCarAppService.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlacesCarAppService.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlacesCarAppService.java
index 7c80ab2..de90086 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlacesCarAppService.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlacesCarAppService.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/SearchScreen.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/SearchScreen.java
similarity index 94%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/SearchScreen.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/SearchScreen.java
index 94829d9..51e427d 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/SearchScreen.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/SearchScreen.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
-import static androidx.car.app.samples.places.Executors.BACKGROUND_EXECUTOR;
-import static androidx.car.app.samples.places.Executors.UI_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.BACKGROUND_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.UI_EXECUTOR;
 
 import android.location.Geocoder;
 import android.location.Location;
@@ -31,8 +31,8 @@
 import androidx.car.app.model.SearchTemplate;
 import androidx.car.app.model.SearchTemplate.SearchCallback;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.places.places.PlaceFinder;
-import androidx.car.app.samples.places.places.PlaceInfo;
+import androidx.car.app.sample.places.places.PlaceFinder;
+import androidx.car.app.sample.places.places.PlaceInfo;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/UiExecutor.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/UiExecutor.java
similarity index 98%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/UiExecutor.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/UiExecutor.java
index c539ffd..cd0c8da 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/UiExecutor.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/UiExecutor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
 
 import android.os.Handler;
 import android.os.Looper;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/LocationUtil.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/LocationUtil.java
similarity index 96%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/LocationUtil.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/LocationUtil.java
index 401aa73..0f376e7 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/LocationUtil.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/LocationUtil.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
 
 import android.location.Address;
 import android.location.Geocoder;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceCategory.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceCategory.java
similarity index 96%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceCategory.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceCategory.java
index c3ae62e..2c99e25 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceCategory.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceCategory.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
 
 import androidx.annotation.NonNull;
 
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceDetails.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceDetails.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceDetails.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceDetails.java
index 324cc5c..d2c6f5e 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceDetails.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceDetails.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceFinder.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceFinder.java
similarity index 99%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceFinder.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceFinder.java
index 4b4ea2a..70ff88a 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceFinder.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceFinder.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
 
 import android.location.Location;
 import android.util.Log;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceInfo.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceInfo.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceInfo.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceInfo.java
index 3f01357..6f92a08 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceInfo.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceInfo.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
 
 import android.location.Address;
 import android.location.Geocoder;
diff --git a/car/app/app-samples/places/src/main/res/values/themes.xml b/car/app/app-samples/places/src/main/res/values/themes.xml
index 1f311ba..10c8630 100644
--- a/car/app/app-samples/places/src/main/res/values/themes.xml
+++ b/car/app/app-samples/places/src/main/res/values/themes.xml
@@ -16,8 +16,7 @@
 -->
 
 <resources>
-  <style name="CarAppTheme"
-      xmlns:custom="http://schemas.android.com/apk/res-auto">
+  <style name="CarAppTheme">
     <item name="carColorPrimary">#ff41af6a</item>
     <item name="carColorPrimaryDark">#ff41af6a</item>
     <item name="carColorSecondary">#fff3b713</item>
diff --git a/car/app/app-samples/showcase/build.gradle b/car/app/app-samples/showcase/build.gradle
index 661adad..04c40c5 100644
--- a/car/app/app-samples/showcase/build.gradle
+++ b/car/app/app-samples/showcase/build.gradle
@@ -21,7 +21,7 @@
 
 android {
     defaultConfig {
-        applicationId "androidx.car.app.samples.showcase"
+        applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 23
         targetSdkVersion 29
         versionCode 1
@@ -43,4 +43,5 @@
     implementation(project(":car:app:app"))
 
     implementation("androidx.core:core:1.5.0-alpha01")
+    implementation project(path: ':annotation:annotation-experimental')
 }
diff --git a/car/app/app-samples/showcase/github_build.gradle b/car/app/app-samples/showcase/github_build.gradle
index 68f2c69..f07e0ae 100644
--- a/car/app/app-samples/showcase/github_build.gradle
+++ b/car/app/app-samples/showcase/github_build.gradle
@@ -20,7 +20,7 @@
     compileSdkVersion 29
 
     defaultConfig {
-        applicationId "androidx.car.app.samples.showcase"
+        applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 23
         targetSdkVersion 29
         versionCode 1
@@ -38,5 +38,6 @@
 dependencies {
     implementation "androidx.core:core:1.5.0-alpha01"
 
-    implementation "androidx.car.app:app:1.0.0-beta01"
+    implementation "androidx.car.app:app:1.0.0-rc01"
+    implementation "androidx.annotation:annotation-experimental:1.0.0"
 }
diff --git a/car/app/app-samples/showcase/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/src/main/AndroidManifest.xml
index 50f778d..9a8b0281 100644
--- a/car/app/app-samples/showcase/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/src/main/AndroidManifest.xml
@@ -17,7 +17,7 @@
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    package="androidx.car.app.samples.showcase"
+    package="androidx.car.app.sample.showcase"
     android:versionCode="1"
     android:versionName="1.0">
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/DeepLinkNotificationReceiver.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/DeepLinkNotificationReceiver.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/DeepLinkNotificationReceiver.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/DeepLinkNotificationReceiver.java
index a6f7663..f77f978 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/DeepLinkNotificationReceiver.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/DeepLinkNotificationReceiver.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/SelectableListsDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/SelectableListsDemoScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/SelectableListsDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/SelectableListsDemoScreen.java
index d83979f..0904197 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/SelectableListsDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/SelectableListsDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
 
 import static androidx.car.app.CarToast.LENGTH_LONG;
 import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseService.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseService.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseService.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseService.java
index 146af0c..1abcb557d 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseService.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseService.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
 
 import android.content.pm.ApplicationInfo;
 import android.net.Uri;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseSession.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseSession.java
similarity index 93%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseSession.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseSession.java
index 1c4d160..0b47170 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseSession.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseSession.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -31,12 +31,12 @@
 import androidx.car.app.Screen;
 import androidx.car.app.ScreenManager;
 import androidx.car.app.Session;
-import androidx.car.app.samples.showcase.misc.GoToPhoneScreen;
-import androidx.car.app.samples.showcase.misc.PreSeedingFlowScreen;
-import androidx.car.app.samples.showcase.misc.ReservationCancelledScreen;
-import androidx.car.app.samples.showcase.navigation.NavigationNotificationsDemoScreen;
-import androidx.car.app.samples.showcase.navigation.SurfaceRenderer;
-import androidx.car.app.samples.showcase.navigation.routing.NavigatingDemoScreen;
+import androidx.car.app.sample.showcase.misc.GoToPhoneScreen;
+import androidx.car.app.sample.showcase.misc.PreSeedingFlowScreen;
+import androidx.car.app.sample.showcase.misc.ReservationCancelledScreen;
+import androidx.car.app.sample.showcase.navigation.NavigationNotificationsDemoScreen;
+import androidx.car.app.sample.showcase.navigation.SurfaceRenderer;
+import androidx.car.app.sample.showcase.navigation.routing.NavigatingDemoScreen;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/StartScreen.java
similarity index 93%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/StartScreen.java
index 6b66d75..9ca77f1 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/StartScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
@@ -25,10 +25,10 @@
 import androidx.car.app.model.ListTemplate;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.misc.MiscDemoScreen;
-import androidx.car.app.samples.showcase.navigation.NavigationDemosScreen;
-import androidx.car.app.samples.showcase.templates.MiscTemplateDemosScreen;
-import androidx.car.app.samples.showcase.textandicons.TextAndIconsDemosScreen;
+import androidx.car.app.sample.showcase.misc.MiscDemoScreen;
+import androidx.car.app.sample.showcase.navigation.NavigationDemosScreen;
+import androidx.car.app.sample.showcase.templates.MiscTemplateDemosScreen;
+import androidx.car.app.sample.showcase.textandicons.TextAndIconsDemosScreen;
 import androidx.core.graphics.drawable.IconCompat;
 
 /** The starting screen of the app. */
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/TaskRestrictionDemoScreen.java
similarity index 99%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/TaskRestrictionDemoScreen.java
index de416c6..7eca43c 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/TaskRestrictionDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceDetailsScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceDetailsScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceDetailsScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceDetailsScreen.java
index a1fd9b2..3ea8e4f 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceDetailsScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceDetailsScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.common;
+package androidx.car.app.sample.showcase.common;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceInfo.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceInfo.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceInfo.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceInfo.java
index 651177f..d3d5db8 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceInfo.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceInfo.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.common;
+package androidx.car.app.sample.showcase.common;
 
 import android.location.Location;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/SamplePlaces.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/SamplePlaces.java
index 1503215..83ef219 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/SamplePlaces.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.common;
+package androidx.car.app.sample.showcase.common;
 
 import android.graphics.BitmapFactory;
 import android.location.Location;
@@ -35,7 +35,7 @@
 import androidx.car.app.model.Place;
 import androidx.car.app.model.PlaceMarker;
 import androidx.car.app.model.Row;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 
 import java.util.ArrayList;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/Utils.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/Utils.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/Utils.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/Utils.java
index fdc2329..9040f58 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/Utils.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/Utils.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.common;
+package androidx.car.app.sample.showcase.common;
 
 import android.text.Spannable;
 import android.text.SpannableString;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/FinishAppScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/FinishAppScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/FinishAppScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/FinishAppScreen.java
index 9c2cddd..b4457a7 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/FinishAppScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/FinishAppScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
 import static androidx.car.app.model.Action.BACK;
 
@@ -26,7 +26,7 @@
 import androidx.car.app.model.Action;
 import androidx.car.app.model.MessageTemplate;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.ShowcaseService;
+import androidx.car.app.sample.showcase.ShowcaseService;
 
 /** A {@link Screen} that provides an action to exit the car app. */
 public class FinishAppScreen extends Screen {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/GoToPhoneScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/GoToPhoneScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/GoToPhoneScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/GoToPhoneScreen.java
index 2e2c350..37afcd1 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/GoToPhoneScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/GoToPhoneScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/LoadingDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/LoadingDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/LoadingDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/LoadingDemoScreen.java
index 0aed332..719cf61 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/LoadingDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/LoadingDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/MiscDemoScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/MiscDemoScreen.java
index 6f8d97a..b8ecfd8 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/MiscDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/NotificationDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/NotificationDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/NotificationDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/NotificationDemoScreen.java
index 7a578e0..bd1de79 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/NotificationDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/NotificationDemoScreen.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
-import static androidx.car.app.samples.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_CANCEL_RESERVATION;
-import static androidx.car.app.samples.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_PHONE;
+import static androidx.car.app.sample.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_CANCEL_RESERVATION;
+import static androidx.car.app.sample.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_PHONE;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -48,8 +48,8 @@
 import androidx.car.app.model.ItemList;
 import androidx.car.app.model.Template;
 import androidx.car.app.notification.CarAppExtender;
-import androidx.car.app.samples.showcase.DeepLinkNotificationReceiver;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.DeepLinkNotificationReceiver;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.core.graphics.drawable.IconCompat;
@@ -70,9 +70,9 @@
     private static final CharSequence NOTIFICATION_CHANNEL_LOW_NAME = "Low Channel";
 
     private static final String INTENT_ACTION_PRIMARY_PHONE =
-            "androidx.car.app.samples.showcase.INTENT_ACTION_PRIMARY_PHONE";
+            "androidx.car.app.sample.showcase.INTENT_ACTION_PRIMARY_PHONE";
     private static final String INTENT_ACTION_SECONDARY_PHONE =
-            "androidx.car.app.samples.showcase.INTENT_ACTION_SECONDARY_PHONE";
+            "androidx.car.app.sample.showcase.INTENT_ACTION_SECONDARY_PHONE";
 
     private static final int MSG_SEND_NOTIFICATION = 1;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/OnPhoneActivity.java
similarity index 94%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/OnPhoneActivity.java
index 493eec5..c452596 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/OnPhoneActivity.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
 import android.app.Activity;
 import android.content.Intent;
@@ -23,7 +23,7 @@
 import android.view.WindowManager.LayoutParams;
 
 import androidx.annotation.Nullable;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 
 /** Displays a simple {@link Activity} on the phone to show phone/car interaction */
 public class OnPhoneActivity extends Activity {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PopToDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PopToDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PopToDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PopToDemoScreen.java
index f50ad0d..12b5c64 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PopToDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PopToDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PreSeedingFlowScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PreSeedingFlowScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PreSeedingFlowScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PreSeedingFlowScreen.java
index 52ddf5a..1acfb06 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PreSeedingFlowScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PreSeedingFlowScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/ReservationCancelledScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/ReservationCancelledScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/ReservationCancelledScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/ReservationCancelledScreen.java
index 58bdd9b..ad3d7fe 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/ReservationCancelledScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/ReservationCancelledScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationDemosScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationDemosScreen.java
similarity index 95%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationDemosScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationDemosScreen.java
index 124e1d6..8ab0795 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationDemosScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationDemosScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
@@ -25,8 +25,8 @@
 import androidx.car.app.model.ListTemplate;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
-import androidx.car.app.samples.showcase.navigation.routing.NavigationTemplateDemoScreen;
+import androidx.car.app.sample.showcase.R;
+import androidx.car.app.sample.showcase.navigation.routing.NavigationTemplateDemoScreen;
 import androidx.core.graphics.drawable.IconCompat;
 
 /** A screen showing a list of navigation demos */
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationMapOnlyScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationMapOnlyScreen.java
index 1057cee..479463f 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationMapOnlyScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationService.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationService.java
index 98c44a2..e8bcea7 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationService.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
 
-import static androidx.car.app.samples.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP;
+import static androidx.car.app.sample.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -38,8 +38,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.notification.CarAppExtender;
-import androidx.car.app.samples.showcase.DeepLinkNotificationReceiver;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.DeepLinkNotificationReceiver;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationManagerCompat;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationsDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationsDemoScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationsDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationsDemoScreen.java
index f601e68..70a4d81 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationsDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationsDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
similarity index 94%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
index 5d090be..96f97e6 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
@@ -23,7 +23,7 @@
 import androidx.car.app.model.ActionStrip;
 import androidx.car.app.model.Template;
 import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
-import androidx.car.app.samples.showcase.common.SamplePlaces;
+import androidx.car.app.sample.showcase.common.SamplePlaces;
 
 /** Creates a screen using the {@link PlaceListNavigationTemplate} */
 public final class PlaceListNavigationTemplateDemoScreen extends Screen {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/RoutePreviewDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/RoutePreviewDemoScreen.java
similarity index 76%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/RoutePreviewDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/RoutePreviewDemoScreen.java
index 77469bd..2393859 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/RoutePreviewDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/RoutePreviewDemoScreen.java
@@ -14,17 +14,20 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
 
 import static androidx.car.app.CarToast.LENGTH_LONG;
 
 import android.text.SpannableString;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.car.app.CarContext;
 import androidx.car.app.CarToast;
 import androidx.car.app.Screen;
+import androidx.car.app.annotations.ExperimentalCarApi;
 import androidx.car.app.model.Action;
+import androidx.car.app.model.CarText;
 import androidx.car.app.model.DurationSpan;
 import androidx.car.app.model.ItemList;
 import androidx.car.app.model.Row;
@@ -39,16 +42,30 @@
         super(carContext);
     }
 
+    @OptIn(markerClass = ExperimentalCarApi.class)
     @NonNull
     @Override
     public Template onGetTemplate() {
-        SpannableString firstRoute = new SpannableString("   \u00b7 Shortest route");
-        firstRoute.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
+        // Set text variants for the first route.
+        SpannableString firstRouteLongText = new SpannableString(
+                "   \u00b7 ---------------- Short" + "  " + "route " + "-------------------");
+        firstRouteLongText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
+        SpannableString firstRouteShortText = new SpannableString("   \u00b7 Short Route");
+        firstRouteShortText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
+        CarText firstRoute = new CarText.Builder(firstRouteLongText)
+                .addVariant(firstRouteShortText)
+                .build();
+
         SpannableString secondRoute = new SpannableString("   \u00b7 Less busy");
         secondRoute.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(24)), 0, 1, 0);
         SpannableString thirdRoute = new SpannableString("   \u00b7 HOV friendly");
         thirdRoute.setSpan(DurationSpan.create(TimeUnit.MINUTES.toSeconds(867)), 0, 1, 0);
 
+        // Set text variants for the navigate action text.
+        CarText navigateActionText =
+                new CarText.Builder("Continue to start navigation").addVariant("Continue to "
+                        + "route").build();
+
         return new RoutePreviewNavigationTemplate.Builder()
                 .setItemList(
                         new ItemList.Builder()
@@ -72,7 +89,7 @@
                                 .build())
                 .setNavigateAction(
                         new Action.Builder()
-                                .setTitle("Continue to route")
+                                .setTitle(navigateActionText)
                                 .setOnClickListener(this::onNavigate)
                                 .build())
                 .setTitle("Routes")
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/SurfaceRenderer.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/SurfaceRenderer.java
similarity index 99%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/SurfaceRenderer.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/SurfaceRenderer.java
index 370fd1e..1df7db8 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/SurfaceRenderer.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/SurfaceRenderer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
 
 import android.graphics.Canvas;
 import android.graphics.Color;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/ArrivedDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/ArrivedDemoScreen.java
similarity index 95%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/ArrivedDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/ArrivedDemoScreen.java
index 386cf83..8a0be87 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/ArrivedDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/ArrivedDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
 
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -28,7 +28,7 @@
 import androidx.car.app.model.Template;
 import androidx.car.app.navigation.model.MessageInfo;
 import androidx.car.app.navigation.model.NavigationTemplate;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.lifecycle.DefaultLifecycleObserver;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/JunctionImageDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/JunctionImageDemoScreen.java
similarity index 95%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/JunctionImageDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/JunctionImageDemoScreen.java
index 3281f8a..769de10 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/JunctionImageDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/JunctionImageDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
@@ -24,7 +24,7 @@
 import androidx.car.app.model.Template;
 import androidx.car.app.navigation.model.NavigationTemplate;
 import androidx.car.app.navigation.model.RoutingInfo;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.lifecycle.DefaultLifecycleObserver;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/LoadingDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/LoadingDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/LoadingDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/LoadingDemoScreen.java
index ff6b8f7..53549f6 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/LoadingDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/LoadingDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigatingDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigatingDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigatingDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigatingDemoScreen.java
index 0d1ab66..a2845e3a 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigatingDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigatingDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigationTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigationTemplateDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigationTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigationTemplateDemoScreen.java
index 30b5ca9..a3c4b9d 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigationTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigationTemplateDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/RoutingDemoModels.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/RoutingDemoModels.java
index 0941b49..9920f2d 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/RoutingDemoModels.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
 
 import static androidx.car.app.navigation.model.LaneDirection.SHAPE_NORMAL_RIGHT;
 import static androidx.car.app.navigation.model.LaneDirection.SHAPE_STRAIGHT;
@@ -38,7 +38,7 @@
 import androidx.car.app.navigation.model.Maneuver;
 import androidx.car.app.navigation.model.Step;
 import androidx.car.app.navigation.model.TravelEstimate;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 
 import java.util.TimeZone;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/GridTemplateDemoScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/GridTemplateDemoScreen.java
index 239efec..32fbab7 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/GridTemplateDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
 
 import static androidx.car.app.CarToast.LENGTH_LONG;
 import static androidx.car.app.model.Action.BACK;
@@ -36,7 +36,7 @@
 import androidx.car.app.model.GridTemplate;
 import androidx.car.app.model.ItemList;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/ListTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/ListTemplateDemoScreen.java
similarity index 79%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/ListTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/ListTemplateDemoScreen.java
index b67d75d..b1f502c 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/ListTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/ListTemplateDemoScreen.java
@@ -14,17 +14,20 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
 
 import static androidx.car.app.CarToast.LENGTH_LONG;
 import static androidx.car.app.model.Action.BACK;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.car.app.CarContext;
 import androidx.car.app.CarToast;
 import androidx.car.app.Screen;
+import androidx.car.app.annotations.ExperimentalCarApi;
 import androidx.car.app.model.Action;
 import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
 import androidx.car.app.model.ItemList;
 import androidx.car.app.model.ListTemplate;
 import androidx.car.app.model.ParkedOnlyOnClickListener;
@@ -44,6 +47,7 @@
 
     @NonNull
     @Override
+    @OptIn(markerClass = ExperimentalCarApi.class)
     public Template onGetTemplate() {
         ItemList.Builder listBuilder = new ItemList.Builder();
 
@@ -56,13 +60,22 @@
                         .build());
 
         for (int i = 2; i <= 6; ++i) {
+            // For row text, set text variants that fit best in different screen sizes.
+            String secondTextStr = "Second line of text";
+            CarText secondText =
+                    new CarText.Builder(
+                            "================= " + secondTextStr + " ================")
+                            .addVariant("--------------------- " + secondTextStr
+                                    + " ----------------------")
+                            .addVariant(secondTextStr)
+                            .build();
             final String onClickText = "Clicked row: " + i;
             listBuilder.addItem(
                     new Row.Builder()
                             .setOnClickListener(() -> onClick(onClickText))
                             .setTitle("Title " + i)
                             .addText("First line of text")
-                            .addText("Second line of text")
+                            .addText(secondText)
                             .build());
         }
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MessageTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MessageTemplateDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MessageTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MessageTemplateDemoScreen.java
index b25e010..0dc4ef6 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MessageTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MessageTemplateDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
 
 import static androidx.car.app.model.Action.BACK;
 
@@ -26,7 +26,7 @@
 import androidx.car.app.model.CarIcon;
 import androidx.car.app.model.MessageTemplate;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 
 /** A screen that demonstrates the message template. */
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MiscTemplateDemosScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MiscTemplateDemosScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MiscTemplateDemosScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MiscTemplateDemosScreen.java
index bf49fa3..77b2440 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MiscTemplateDemosScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MiscTemplateDemosScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PaneTemplateDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PaneTemplateDemoScreen.java
index f525791..c3d2f5a 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PaneTemplateDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
 
 import static androidx.car.app.CarToast.LENGTH_SHORT;
 
@@ -35,7 +35,7 @@
 import androidx.car.app.model.PaneTemplate;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PlaceListTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PlaceListTemplateDemoScreen.java
similarity index 92%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PlaceListTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PlaceListTemplateDemoScreen.java
index 1ebd95f..ff41412 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PlaceListTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PlaceListTemplateDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
 
 import androidx.annotation.NonNull;
 import androidx.car.app.CarContext;
@@ -22,7 +22,7 @@
 import androidx.car.app.model.Action;
 import androidx.car.app.model.PlaceListMapTemplate;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.common.SamplePlaces;
+import androidx.car.app.sample.showcase.common.SamplePlaces;
 
 /** Creates a screen using the {@link PlaceListMapTemplate} */
 public final class PlaceListTemplateDemoScreen extends Screen {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/SearchTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/SearchTemplateDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/SearchTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/SearchTemplateDemoScreen.java
index 85d3466..15a954b 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/SearchTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/SearchTemplateDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
 
 import static androidx.car.app.CarToast.LENGTH_LONG;
 import static androidx.car.app.CarToast.LENGTH_SHORT;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ColoredTextDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ColoredTextDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ColoredTextDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ColoredTextDemoScreen.java
index da9f030..807ba03 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ColoredTextDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ColoredTextDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
 
 import static androidx.car.app.model.Action.BACK;
 import static androidx.car.app.model.CarColor.BLUE;
@@ -31,7 +31,7 @@
 import androidx.car.app.model.ListTemplate;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.common.Utils;
+import androidx.car.app.sample.showcase.common.Utils;
 
 /** Creates a screen that demonstrate the usage of colored text in the library. */
 public final class ColoredTextDemoScreen extends Screen {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ContentProviderIconsDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ContentProviderIconsDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ContentProviderIconsDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ContentProviderIconsDemoScreen.java
index 21214d7..4a9c684 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ContentProviderIconsDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ContentProviderIconsDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
 
 import static androidx.car.app.model.Action.BACK;
 
@@ -30,7 +30,7 @@
 import androidx.car.app.model.ListTemplate;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 
 import java.io.IOException;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/DelayedFileProvider.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/DelayedFileProvider.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/DelayedFileProvider.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/DelayedFileProvider.java
index b528b28..7bb076a 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/DelayedFileProvider.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/DelayedFileProvider.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
 
 import android.content.Context;
 import android.graphics.Bitmap;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/IconsDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/IconsDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/IconsDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/IconsDemoScreen.java
index 296ff47..5ad799b 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/IconsDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/IconsDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
 
 import static androidx.car.app.model.Action.BACK;
 import static androidx.car.app.model.CarColor.GREEN;
@@ -29,7 +29,7 @@
 import androidx.car.app.model.ListTemplate;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
 import androidx.core.graphics.drawable.IconCompat;
 
 /** Creates a screen that demonstrate the usage of icons in the library. */
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/RowDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/RowDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/RowDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/RowDemoScreen.java
index 0d565d9..096ac9d 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/RowDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/RowDemoScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
 
 import static androidx.car.app.model.Action.BACK;
 import static androidx.car.app.model.CarColor.YELLOW;
@@ -29,8 +29,8 @@
 import androidx.car.app.model.ListTemplate;
 import androidx.car.app.model.Row;
 import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
-import androidx.car.app.samples.showcase.common.Utils;
+import androidx.car.app.sample.showcase.R;
+import androidx.car.app.sample.showcase.common.Utils;
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.lifecycle.DefaultLifecycleObserver;
 
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/TextAndIconsDemosScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/TextAndIconsDemosScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/TextAndIconsDemosScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/TextAndIconsDemosScreen.java
index 34e5b5a..747350f 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/TextAndIconsDemosScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/TextAndIconsDemosScreen.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
 
 import static androidx.car.app.model.Action.BACK;
 
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 0201474..c340f85 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -481,6 +481,14 @@
     method public void onClick();
   }
 
+  @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(2) public interface OnInputCompletedDelegate {
+    method public void sendInputCompleted(String, androidx.car.app.OnDoneCallback);
+  }
+
+  @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(2) public interface OnInputCompletedListener {
+    method public void onInputCompleted(String);
+  }
+
   public interface OnItemVisibilityChangedDelegate {
     method public void sendItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
   }
@@ -694,7 +702,7 @@
     method public androidx.car.app.model.CarText? getHint();
     method public int getInputType();
     method public int getKeyboardType();
-    method public androidx.car.app.model.signin.OnInputCompletedDelegate getOnInputCompletedDelegate();
+    method public androidx.car.app.model.OnInputCompletedDelegate getOnInputCompletedDelegate();
     method public boolean isShowKeyboardByDefault();
     field public static final int INPUT_TYPE_DEFAULT = 1; // 0x1
     field public static final int INPUT_TYPE_PASSWORD = 2; // 0x2
@@ -705,7 +713,7 @@
   }
 
   public static final class InputSignInMethod.Builder {
-    ctor public InputSignInMethod.Builder(androidx.car.app.model.signin.InputSignInMethod.OnInputCompletedListener);
+    ctor public InputSignInMethod.Builder(androidx.car.app.model.OnInputCompletedListener);
     method public androidx.car.app.model.signin.InputSignInMethod build();
     method public androidx.car.app.model.signin.InputSignInMethod.Builder setDefaultValue(String);
     method public androidx.car.app.model.signin.InputSignInMethod.Builder setErrorMessage(CharSequence);
@@ -715,14 +723,6 @@
     method public androidx.car.app.model.signin.InputSignInMethod.Builder setShowKeyboardByDefault(boolean);
   }
 
-  public static interface InputSignInMethod.OnInputCompletedListener {
-    method public void onInputCompleted(String);
-  }
-
-  @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(2) public interface OnInputCompletedDelegate {
-    method public void sendInputCompleted(String, androidx.car.app.OnDoneCallback);
-  }
-
   @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(2) public final class PinSignInMethod implements androidx.car.app.model.signin.SignInTemplate.SignInMethod {
     method public String getPin();
   }
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnInputCompletedListener.aidl
similarity index 95%
rename from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/IOnInputCompletedListener.aidl
index e022dc3..632f921 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnInputCompletedListener.aidl
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.model.signin;
+package androidx.car.app.model;
 
 import androidx.car.app.IOnDoneCallback;
 
diff --git a/car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegate.java b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegate.java
similarity index 97%
rename from car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegate.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegate.java
index 73bf42a..4fcb34d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegate.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.model.signin;
+package androidx.car.app.model;
 
 import android.annotation.SuppressLint;
 
diff --git a/car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegateImpl.java
similarity index 93%
rename from car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegateImpl.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegateImpl.java
index d9d5141..50b85ad 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegateImpl.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.car.app.model.signin;
+package androidx.car.app.model;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 
@@ -30,7 +30,6 @@
 import androidx.car.app.IOnDoneCallback;
 import androidx.car.app.OnDoneCallback;
 import androidx.car.app.annotations.ExperimentalCarApi;
-import androidx.car.app.model.signin.InputSignInMethod.OnInputCompletedListener;
 import androidx.car.app.utils.RemoteUtils;
 
 /**
@@ -57,10 +56,11 @@
         }
     }
 
+    /** Creates an instance of {@link OnInputCompletedDelegateImpl}. */
     // This mirrors the AIDL class and is not supposed to support an executor as an input.
     @SuppressLint("ExecutorRegistration")
     @NonNull
-    static OnInputCompletedDelegate create(@NonNull OnInputCompletedListener listener) {
+    public static OnInputCompletedDelegate create(@NonNull OnInputCompletedListener listener) {
         return new OnInputCompletedDelegateImpl(requireNonNull(listener));
     }
 
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedListener.java b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedListener.java
new file mode 100644
index 0000000..7435ac9
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedListener.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.model;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.RequiresCarApi;
+
+/** A listener for handling text input completion event. */
+@ExperimentalCarApi
+@RequiresCarApi(2)
+public interface OnInputCompletedListener {
+    /**
+     * Notifies when the user finished entering text in an input box.
+     *
+     * <p>This event is sent when the user finishes typing in the keyboard and pressed enter.
+     * If the user simply stops typing and closes the keyboard, this event will not be sent.
+     *
+     * @param text the text that was entered, or an empty string if no text was typed.
+     */
+    void onInputCompleted(@NonNull String text);
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/signin/InputSignInMethod.java b/car/app/app/src/main/java/androidx/car/app/model/signin/InputSignInMethod.java
index 3b63945..aea8f80 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/signin/InputSignInMethod.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/signin/InputSignInMethod.java
@@ -31,6 +31,9 @@
 import androidx.car.app.annotations.ExperimentalCarApi;
 import androidx.car.app.annotations.RequiresCarApi;
 import androidx.car.app.model.CarText;
+import androidx.car.app.model.OnInputCompletedDelegate;
+import androidx.car.app.model.OnInputCompletedDelegateImpl;
+import androidx.car.app.model.OnInputCompletedListener;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -45,19 +48,6 @@
 @ExperimentalCarApi
 @RequiresCarApi(2)
 public final class InputSignInMethod implements SignInTemplate.SignInMethod {
-    /** A listener for handling text input completion event. */
-    public interface OnInputCompletedListener {
-        /**
-         * Notifies when the user finished entering text in an input box.
-         *
-         * <p>This event is sent when the user finishes typing in the keyboard and pressed enter.
-         * If the user simply stops typing and closes the keyboard, this event will not be sent.
-         *
-         * @param text the text that was entered, or an empty string if no text was typed.
-         */
-        void onInputCompleted(@NonNull String text);
-    }
-
     /**
      * The type of input represented by the {@link InputSignInMethod} instance.
      *
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
index 47ff35a..a7a2d10 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
@@ -208,7 +208,8 @@
          * Sets the {@link TravelEstimate} to the final destination.
          *
          * @throws IllegalArgumentException if the {@link TravelEstimate}'s remaining time is
-         *                                  less than zero
+         *                                  {@link TravelEstimate#REMAINING_TIME_UNKNOWN} or less
+         *                                  than zero
          * @throws NullPointerException     if {@code destinationTravelEstimate} is {@code null}
          */
         @NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
index 42b162c..558bc8d 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
@@ -213,6 +213,10 @@
          *
          * <p>If not set, {@link #REMAINING_TIME_UNKNOWN} will be used.
          *
+         * <p>Note that {@link #REMAINING_TIME_UNKNOWN} may not be supported depending on where the
+         * {@link TravelEstimate} is used. See the documentation of where {@link TravelEstimate}
+         * is used for any restrictions that might apply.
+         *
          * @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value
          *                                  but not {@link #REMAINING_TIME_UNKNOWN}
          */
diff --git a/car/app/app/src/test/java/androidx/car/app/model/signin/InputSignInMethodTest.java b/car/app/app/src/test/java/androidx/car/app/model/signin/InputSignInMethodTest.java
index a2f67f5..eeeaa4e 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/signin/InputSignInMethodTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/signin/InputSignInMethodTest.java
@@ -27,6 +27,8 @@
 import static org.mockito.Mockito.verify;
 
 import androidx.car.app.OnDoneCallback;
+import androidx.car.app.model.OnInputCompletedDelegate;
+import androidx.car.app.model.OnInputCompletedListener;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -45,7 +47,7 @@
     public final MockitoRule mockito = MockitoJUnit.rule();
 
     @Mock
-    InputSignInMethod.OnInputCompletedListener mListener;
+    OnInputCompletedListener mListener;
 
     @Test
     public void create_defaultValues() {
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
index 78a236a..bc0c712 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
@@ -23,7 +23,6 @@
 import junit.framework.TestCase.assertEquals
 import junit.framework.TestCase.assertFalse
 import junit.framework.TestCase.assertTrue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
@@ -185,7 +184,6 @@
             }
         }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun animateToWithInterruption() {
         runBlocking {
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt
index 72c6567..55d263b 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt
@@ -21,7 +21,6 @@
 import junit.framework.TestCase.assertEquals
 import junit.framework.TestCase.assertFalse
 import junit.framework.TestCase.assertTrue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
@@ -29,7 +28,6 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(JUnit4::class)
 class SuspendAnimationTest {
     @Test
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringChainDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringChainDemo.kt
index d03313b..8ba6ba8 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringChainDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringChainDemo.kt
@@ -16,15 +16,23 @@
 
 package androidx.compose.animation.demos
 
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntAsState
 import androidx.compose.animation.core.animateOffsetAsState
+import androidx.compose.animation.core.spring
 import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Icon
 import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
@@ -35,8 +43,10 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.consumeAllChanges
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
 
@@ -55,34 +65,56 @@
             modifier = Modifier.align(Alignment.Center),
             text = "Since we are here, why not drag me around?"
         )
-        val size = pastelAwakening.size
+        val size = vibrantColors.size
         val followers = remember { Array<State<Offset>>(size) { mutableStateOf(Offset.Zero) } }
         for (i in 0 until size) {
             // Each follower on the spring chain uses the previous follower's position as target
             followers[i] = animateOffsetAsState(if (i == 0) leader else followers[i - 1].value)
         }
 
+        var expanded by remember { mutableStateOf(false) }
+        // Put space between followers when expanded
+        val spacing by animateIntAsState(if (expanded) -300 else 0, spring(dampingRatio = 0.7f))
+
         // Followers stacked in reverse orders
         for (i in followers.size - 1 downTo 0) {
             Box(
                 Modifier
                     .offset { followers[i].value.round() }
-                    .size(80.dp)
-                    .background(pastelAwakening[i], CircleShape)
+                    .offset { IntOffset(0, spacing * (i + 1)) }
+                    .size(circleSize)
+                    .background(vibrantColors[i], CircleShape)
             )
         }
+
         // Leader
         Box(
-            Modifier.offset { leader.round() }.size(80.dp)
-                .background(Color(0xFFfffbd0), CircleShape)
-        )
+            Modifier.offset { leader.round() }.size(circleSize)
+                .clickable(
+                    indication = null,
+                    interactionSource = remember { MutableInteractionSource() }
+                ) { expanded = !expanded }
+                .background(Color(0xFFfff8ad), CircleShape)
+        ) {
+            // Rotate icon when expanded / collapsed
+            val rotation by animateFloatAsState(if (expanded) 180f else 0f)
+            Icon(
+                Icons.Filled.KeyboardArrowDown,
+                contentDescription = "Expand or Collapse",
+                modifier = Modifier.size(30.dp).align(Alignment.Center)
+                    .graphicsLayer { this.rotationZ = rotation },
+                tint = Color.Gray
+            )
+        }
     }
 }
 
-private val pastelAwakening = listOf(
-    Color(0xffdfdeff),
-    Color(0xffffe0f5),
-    Color(0xffffefd8),
-    Color(0xffe6ffd0),
-    Color(0xffd9f6ff)
+val circleSize = 60.dp
+
+private val vibrantColors = listOf(
+    Color(0xffbfbdff),
+    Color(0xffffc7ed),
+    Color(0xffffdcab),
+    Color(0xffd5ffb0),
+    Color(0xffbaefff)
 )
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/build.gradle b/compose/compiler/compiler-hosted/integration-tests/build.gradle
index 2657faf..91c88cc 100644
--- a/compose/compiler/compiler-hosted/integration-tests/build.gradle
+++ b/compose/compiler/compiler-hosted/integration-tests/build.gradle
@@ -18,8 +18,6 @@
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
 import androidx.build.Publish
 
 plugins {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
index 49f83020..c091eea 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
@@ -273,6 +273,7 @@
 
            $text
 
+            fun used(x: Any?) {}
         """,
             dumpClasses
         )
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
index 9fb039e..64dc648 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
@@ -87,6 +87,8 @@
            import androidx.compose.runtime.*
 
            $src
+
+            fun used(x: Any?) {}
         """,
             fileName, dumpClasses
         )
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
index 6355b8b..c9657c0 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
@@ -23,7 +23,8 @@
         @Language("kotlin")
         source: String,
         expectedTransformed: String,
-        dumpTree: Boolean = false
+        dumpTree: Boolean = false,
+        compilation: Compilation = JvmCompilation()
     ) = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
@@ -37,6 +38,7 @@
             import androidx.compose.runtime.Composable
 
             inline class InlineClass(val value: Int)
+            fun used(x: Any?) {}
 
             @Composable fun A() {}
             @Composable fun A(x: Int) { }
@@ -56,6 +58,7 @@
             var b = 2
             var c = 3
         """.trimIndent(),
-        dumpTree = dumpTree
+        dumpTree = dumpTree,
+        compilation = compilation
     )
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index 81efc17..929bc3c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -69,6 +69,7 @@
 abstract class ComposeIrTransformTest : AbstractIrTransformTest() {
     open val liveLiteralsEnabled get() = false
     open val sourceInformationEnabled get() = true
+
     private val extension = ComposeIrGenerationExtension(
         liveLiteralsEnabled,
         sourceInformationEnabled,
@@ -77,6 +78,7 @@
     // Some tests require the plugin context in order to perform assertions, for example, a
     // context is required to determine the stability of a type using the StabilityInferencer.
     var pluginContext: IrPluginContext? = null
+
     override fun postProcessingStep(
         module: IrModuleFragment,
         generatorContext: GeneratorContext,
@@ -163,13 +165,19 @@
         expectedTransformed: String,
         extra: String = "",
         validator: (element: IrElement) -> Unit = { },
-        dumpTree: Boolean = false
+        dumpTree: Boolean = false,
+        compilation: Compilation = JvmCompilation()
     ) {
+        if (!compilation.enabled) {
+            // todo indicate ignore?
+            return
+        }
+
         val files = listOf(
             sourceFile("Test.kt", source.replace('%', '$')),
             sourceFile("Extra.kt", extra.replace('%', '$'))
         )
-        val irModule = generateIrModuleWithJvmResolve(files)
+        val irModule = compilation.compile(files)
         val keySet = mutableListOf<Int>()
         fun IrElement.validate(): IrElement = this.also { validator(it) }
         val actualTransformed = irModule
@@ -344,116 +352,6 @@
         return result
     }
 
-    protected fun generateIrModuleWithJvmResolve(files: List<KtFile>): IrModuleFragment {
-        val classPath = createClasspath() + additionalPaths
-        val configuration = newConfiguration()
-        configuration.addJvmClasspathRoots(classPath)
-        configuration.put(JVMConfigurationKeys.IR, true)
-        configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
-
-        val environment = KotlinCoreEnvironment.createForTests(
-            myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
-        ).also { setupEnvironment(it) }
-
-        val mangler = JvmManglerDesc(null)
-
-        val psi2ir = Psi2IrTranslator(
-            environment.configuration.languageVersionSettings,
-            Psi2IrConfiguration(ignoreErrors = false)
-        )
-        val symbolTable = SymbolTable(
-            JvmIdSignatureDescriptor(mangler),
-            IrFactoryImpl,
-            JvmNameProvider
-        )
-
-        val analysisResult = JvmResolveUtil.analyze(files, environment)
-        if (!psi2ir.configuration.ignoreErrors) {
-            analysisResult.throwIfError()
-            AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext)
-        }
-        val extensions = JvmGeneratorExtensions()
-        val generatorContext = psi2ir.createGeneratorContext(
-            analysisResult.moduleDescriptor,
-            analysisResult.bindingContext,
-            symbolTable,
-            extensions = extensions
-        )
-        val stubGenerator = DeclarationStubGenerator(
-            generatorContext.moduleDescriptor,
-            generatorContext.symbolTable,
-            generatorContext.irBuiltIns.languageVersionSettings,
-            extensions
-        )
-        val functionFactory = IrFunctionFactory(
-            generatorContext.irBuiltIns,
-            generatorContext.symbolTable
-        )
-        val frontEndContext = object : TranslationPluginContext {
-            override val moduleDescriptor: ModuleDescriptor
-                get() = generatorContext.moduleDescriptor
-            override val bindingContext: BindingContext
-                get() = generatorContext.bindingContext
-            override val symbolTable: ReferenceSymbolTable
-                get() = symbolTable
-            override val typeTranslator: TypeTranslator
-                get() = generatorContext.typeTranslator
-            override val irBuiltIns: IrBuiltIns
-                get() = generatorContext.irBuiltIns
-        }
-        generatorContext.irBuiltIns.functionFactory = functionFactory
-        val irLinker = JvmIrLinker(
-            generatorContext.moduleDescriptor,
-            EmptyLoggingContext,
-            generatorContext.irBuiltIns,
-            generatorContext.symbolTable,
-            functionFactory,
-            frontEndContext,
-            stubGenerator,
-            mangler
-        )
-
-        generatorContext.moduleDescriptor.allDependencyModules.map {
-            val capability = it.getCapability(KlibModuleOrigin.CAPABILITY)
-            val kotlinLibrary = (capability as? DeserializedKlibModuleOrigin)?.library
-            irLinker.deserializeIrModuleHeader(it, kotlinLibrary)
-        }
-
-        val irProviders = listOf(irLinker)
-
-        val symbols = BuiltinSymbolsBase(
-            generatorContext.irBuiltIns,
-            generatorContext.moduleDescriptor.builtIns,
-            generatorContext.symbolTable.lazyWrapper
-        )
-
-        ExternalDependenciesGenerator(
-            generatorContext.symbolTable,
-            irProviders,
-            generatorContext.languageVersionSettings
-        ).generateUnboundSymbolsAsDependencies()
-
-        psi2ir.addPostprocessingStep { module ->
-            val old = stubGenerator.unboundSymbolGeneration
-            try {
-                stubGenerator.unboundSymbolGeneration = true
-                postProcessingStep(module, generatorContext, irLinker, symbols)
-            } finally {
-                stubGenerator.unboundSymbolGeneration = old
-            }
-        }
-
-        val irModuleFragment = psi2ir.generateModuleFragment(
-            generatorContext,
-            files,
-            irProviders,
-            IrGenerationExtension.getInstances(myEnvironment!!.project),
-            expectDescriptorToSymbol = null
-        )
-        irLinker.postProcess()
-        return irModuleFragment
-    }
-
     fun facadeClassGenerator(
         generatorContext: GeneratorContext,
         source: DeserializedContainerSource
@@ -467,4 +365,124 @@
             it.createParameterDeclarations()
         }
     }
+
+    inner class JvmCompilation : Compilation {
+        override val enabled: Boolean = true
+
+        override fun compile(files: List<KtFile>): IrModuleFragment {
+            val classPath = createClasspath() + additionalPaths
+            val configuration = newConfiguration()
+            configuration.addJvmClasspathRoots(classPath)
+            configuration.put(JVMConfigurationKeys.IR, true)
+            configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
+
+            val environment = KotlinCoreEnvironment.createForTests(
+                myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
+            ).also { setupEnvironment(it) }
+
+            val mangler = JvmManglerDesc(null)
+
+            val psi2ir = Psi2IrTranslator(
+                environment.configuration.languageVersionSettings,
+                Psi2IrConfiguration(ignoreErrors = false)
+            )
+            val symbolTable = SymbolTable(
+                JvmIdSignatureDescriptor(mangler),
+                IrFactoryImpl,
+                JvmNameProvider
+            )
+
+            val analysisResult = JvmResolveUtil.analyze(files, environment)
+            if (!psi2ir.configuration.ignoreErrors) {
+                analysisResult.throwIfError()
+                AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext)
+            }
+            val extensions = JvmGeneratorExtensions()
+            val generatorContext = psi2ir.createGeneratorContext(
+                analysisResult.moduleDescriptor,
+                analysisResult.bindingContext,
+                symbolTable,
+                extensions = extensions
+            )
+            val stubGenerator = DeclarationStubGenerator(
+                generatorContext.moduleDescriptor,
+                generatorContext.symbolTable,
+                generatorContext.irBuiltIns.languageVersionSettings,
+                extensions
+            )
+            val functionFactory = IrFunctionFactory(
+                generatorContext.irBuiltIns,
+                generatorContext.symbolTable
+            )
+            val frontEndContext = object : TranslationPluginContext {
+                override val moduleDescriptor: ModuleDescriptor
+                    get() = generatorContext.moduleDescriptor
+                override val bindingContext: BindingContext
+                    get() = generatorContext.bindingContext
+                override val symbolTable: ReferenceSymbolTable
+                    get() = symbolTable
+                override val typeTranslator: TypeTranslator
+                    get() = generatorContext.typeTranslator
+                override val irBuiltIns: IrBuiltIns
+                    get() = generatorContext.irBuiltIns
+            }
+            generatorContext.irBuiltIns.functionFactory = functionFactory
+            val irLinker = JvmIrLinker(
+                generatorContext.moduleDescriptor,
+                EmptyLoggingContext,
+                generatorContext.irBuiltIns,
+                generatorContext.symbolTable,
+                functionFactory,
+                frontEndContext,
+                stubGenerator,
+                mangler
+            )
+
+            generatorContext.moduleDescriptor.allDependencyModules.map {
+                val capability = it.getCapability(KlibModuleOrigin.CAPABILITY)
+                val kotlinLibrary = (capability as? DeserializedKlibModuleOrigin)?.library
+                irLinker.deserializeIrModuleHeader(it, kotlinLibrary)
+            }
+
+            val irProviders = listOf(irLinker)
+
+            val symbols = BuiltinSymbolsBase(
+                generatorContext.irBuiltIns,
+                generatorContext.moduleDescriptor.builtIns,
+                generatorContext.symbolTable.lazyWrapper
+            )
+
+            ExternalDependenciesGenerator(
+                generatorContext.symbolTable,
+                irProviders,
+                generatorContext.languageVersionSettings
+            ).generateUnboundSymbolsAsDependencies()
+
+            psi2ir.addPostprocessingStep { module ->
+                val old = stubGenerator.unboundSymbolGeneration
+                try {
+                    stubGenerator.unboundSymbolGeneration = true
+                    postProcessingStep(module, generatorContext, irLinker, symbols)
+                } finally {
+                    stubGenerator.unboundSymbolGeneration = old
+                }
+            }
+
+            val irModuleFragment = psi2ir.generateModuleFragment(
+                generatorContext,
+                files,
+                irProviders,
+                IrGenerationExtension.getInstances(myEnvironment!!.project),
+                expectDescriptorToSymbol = null
+            )
+            irLinker.postProcess()
+            return irModuleFragment
+        }
+    }
+
+    // This interface enables different Compilation variants for compiler tests
+    interface Compilation {
+        val enabled: Boolean
+        fun compile(files: List<KtFile>): IrModuleFragment
+    }
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
index 7562046..84aba6a 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
@@ -512,12 +512,14 @@
             class UnstableDelegateProp {
                 var p1 by UnstableDelegate()
             }
+            fun used(x: Any?) {}
         """,
         """
             import a.*
             import androidx.compose.runtime.Composable
 
             @Composable fun A(y: Any) {
+                used(y)
                 A(EmptyClass())
                 A(SingleStableValInt(123))
                 A(SingleStableVal(StableClass()))
@@ -541,6 +543,7 @@
             @Composable
             fun A(y: Any, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(A)<A(Empt...>,<A(Sing...>,<A(Sing...>,<A(Sing...>,<A(Sing...>,<A(Sing...>,<A(Sing...>,<A(Doub...>,<A(Doub...>,<A(Doub...>,<A(Doub...>,<A(X(li...>,<A(X(li...>,<A(NonB...>,<A(NonB...>,<A(Stab...>,<A(Unst...>:Test.kt")
+              used(y)
               A(EmptyClass(), %composer, EmptyClass.%stable)
               A(SingleStableValInt(123), %composer, SingleStableValInt.%stable)
               A(SingleStableVal(StableClass()), %composer, SingleStableVal.%stable)
@@ -609,6 +612,7 @@
                     get() { TODO() }
                     set(value) { }
             }
+            fun used(x: Any?) {}
         """,
         """
             import a.*
@@ -622,6 +626,7 @@
                 var p1 by UnstableDelegate()
             }
             @Composable fun A(y: Any) {
+                used(y)
                 A(X(listOf(StableClass())))
                 A(StableDelegateProp())
                 A(UnstableDelegateProp())
@@ -657,6 +662,7 @@
             @Composable
             fun A(y: Any, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(A)<A(X(li...>,<A(Stab...>,<A(Unst...>:Test.kt")
+              used(y)
               A(X(listOf(StableClass())), %composer, 0b1000)
               A(StableDelegateProp(), %composer, 0)
               A(UnstableDelegateProp(), %composer, UnstableDelegate.%stable)
@@ -675,13 +681,14 @@
               fun make(): T = error("")
             }
             class Foo
+            fun used(x: Any?) {}
         """,
         """
-            import a.Wrapper
-            import a.Foo
+            import a.*
             import androidx.compose.runtime.Composable
 
             @Composable fun A(y: Any) {
+                used(y)
                 A(Wrapper(Foo()))
             }
         """,
@@ -689,6 +696,7 @@
             @Composable
             fun A(y: Any, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(A)<A(Wrap...>:Test.kt")
+              used(y)
               A(Wrapper(Foo()), %composer, Wrapper.%stable)
               %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
                 A(y, %composer, %changed or 0b0001)
@@ -742,10 +750,11 @@
             @Composable
             fun <T> X(items: List<T>, itemContent: Function3<T, Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(X)P(1)*<itemCo...>:Test.kt")
+              val %dirty = %changed
               val tmp0_iterator = items.iterator()
               while (tmp0_iterator.hasNext()) {
                 val item = tmp0_iterator.next()
-                itemContent(item, %composer, 0b01110000 and %changed)
+                itemContent(item, %composer, 0b01110000 and %dirty)
               }
               %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
                 X(items, itemContent, %composer, %changed or 0b0001)
@@ -801,9 +810,12 @@
 
             class Foo
             @Composable fun A(y: Int, x: Any) {
+                used(y)
                 B(x)
             }
-            @Composable fun B(x: Any) {}
+            @Composable fun B(x: Any) {
+                used(x)
+            }
         """,
         """
             @StabilityInferred(parameters = 0)
@@ -813,6 +825,7 @@
             @Composable
             fun A(y: Int, x: Any, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(A)P(1)<B(x)>:Test.kt")
+              used(y)
               B(x, %composer, 0b1000)
               %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
                 A(y, x, %composer, %changed or 0b0001)
@@ -821,6 +834,7 @@
             @Composable
             fun B(x: Any, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(B):Test.kt")
+              used(x)
               %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
                 B(x, %composer, %changed or 0b0001)
               }
@@ -835,9 +849,12 @@
 
             class Foo(var bar: Int = 0)
             @Composable fun A(y: Int, x: Foo) {
+                used(y)
                 B(x)
             }
-            @Composable fun B(x: Any) {}
+            @Composable fun B(x: Any) {
+                used(x)
+            }
         """,
         """
             @StabilityInferred(parameters = 0)
@@ -847,6 +864,7 @@
             @Composable
             fun A(y: Int, x: Foo, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(A)P(1)<B(x)>:Test.kt")
+              used(y)
               B(x, %composer, 0b1000)
               %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
                 A(y, x, %composer, %changed or 0b0001)
@@ -855,6 +873,7 @@
             @Composable
             fun B(x: Any, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(B):Test.kt")
+              used(x)
               %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
                 B(x, %composer, %changed or 0b0001)
               }
@@ -884,7 +903,7 @@
         val files = listOf(
             sourceFile("Test.kt", source.replace('%', '$'))
         )
-        val irModule = generateIrModuleWithJvmResolve(files)
+        val irModule = JvmCompilation().compile(files)
         val irClass = irModule.files.last().declarations.first() as IrClass
         val classStability = StabilityInferencer(pluginContext!!).stabilityOf(irClass.defaultType)
 
@@ -1006,7 +1025,7 @@
         val files = listOf(
             sourceFile("Test.kt", source.replace('%', '$'))
         )
-        return generateIrModuleWithJvmResolve(files)
+        return JvmCompilation().compile(files)
     }
 
     private fun assertTransform(
@@ -1018,7 +1037,10 @@
     ) = verifyComposeIrTransform(
         checked,
         expectedTransformed,
-        unchecked,
+        """
+            $unchecked
+            fun used(x: Any?) {}
+        """,
         dumpTree = dumpTree
     )
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
index d93c8be..e36dde9 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
@@ -97,6 +97,33 @@
     }
 
     @Test
+    fun testStrangeReceiverIssue(): Unit = codegen(
+        """
+        import androidx.compose.runtime.ExplicitGroupsComposable
+        import androidx.compose.runtime.NonRestartableComposable
+        class Foo
+
+        @Composable
+        @ExplicitGroupsComposable
+        fun A(foo: Foo) {
+            foo.b()
+        }
+
+        @Composable
+        @ExplicitGroupsComposable
+        inline fun Foo.b(label: String = "") {
+            c(this, label)
+        }
+
+        @Composable
+        @ExplicitGroupsComposable
+        inline fun c(foo: Foo, label: String) {
+            used(label)
+        }
+        """
+    )
+
+    @Test
     fun testInterfaceMethodWithComposableParameter(): Unit = validateBytecode(
         """
             @Composable
@@ -139,7 +166,16 @@
             f: Float,
             g: Long,
             h: Double
-        ) {}
+        ) {
+            used(a)
+            used(b)
+            used(c)
+            used(d)
+            used(e)
+            used(f)
+            used(g)
+            used(h)
+        }
         """
     ) {
         assert(it.contains("INVOKEINTERFACE androidx/compose/runtime/Composer.changed (Z)Z"))
@@ -159,7 +195,9 @@
         import androidx.compose.runtime.Stable
 
         @Stable class Bar
-        @Composable fun Foo(a: Bar) {}
+        @Composable fun Foo(a: Bar) {
+            used(a)
+        }
         """
     ) {
         assert(!it.contains("INVOKEINTERFACE androidx/compose/runtime/Composer.changed (Z)Z"))
@@ -177,7 +215,9 @@
     fun testInlineClassChangedCalls(): Unit = validateBytecode(
         """
         inline class Bar(val value: Int)
-        @Composable fun Foo(a: Bar) {}
+        @Composable fun Foo(a: Bar) {
+            used(a)
+        }
         """
     ) {
         assert(!it.contains("INVOKESTATIC Bar.box-impl (I)LBar;"))
@@ -191,7 +231,9 @@
     fun testNullableInlineClassChangedCalls(): Unit = validateBytecode(
         """
         inline class Bar(val value: Int)
-        @Composable fun Foo(a: Bar?) {}
+        @Composable fun Foo(a: Bar?) {
+            used(a)
+        }
         """
     ) {
         val testClass = it.split("public final class ").single { it.startsWith("test/TestKt") }
@@ -1081,11 +1123,9 @@
               public final static Wat(Landroidx/compose/runtime/Composer;I)V
               public final static Foo(ILandroidx/compose/runtime/Composer;I)V
               private final static Foo%goo(Landroidx/compose/runtime/Composer;I)V
-              public final static synthetic access%Foo%goo(Landroidx/compose/runtime/Composer;I)V
               final static INNERCLASS TestKt%Wat%1 null null
               public final static INNERCLASS TestKt%Foo%Bar null Bar
               final static INNERCLASS TestKt%Foo%1 null null
-              final static INNERCLASS TestKt%Foo%goo%1 null null
             }
             final class TestKt%Wat%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
               <init>(I)V
@@ -1099,17 +1139,6 @@
               public <init>()V
               public final baz(Landroidx/compose/runtime/Composer;I)V
               OUTERCLASS TestKt Foo (ILandroidx/compose/runtime/Composer;I)V
-              final static INNERCLASS TestKt%Foo%Bar%baz%1 null null
-              public final static INNERCLASS TestKt%Foo%Bar null Bar
-            }
-            final class TestKt%Foo%Bar%baz%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
-              <init>(LTestKt%Foo%Bar;I)V
-              public final invoke(Landroidx/compose/runtime/Composer;I)V
-              public synthetic bridge invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
-              final synthetic LTestKt%Foo%Bar; %tmp0_rcvr
-              final synthetic I %%changed
-              OUTERCLASS TestKt%Foo%Bar baz (Landroidx/compose/runtime/Composer;I)V
-              final static INNERCLASS TestKt%Foo%Bar%baz%1 null null
               public final static INNERCLASS TestKt%Foo%Bar null Bar
             }
             final class TestKt%Foo%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
@@ -1121,14 +1150,6 @@
               OUTERCLASS TestKt Foo (ILandroidx/compose/runtime/Composer;I)V
               final static INNERCLASS TestKt%Foo%1 null null
             }
-            final class TestKt%Foo%goo%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
-              <init>(I)V
-              public final invoke(Landroidx/compose/runtime/Composer;I)V
-              public synthetic bridge invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
-              final synthetic I %%changed
-              OUTERCLASS TestKt Foo%goo (Landroidx/compose/runtime/Composer;I)V
-              final static INNERCLASS TestKt%Foo%goo%1 null null
-            }
         """
     )
 
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index a1bb13c..dd98c75 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -47,7 +47,10 @@
             $source
         """.trimIndent(),
         expectedTransformed,
-        "",
+        """
+            package test
+            fun used(x: Any?) {}
+        """,
         validator,
         dumpTree
     )
@@ -349,7 +352,9 @@
                 }
 
                 @Composable
-                fun Leaf(text: String) { }
+                fun Leaf(text: String) {
+                    used(text)
+                }
 
                 @Composable
                 fun Test(value: Int) {
@@ -385,6 +390,7 @@
                     %dirty = %dirty or if (%composer.changed(text)) 0b0100 else 0b0010
                   }
                   if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                    used(text)
                   } else {
                     %composer.skipToGroupEnd()
                   }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index 5a3ba82..d7f5794 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -2430,6 +2430,7 @@
         """
             @Composable
             fun Test(value: InlineClass) {
+                used(value)
                 A()
             }
         """,
@@ -2442,6 +2443,7 @@
                 %dirty = %dirty or if (%composer.changed(value.value)) 0b0100 else 0b0010
               }
               if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                used(value)
                 A(%composer, 0)
               } else {
                 %composer.skipToGroupEnd()
@@ -2456,30 +2458,150 @@
     @Test
     fun testParameterOrderInformation(): Unit = controlFlow(
         """
-            @Composable fun Test01(p0: Int, p1: Int, p2: Int, p3: Int) { }
-            @Composable fun Test02(p0: Int, p1: Int, p3: Int, p2: Int) { }
-            @Composable fun Test03(p0: Int, p2: Int, p1: Int, p3: Int) { }
-            @Composable fun Test04(p0: Int, p2: Int, p3: Int, p1: Int) { }
-            @Composable fun Test05(p0: Int, p3: Int, p1: Int, p2: Int) { }
-            @Composable fun Test06(p0: Int, p3: Int, p2: Int, p1: Int) { }
-            @Composable fun Test07(p1: Int, p0: Int, p2: Int, p3: Int) { }
-            @Composable fun Test08(p1: Int, p0: Int, p3: Int, p2: Int) { }
-            @Composable fun Test09(p1: Int, p2: Int, p0: Int, p3: Int) { }
-            @Composable fun Test00(p1: Int, p2: Int, p3: Int, p0: Int) { }
-            @Composable fun Test11(p1: Int, p3: Int, p0: Int, p2: Int) { }
-            @Composable fun Test12(p1: Int, p3: Int, p2: Int, p0: Int) { }
-            @Composable fun Test13(p2: Int, p0: Int, p1: Int, p3: Int) { }
-            @Composable fun Test14(p2: Int, p0: Int, p3: Int, p1: Int) { }
-            @Composable fun Test15(p2: Int, p1: Int, p0: Int, p3: Int) { }
-            @Composable fun Test16(p2: Int, p1: Int, p3: Int, p0: Int) { }
-            @Composable fun Test17(p2: Int, p3: Int, p0: Int, p1: Int) { }
-            @Composable fun Test18(p2: Int, p3: Int, p1: Int, p0: Int) { }
-            @Composable fun Test19(p3: Int, p0: Int, p1: Int, p2: Int) { }
-            @Composable fun Test20(p3: Int, p0: Int, p2: Int, p1: Int) { }
-            @Composable fun Test21(p3: Int, p1: Int, p0: Int, p2: Int) { }
-            @Composable fun Test22(p3: Int, p1: Int, p2: Int, p0: Int) { }
-            @Composable fun Test23(p3: Int, p2: Int, p0: Int, p1: Int) { }
-            @Composable fun Test24(p3: Int, p2: Int, p1: Int, p0: Int) { }
+            @Composable fun Test01(p0: Int, p1: Int, p2: Int, p3: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test02(p0: Int, p1: Int, p3: Int, p2: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test03(p0: Int, p2: Int, p1: Int, p3: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test04(p0: Int, p2: Int, p3: Int, p1: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test05(p0: Int, p3: Int, p1: Int, p2: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test06(p0: Int, p3: Int, p2: Int, p1: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test07(p1: Int, p0: Int, p2: Int, p3: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test08(p1: Int, p0: Int, p3: Int, p2: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test09(p1: Int, p2: Int, p0: Int, p3: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test00(p1: Int, p2: Int, p3: Int, p0: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test11(p1: Int, p3: Int, p0: Int, p2: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test12(p1: Int, p3: Int, p2: Int, p0: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test13(p2: Int, p0: Int, p1: Int, p3: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test14(p2: Int, p0: Int, p3: Int, p1: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test15(p2: Int, p1: Int, p0: Int, p3: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test16(p2: Int, p1: Int, p3: Int, p0: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test17(p2: Int, p3: Int, p0: Int, p1: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test18(p2: Int, p3: Int, p1: Int, p0: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test19(p3: Int, p0: Int, p1: Int, p2: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test20(p3: Int, p0: Int, p2: Int, p1: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test21(p3: Int, p1: Int, p0: Int, p2: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test22(p3: Int, p1: Int, p2: Int, p0: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test23(p3: Int, p2: Int, p0: Int, p1: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
+            @Composable fun Test24(p3: Int, p2: Int, p1: Int, p0: Int) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
+            }
         """,
         """
             @Composable
@@ -2499,6 +2621,10 @@
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2523,6 +2649,10 @@
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2547,6 +2677,10 @@
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2571,6 +2705,10 @@
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2595,6 +2733,10 @@
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2619,6 +2761,10 @@
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2643,6 +2789,10 @@
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2667,6 +2817,10 @@
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2691,6 +2845,10 @@
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2715,6 +2873,10 @@
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2739,6 +2901,10 @@
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2763,6 +2929,10 @@
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2787,6 +2957,10 @@
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2811,6 +2985,10 @@
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2835,6 +3013,10 @@
                 %dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2859,6 +3041,10 @@
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2883,6 +3069,10 @@
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2907,6 +3097,10 @@
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2931,6 +3125,10 @@
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2955,6 +3153,10 @@
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2979,6 +3181,10 @@
                 %dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -3003,6 +3209,10 @@
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -3027,6 +3237,10 @@
                 %dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -3051,6 +3265,10 @@
                 %dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
               }
               if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+                used(p0)
+                used(p1)
+                used(p2)
+                used(p3)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -3070,13 +3288,14 @@
 
             @Composable
             fun Test(value: LocalInlineClass) {
-
+                used(value)
             }
         """,
         extra = """
             package androidx.compose.runtime.tests
 
             inline class LocalInlineClass(val value: Int)
+            fun used(x: Any?) {}
         """,
         expectedTransformed = """
             @Composable
@@ -3087,6 +3306,7 @@
                 %dirty = %dirty or if (%composer.changed(value.value)) 0b0100 else 0b0010
               }
               if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                used(value)
               } else {
                 %composer.skipToGroupEnd()
               }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
index 4b90282..1c8d410 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
@@ -40,6 +40,8 @@
             import androidx.compose.runtime.NonRestartableComposable
 
             $unchecked
+
+            fun used(x: Any?) {}
         """.trimIndent(),
         dumpTree = dumpTree
     )
@@ -165,7 +167,7 @@
         """
             @Composable
             fun Test(x: Int = makeInt()) {
-
+                used(x)
             }
         """,
         """
@@ -190,6 +192,7 @@
                     %dirty = %dirty and 0b1110.inv()
                   }
                 }
+                used(x)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -292,7 +295,37 @@
                 a29: Int = 0,
                 a30: Int = 0
             ) {
-                print("Hello world!")
+                used(a00)
+                used(a01)
+                used(a02)
+                used(a03)
+                used(a04)
+                used(a05)
+                used(a06)
+                used(a07)
+                used(a08)
+                used(a09)
+                used(a10)
+                used(a11)
+                used(a12)
+                used(a13)
+                used(a14)
+                used(a15)
+                used(a16)
+                used(a17)
+                used(a18)
+                used(a19)
+                used(a20)
+                used(a21)
+                used(a22)
+                used(a23)
+                used(a24)
+                used(a25)
+                used(a26)
+                used(a27)
+                used(a28)
+                used(a29)
+                used(a30)
             }
         """,
         """
@@ -552,7 +585,37 @@
                 if (%default and 0b01000000000000000000000000000000 !== 0) {
                   a30 = 0
                 }
-                print("Hello world!")
+                used(a00)
+                used(a01)
+                used(a02)
+                used(a03)
+                used(a04)
+                used(a05)
+                used(a06)
+                used(a07)
+                used(a08)
+                used(a09)
+                used(a10)
+                used(a11)
+                used(a12)
+                used(a13)
+                used(a14)
+                used(a15)
+                used(a16)
+                used(a17)
+                used(a18)
+                used(a19)
+                used(a20)
+                used(a21)
+                used(a22)
+                used(a23)
+                used(a24)
+                used(a25)
+                used(a26)
+                used(a27)
+                used(a28)
+                used(a29)
+                used(a30)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -603,7 +666,38 @@
                 a30: Int = 0,
                 a31: Int = 0
             ) {
-                print("Hello world!")
+                used(a00)
+                used(a01)
+                used(a02)
+                used(a03)
+                used(a04)
+                used(a05)
+                used(a06)
+                used(a07)
+                used(a08)
+                used(a09)
+                used(a10)
+                used(a11)
+                used(a12)
+                used(a13)
+                used(a14)
+                used(a15)
+                used(a16)
+                used(a17)
+                used(a18)
+                used(a19)
+                used(a20)
+                used(a21)
+                used(a22)
+                used(a23)
+                used(a24)
+                used(a25)
+                used(a26)
+                used(a27)
+                used(a28)
+                used(a29)
+                used(a30)
+                used(a31)
             }
         """,
         """
@@ -871,7 +965,38 @@
                 if (%default1 and 0b0001 !== 0) {
                   a31 = 0
                 }
-                print("Hello world!")
+                used(a00)
+                used(a01)
+                used(a02)
+                used(a03)
+                used(a04)
+                used(a05)
+                used(a06)
+                used(a07)
+                used(a08)
+                used(a09)
+                used(a10)
+                used(a11)
+                used(a12)
+                used(a13)
+                used(a14)
+                used(a15)
+                used(a16)
+                used(a17)
+                used(a18)
+                used(a19)
+                used(a20)
+                used(a21)
+                used(a22)
+                used(a23)
+                used(a24)
+                used(a25)
+                used(a26)
+                used(a27)
+                used(a28)
+                used(a29)
+                used(a30)
+                used(a31)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -923,7 +1048,38 @@
                 a30: Int = 0,
                 a31: Foo = Foo()
             ) {
-                print("Hello world!")
+                used(a00)
+                used(a01)
+                used(a02)
+                used(a03)
+                used(a04)
+                used(a05)
+                used(a06)
+                used(a07)
+                used(a08)
+                used(a09)
+                used(a10)
+                used(a11)
+                used(a12)
+                used(a13)
+                used(a14)
+                used(a15)
+                used(a16)
+                used(a17)
+                used(a18)
+                used(a19)
+                used(a20)
+                used(a21)
+                used(a22)
+                used(a23)
+                used(a24)
+                used(a25)
+                used(a26)
+                used(a27)
+                used(a28)
+                used(a29)
+                used(a30)
+                used(a31)
             }
         """,
         """
@@ -1201,7 +1357,38 @@
                     %dirty3 = %dirty3 and 0b01110000.inv()
                   }
                 }
-                print("Hello world!")
+                used(a00)
+                used(a01)
+                used(a02)
+                used(a03)
+                used(a04)
+                used(a05)
+                used(a06)
+                used(a07)
+                used(a08)
+                used(a09)
+                used(a10)
+                used(a11)
+                used(a12)
+                used(a13)
+                used(a14)
+                used(a15)
+                used(a16)
+                used(a17)
+                used(a18)
+                used(a19)
+                used(a20)
+                used(a21)
+                used(a22)
+                used(a23)
+                used(a24)
+                used(a25)
+                used(a26)
+                used(a27)
+                used(a28)
+                used(a29)
+                used(a30)
+                used(a31)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -1246,7 +1433,7 @@
               @Composable
               fun Example(%composer: Composer?, %changed: Int) {
                 %composer.startReplaceableGroup(<>, "C(Example)<foo()>:Test.kt")
-                foo(0, %composer, 0, 0b0001)
+                foo(0, %composer, 0b01110000 and %changed shl 0b0011, 0b0001)
                 %composer.endReplaceableGroup()
               }
               static val %stable: Int = 0
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
index b27a838..b07a52e 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
@@ -40,6 +40,7 @@
             import androidx.compose.runtime.Composable
 
             $unchecked
+            fun used(x: Any?) {}
         """.trimIndent(),
         dumpTree = dumpTree
     )
@@ -55,6 +56,7 @@
         """
             @Composable
             fun Test(x: Int = 0, y: Int = 0) {
+                used(y)
                 Wrap {
                     if (x > 0) {
                         A(x)
@@ -86,6 +88,7 @@
                 if (%default and 0b0010 !== 0) {
                   y = 0
                 }
+                used(y)
                 Wrap(composableLambda(%composer, <>, true, "C:Test.kt") { %composer: Composer?, %changed: Int ->
                   if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
                     if (x > 0) {
@@ -120,11 +123,13 @@
         """,
         """
             fun Example(a: A) {
+                used(a)
                 Example { it -> a.compute(it) }
             }
         """,
         """
             fun Example(a: A) {
+              used(a)
               Example(class <no name provided> : A {
                 @Composable
                 override fun compute(it: Int, %composer: Composer?, %changed: Int) {
@@ -133,8 +138,7 @@
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
                   }
-                  %dirty = %dirty or 0b00110000
-                  if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                  if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
                     a.compute(it, %composer, 0b1110 and %dirty)
                   } else {
                     %composer.skipToGroupEnd()
@@ -273,7 +277,12 @@
               crossAxisSize: SizeMode = SizeMode.Wrap,
               content: @Composable() ()->Unit
             ) {
-              println()
+                used(orientation)
+                used(modifier)
+                used(arrangement)
+                used(crossAxisAlignment)
+                used(crossAxisSize)
+                content()
             }
 
             @Composable
@@ -296,7 +305,7 @@
         """
             @Composable
             fun RowColumnImpl(orientation: LayoutOrientation, modifier: Modifier?, arrangement: Vertical?, crossAxisAlignment: Horizontal?, crossAxisSize: SizeMode?, content: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int, %default: Int) {
-              %composer = %composer.startRestartGroup(<>, "C(RowColumnImpl)P(5,4!1,2,3):Test.kt")
+              %composer = %composer.startRestartGroup(<>, "C(RowColumnImpl)P(5,4!1,2,3)<conten...>:Test.kt")
               val %dirty = %changed
               if (%default and 0b0001 !== 0) {
                 %dirty = %dirty or 0b0110
@@ -349,7 +358,12 @@
                     %dirty = %dirty and 0b0001110000000000.inv()
                   }
                 }
-                println()
+                used(orientation)
+                used(modifier)
+                used(arrangement)
+                used(crossAxisAlignment)
+                used(crossAxisSize)
+                content(%composer, 0b1110 and %dirty shr 0b1111)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -425,7 +439,7 @@
         """
             @Composable
             fun SimpleBox(modifier: Modifier = Modifier) {
-               println()
+               used(modifier)
             }
         """,
         """
@@ -442,7 +456,7 @@
                 if (%default and 0b0001 !== 0) {
                   modifier = Companion
                 }
-                println()
+                used(modifier)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -498,6 +512,46 @@
     )
 
     @Test
+    fun testLocalComposableFunctions(): Unit = comparisonPropagation(
+        """
+            @Composable fun A(a: Int) {}
+        """,
+        """
+            @Composable
+            fun Example(a: Int) {
+                @Composable fun Inner() {
+                    A(a)
+                }
+                Inner()
+            }
+        """,
+        """
+            @Composable
+            fun Example(a: Int, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(Example)<Inner(...>:Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
+              }
+              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                @Composable
+                fun Inner(%composer: Composer?, %changed: Int) {
+                  %composer.startReplaceableGroup(<>, "C(Inner)<A(a)>:Test.kt")
+                  A(a, %composer, 0b1110 and %dirty)
+                  %composer.endReplaceableGroup()
+                }
+                Inner(%composer, 0)
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Example(a, %composer, %changed or 0b0001)
+              }
+            }
+        """
+    )
+
+    @Test
     fun testLoopWithContinueAndCallAfter(): Unit = comparisonPropagation(
         """
             @Composable fun Call() {}
@@ -557,7 +611,8 @@
         """
             @Composable
             fun SimpleBox(modifier: Modifier = Modifier, shape: Shape = RectangleShape) {
-               println()
+                used(modifier)
+                used(shape)
             }
         """,
         """
@@ -590,7 +645,8 @@
                     %dirty = %dirty and 0b01110000.inv()
                   }
                 }
-                println()
+                used(modifier)
+                used(shape)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -614,13 +670,14 @@
         """
             @Composable
             fun SimpleBox(modifier: Modifier = Modifier, content: @Composable() () -> Unit = {}) {
-               println()
+                used(modifier)
+                content()
             }
         """,
         """
             @Composable
             fun SimpleBox(modifier: Modifier?, content: Function2<Composer, Int, Unit>?, %composer: Composer?, %changed: Int, %default: Int) {
-              %composer = %composer.startRestartGroup(<>, "C(SimpleBox)P(1):Test.kt")
+              %composer = %composer.startRestartGroup(<>, "C(SimpleBox)P(1)<conten...>:Test.kt")
               val %dirty = %changed
               if (%default and 0b0001 !== 0) {
                 %dirty = %dirty or 0b0110
@@ -647,7 +704,8 @@
                     %dirty = %dirty and 0b01110000.inv()
                   }
                 }
-                println()
+                used(modifier)
+                content(%composer, 0b1110 and %dirty shr 0b0011)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -733,7 +791,7 @@
         """
         """,
         """
-            @Composable fun SomeThing(content: @Composable() () -> Unit) {}
+            @Composable fun SomeThing(content: @Composable() () -> Unit) { content() }
 
             @Composable
             fun Example() {
@@ -745,12 +803,13 @@
         """
             @Composable
             fun SomeThing(content: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
-              %composer = %composer.startRestartGroup(<>, "C(SomeThing):Test.kt")
+              %composer = %composer.startRestartGroup(<>, "C(SomeThing)<conten...>:Test.kt")
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
               }
               if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                content(%composer, 0b1110 and %dirty)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -904,9 +963,7 @@
               @Composable
               fun A(%composer: Composer?, %changed: Int) {
                 %composer = %composer.startRestartGroup(<>, "C(A):Test.kt")
-                val %dirty = %changed
-                %dirty = %dirty or 0b0110
-                if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b0001 !== 0 || !%composer.skipping) {
                   print("hello world")
                 } else {
                   %composer.skipToGroupEnd()
@@ -919,7 +976,6 @@
               @Composable
               fun B(%composer: Composer?, %changed: Int) {
                 %composer = %composer.startRestartGroup(<>, "C(B):Test.kt")
-                val %dirty = %changed
                 print(counter)
                 val tmp0_rcvr = <this>
                 %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
@@ -939,7 +995,9 @@
         """
             @Composable
             fun Example(a: Int = 0, b: Int = makeInt(), c: Int = 0) {
-
+                used(a)
+                used(b)
+                used(c)
             }
         """,
         """
@@ -980,6 +1038,9 @@
                     %dirty = %dirty and 0b01110000.inv()
                   }
                 }
+                used(a)
+                used(b)
+                used(c)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -1001,7 +1062,9 @@
             }
             @Composable
             fun Test(x: Int = 0, y: Int = 0) {
+                used(y)
                 Wrap(10) {
+                    used(it)
                     A(x)
                 }
             }
@@ -1047,12 +1110,14 @@
                 if (%default and 0b0010 !== 0) {
                   y = 0
                 }
+                used(y)
                 Wrap(10, composableLambda(%composer, <>, true, "C<A(x)>:Test.kt") { it: Int, %composer: Composer?, %changed: Int ->
                   val %dirty = %changed
                   if (%changed and 0b1110 === 0) {
                     %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
                   }
                   if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                    used(it)
                     A(x, 0, %composer, 0b1110 and %dirty, 0b0010)
                   } else {
                     %composer.skipToGroupEnd()
@@ -1186,9 +1251,12 @@
         """,
         """
             @Composable fun CanSkip(a: Int = 0, b: Foo = Foo()) {
-                print("Hello World")
+                used(a)
+                used(b)
             }
             @Composable fun CannotSkip(a: Int, b: Foo) {
+                used(a)
+                used(b)
                 print("Hello World")
             }
             @Composable fun NoParams() {
@@ -1226,7 +1294,8 @@
                     %dirty = %dirty and 0b01110000.inv()
                   }
                 }
-                print("Hello World")
+                used(a)
+                used(b)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -1237,6 +1306,8 @@
             @Composable
             fun CannotSkip(a: Int, b: Foo, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>, "C(CannotSkip):Test.kt")
+              used(a)
+              used(b)
               print("Hello World")
               %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
                 CannotSkip(a, b, %composer, %changed or 0b0001)
@@ -1258,6 +1329,48 @@
     )
 
     @Test
+    fun testOptionalUnstableWithStableExtensionReceiver(): Unit = comparisonPropagation(
+        """
+            class Foo(var value: Int = 0)
+            class Bar
+        """,
+        """
+            @Composable fun Bar.CanSkip(b: Foo = Foo()) {
+                print("Hello World")
+            }
+        """,
+        """
+            @Composable
+            fun Bar.CanSkip(b: Foo?, %composer: Composer?, %changed: Int, %default: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(CanSkip):Test.kt")
+              val %dirty = %changed
+              if (%default.inv() and 0b0001 !== 0 || %dirty and 0b0001 !== 0 || !%composer.skipping) {
+                if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
+                  %composer.startDefaults()
+                  if (%default and 0b0001 !== 0) {
+                    b = Foo(
+                    )
+                    %dirty = %dirty and 0b01110000.inv()
+                  }
+                  %composer.endDefaults()
+                } else {
+                  %composer.skipCurrentGroup()
+                  if (%default and 0b0001 !== 0) {
+                    %dirty = %dirty and 0b01110000.inv()
+                  }
+                }
+                print("Hello World")
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                CanSkip(b, %composer, %changed or 0b0001, %default)
+              }
+            }
+        """
+    )
+
+    @Test
     fun testNoParams(): Unit = comparisonPropagation(
         """
             @Composable fun A() {}
@@ -1331,7 +1444,10 @@
             }
 
             @Composable
-            fun B(text: String, color: Color = Color.Unset) {}
+            fun B(text: String, color: Color = Color.Unset) {
+                used(text)
+                used(color)
+            }
         """,
         """
             @Composable
@@ -1376,6 +1492,8 @@
                     %dirty = %dirty and 0b01110000.inv()
                   }
                 }
+                used(text)
+                used(color)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -1785,6 +1903,151 @@
     )
 
     @Test
+    fun testLambdaSkipping(): Unit = comparisonPropagation(
+        """
+        import androidx.compose.runtime.*
+
+        data class User(
+            val id: Int,
+            val name: String
+        )
+
+        interface LazyPagingItems<T> {
+            val itemCount: Int
+            operator fun get(index: Int): State<T?>
+        }
+
+        @Stable interface LazyListScope {
+            fun items(itemCount: Int, itemContent: @Composable LazyItemScope.(Int) -> Unit)
+        }
+
+        @Stable interface LazyItemScope
+
+        public fun <T : Any> LazyListScope.itemsIndexed(
+            lazyPagingItems: LazyPagingItems<T>,
+            itemContent: @Composable LazyItemScope.(Int, T?) -> Unit
+        ) {
+            items(lazyPagingItems.itemCount) { index ->
+                val item = lazyPagingItems[index].value
+                itemContent(index, item)
+            }
+        }
+        """,
+        """
+            fun LazyListScope.Example(items: LazyPagingItems<User>) {
+                itemsIndexed(items) { index, user ->
+                    print("Hello World")
+                }
+            }
+        """,
+        """
+            fun LazyListScope.Example(items: LazyPagingItems<User>) {
+              itemsIndexed(items, ComposableSingletons%TestKt.lambda-1)
+            }
+            internal object ComposableSingletons%TestKt {
+              val lambda-1: @[ExtensionFunctionType] Function5<LazyItemScope, Int, User?, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:Test.kt") { index: Int, user: User?, %composer: Composer?, %changed: Int ->
+                if (%changed and 0b0001010000000001 xor 0b010000000000 !== 0 || !%composer.skipping) {
+                  print("Hello World")
+                } else {
+                  %composer.skipToGroupEnd()
+                }
+              }
+            }
+        """
+    )
+
+    @Test
+    fun testPassedExtensionWhenExtensionIsPotentiallyUnstable(): Unit = comparisonPropagation(
+        """
+            interface Unstable
+        """,
+        """
+            @Composable fun Unstable.Test() {
+                doSomething(this) // does this reference %dirty without %dirty
+            }
+
+            @Composable fun doSomething(x: Unstable) {}
+        """,
+        """
+            @Composable
+            fun Unstable.Test(%composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(Test)<doSome...>:Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(<this>)) 0b0100 else 0b0010
+              }
+              if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                doSomething(<this>, %composer, 0b1110 and %dirty)
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(%composer, %changed or 0b0001)
+              }
+            }
+            @Composable
+            fun doSomething(x: Unstable, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(doSomething):Test.kt")
+              if (%changed and 0b0001 !== 0 || !%composer.skipping) {
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                doSomething(x, %composer, %changed or 0b0001)
+              }
+            }
+        """
+    )
+
+    @Test
+    fun testReceiverIssue(): Unit = comparisonPropagation(
+        """
+            class Foo
+        """,
+        """
+            import androidx.compose.runtime.ExplicitGroupsComposable
+
+            @Composable
+            @ExplicitGroupsComposable
+            fun A(foo: Foo) {
+                foo.b()
+            }
+
+            @Composable
+            @ExplicitGroupsComposable
+            inline fun Foo.b(label: String = "") {
+                c(this, label)
+            }
+
+            @Composable
+            @ExplicitGroupsComposable
+            inline fun c(foo: Foo, label: String) {
+                print(label)
+            }
+        """,
+        """
+            @Composable
+            @ExplicitGroupsComposable
+            fun A(foo: Foo, %composer: Composer?, %changed: Int) {
+              foo.b(null, %composer, 0b1110 and %changed, 0b0001)
+            }
+            @Composable
+            @ExplicitGroupsComposable
+            fun Foo.b(label: String?, %composer: Composer?, %changed: Int, %default: Int) {
+              if (%default and 0b0001 !== 0) {
+                label = ""
+              }
+              c(<this>, label, %composer, 0b1110 and %changed or 0b01110000 and %changed)
+            }
+            @Composable
+            @ExplicitGroupsComposable
+            fun c(foo: Foo, label: String, %composer: Composer?, %changed: Int) {
+              print(label)
+            }
+        """
+    )
+
+    @Test
     fun testDifferentParameters(): Unit = comparisonPropagation(
         """
             @Composable fun B(a: Int, b: Int, c: Int, d: Int) {}
@@ -1838,12 +2101,12 @@
             val unstableUnused: @Composable Foo.() -> Unit = {
             }
             val unstableUsed: @Composable Foo.() -> Unit = {
-                print(x)
+                used(x)
             }
             val stableUnused: @Composable StableFoo.() -> Unit = {
             }
             val stableUsed: @Composable StableFoo.() -> Unit = {
-                print(x)
+                used(x)
             }
         """,
         """
@@ -1853,9 +2116,7 @@
             val stableUsed: @[ExtensionFunctionType] Function3<StableFoo, Composer, Int, Unit> = ComposableSingletons%TestKt.lambda-4
             internal object ComposableSingletons%TestKt {
               val lambda-1: @[ExtensionFunctionType] Function3<Foo, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:") { %composer: Composer?, %changed: Int ->
-                val %dirty = %changed
-                %dirty = %dirty or 0b0110
-                if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -1863,12 +2124,17 @@
               }
               val lambda-2: @[ExtensionFunctionType] Function3<Foo, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:") { %composer: Composer?, %changed: Int ->
                 val %dirty = %changed
-                print(%this%null.x)
+                if (%changed and 0b1110 === 0) {
+                  %dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
+                }
+                if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                  used(%this%null.x)
+                } else {
+                  %composer.skipToGroupEnd()
+                }
               }
               val lambda-3: @[ExtensionFunctionType] Function3<StableFoo, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:") { %composer: Composer?, %changed: Int ->
-                val %dirty = %changed
-                %dirty = %dirty or 0b0110
-                if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                if (%changed and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
                   Unit
                 } else {
                   %composer.skipToGroupEnd()
@@ -1880,7 +2146,7 @@
                   %dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
                 }
                 if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
-                  print(%this%null.x)
+                  used(%this%null.x)
                 } else {
                   %composer.skipToGroupEnd()
                 }
@@ -1974,19 +2240,9 @@
               if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
                 @Composable
                 fun foo(y: Int, %composer: Composer?, %changed: Int) {
-                  %composer = %composer.startRestartGroup(<>, "C(foo)<B(x,>:Test.kt")
-                  val %dirty = %changed
-                  if (%changed and 0b1110 === 0) {
-                    %dirty = %dirty or if (%composer.changed(y)) 0b0100 else 0b0010
-                  }
-                  if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
-                    B(x, y, %composer, 0b1110 and %dirty or 0b01110000 and %dirty shl 0b0011)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                    foo(y, %composer, %changed or 0b0001)
-                  }
+                  %composer.startReplaceableGroup(<>, "C(foo)<B(x,>:Test.kt")
+                  B(x, y, %composer, 0b1110 and %dirty or 0b01110000 and %changed shl 0b0011)
+                  %composer.endReplaceableGroup()
                 }
                 foo(x, %composer, 0b1110 and %dirty)
               } else {
@@ -2580,13 +2836,15 @@
                 paddingStart: Dp = Dp.Unspecified,
                 content: @Composable () -> Unit = {}
             ) {
-
+                used(modifier)
+                used(paddingStart)
+                content()
             }
         """,
         """
             @Composable
             fun Box2(modifier: Modifier?, paddingStart: Dp, content: Function2<Composer, Int, Unit>?, %composer: Composer?, %changed: Int, %default: Int) {
-              %composer = %composer.startRestartGroup(<>, "C(Box2)P(1,2:c#ui.unit.Dp):Test.kt")
+              %composer = %composer.startRestartGroup(<>, "C(Box2)P(1,2:c#ui.unit.Dp)<conten...>:Test.kt")
               val %dirty = %changed
               if (%default and 0b0001 !== 0) {
                 %dirty = %dirty or 0b0110
@@ -2623,6 +2881,9 @@
                     %dirty = %dirty and 0b001110000000.inv()
                   }
                 }
+                used(modifier)
+                used(paddingStart)
+                content(%composer, 0b1110 and %dirty shr 0b0110)
               } else {
                 %composer.skipToGroupEnd()
               }
@@ -2693,4 +2954,139 @@
             }
         """
     )
+
+    @Test
+    fun testUnusedParameters(): Unit = comparisonPropagation(
+        """
+            class Unstable(var count: Int)
+            class Stable(val count: Int)
+            interface MaybeStable
+        """,
+        """
+            @Composable
+            fun Unskippable(a: Unstable, b: Stable, c: MaybeStable) {
+                used(a)
+            }
+            @Composable
+            fun Skippable1(a: Unstable, b: Stable, c: MaybeStable) {
+                used(b)
+            }
+            @Composable
+            fun Skippable2(a: Unstable, b: Stable, c: MaybeStable) {
+                used(c)
+            }
+            @Composable
+            fun Skippable3(a: Unstable, b: Stable, c: MaybeStable) { }
+        """,
+        """
+            @Composable
+            fun Unskippable(a: Unstable, b: Stable, c: MaybeStable, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(Unskippable):Test.kt")
+              used(a)
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Unskippable(a, b, c, %composer, %changed or 0b0001)
+              }
+            }
+            @Composable
+            fun Skippable1(a: Unstable, b: Stable, c: MaybeStable, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(Skippable1):Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b01110000 === 0) {
+                %dirty = %dirty or if (%composer.changed(b)) 0b00100000 else 0b00010000
+              }
+              if (%dirty and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
+                used(b)
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Skippable1(a, b, c, %composer, %changed or 0b0001)
+              }
+            }
+            @Composable
+            fun Skippable2(a: Unstable, b: Stable, c: MaybeStable, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(Skippable2):Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b001110000000 === 0) {
+                %dirty = %dirty or if (%composer.changed(c)) 0b000100000000 else 0b10000000
+              }
+              if (%dirty and 0b001010000001 xor 0b10000000 !== 0 || !%composer.skipping) {
+                used(c)
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Skippable2(a, b, c, %composer, %changed or 0b0001)
+              }
+            }
+            @Composable
+            fun Skippable3(a: Unstable, b: Stable, c: MaybeStable, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(Skippable3):Test.kt")
+              if (%changed and 0b0001 !== 0 || !%composer.skipping) {
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Skippable3(a, b, c, %composer, %changed or 0b0001)
+              }
+            }
+        """
+    )
+
+    @Test
+    fun testExtensionReceiver(): Unit = comparisonPropagation(
+        """
+            interface MaybeStable
+        """,
+        """
+            @Composable fun MaybeStable.example(x: Int) {
+                used(this)
+                used(x)
+            }
+            val example: @Composable MaybeStable.(Int) -> Unit = {
+                used(this)
+                used(it)
+            }
+        """,
+        """
+            @Composable
+            fun MaybeStable.example(x: Int, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>, "C(example):Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(<this>)) 0b0100 else 0b0010
+              }
+              if (%changed and 0b01110000 === 0) {
+                %dirty = %dirty or if (%composer.changed(x)) 0b00100000 else 0b00010000
+              }
+              if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+                used(<this>)
+                used(x)
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                example(x, %composer, %changed or 0b0001)
+              }
+            }
+            val example: @[ExtensionFunctionType] Function4<MaybeStable, Int, Composer, Int, Unit> = ComposableSingletons%TestKt.lambda-1
+            internal object ComposableSingletons%TestKt {
+              val lambda-1: @[ExtensionFunctionType] Function4<MaybeStable, Int, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:") { it: Int, %composer: Composer?, %changed: Int ->
+                val %dirty = %changed
+                if (%changed and 0b1110 === 0) {
+                  %dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
+                }
+                if (%changed and 0b01110000 === 0) {
+                  %dirty = %dirty or if (%composer.changed(it)) 0b00100000 else 0b00010000
+                }
+                if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+                  used(%this%null)
+                  used(it)
+                } else {
+                  %composer.skipToGroupEnd()
+                }
+              }
+            }
+        """
+    )
 }
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 7e64583..838f9bf 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -57,7 +57,7 @@
 
             @Composable fun Example() {
                 @Composable fun A() { }
-                @Composable fun B(content: @Composable () -> Unit) { }
+                @Composable fun B(content: @Composable () -> Unit) { content() }
                 @Composable fun C() {
                     B { A() }
                 }
@@ -70,47 +70,26 @@
               if (%changed !== 0 || !%composer.skipping) {
                 @Composable
                 fun A(%composer: Composer?, %changed: Int) {
-                  %composer = %composer.startRestartGroup(<>, "C(A):Test.kt")
-                  if (%changed !== 0 || !%composer.skipping) {
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                    A(%composer, %changed or 0b0001)
-                  }
+                  %composer.startReplaceableGroup(<>, "C(A):Test.kt")
+                  %composer.endReplaceableGroup()
                 }
                 @Composable
                 fun B(content: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
-                  %composer = %composer.startRestartGroup(<>, "C(B):Test.kt")
-                  val %dirty = %changed
-                  if (%changed and 0b1110 === 0) {
-                    %dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
-                  }
-                  if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                    B(content, %composer, %changed or 0b0001)
-                  }
+                  %composer.startReplaceableGroup(<>, "C(B)<conten...>:Test.kt")
+                  content(%composer, 0b1110 and %changed)
+                  %composer.endReplaceableGroup()
                 }
                 @Composable
                 fun C(%composer: Composer?, %changed: Int) {
-                  %composer = %composer.startRestartGroup(<>, "C(C)<B>:Test.kt")
-                  if (%changed !== 0 || !%composer.skipping) {
-                    B(composableLambda(%composer, <>, false, "C<A()>:Test.kt") { %composer: Composer?, %changed: Int ->
-                      if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
-                        A(%composer, 0)
-                      } else {
-                        %composer.skipToGroupEnd()
-                      }
-                    }, %composer, 0b0110)
-                  } else {
-                    %composer.skipToGroupEnd()
-                  }
-                  %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
-                    C(%composer, %changed or 0b0001)
-                  }
+                  %composer.startReplaceableGroup(<>, "C(C)<B>:Test.kt")
+                  B(composableLambda(%composer, <>, false, "C<A()>:Test.kt") { %composer: Composer?, %changed: Int ->
+                    if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+                      A(%composer, 0)
+                    } else {
+                      %composer.skipToGroupEnd()
+                    }
+                  }, %composer, 0b0110)
+                  %composer.endReplaceableGroup()
                 }
               } else {
                 %composer.skipToGroupEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
index 6798cf6..6fcf50c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
@@ -597,7 +597,7 @@
     // since the lowering will throw an exception if duplicate keys are found, all we have to do
     // is run the lowering
     private fun assertNoDuplicateKeys(@Language("kotlin") src: String) {
-        generateIrModuleWithJvmResolve(
+        JvmCompilation().compile(
             listOf(
                 sourceFile("Test.kt", src.replace('%', '$'))
             )
@@ -607,7 +607,7 @@
     // For a given src string, a
     private fun assertKeys(vararg keys: String, makeSrc: () -> String) {
         builtKeys = mutableSetOf()
-        generateIrModuleWithJvmResolve(
+        JvmCompilation().compile(
             listOf(
                 sourceFile("Test.kt", makeSrc().replace('%', '$'))
             )
@@ -624,7 +624,7 @@
 
     // test: have two src strings (before/after) and assert that the keys of the params didn't change
     private fun assertDurableChange(before: String, after: String) {
-        generateIrModuleWithJvmResolve(
+        JvmCompilation().compile(
             listOf(
                 sourceFile("Test.kt", before.replace('%', '$'))
             )
@@ -633,7 +633,7 @@
 
         builtKeys = mutableSetOf()
 
-        generateIrModuleWithJvmResolve(
+        JvmCompilation().compile(
             listOf(
                 sourceFile("Test.kt", after.replace('%', '$'))
             )
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 9d46a28..51532ad 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -17,6 +17,7 @@
 package androidx.compose.compiler.plugins.kotlin
 
 import org.intellij.lang.annotations.Language
+import org.junit.Ignore
 import org.junit.Test
 
 class RememberIntrinsicTransformTests : ComposeIrTransformTest() {
@@ -39,6 +40,7 @@
             import androidx.compose.runtime.Composable
 
             $unchecked
+            fun used(x: Any?) {}
         """.trimIndent(),
         dumpTree = dumpTree
     )
@@ -666,6 +668,7 @@
             @Composable
             fun Test(items: List<Int>) {
                 val foo = remember { Foo() }
+                used(items)
             }
         """,
         """
@@ -676,6 +679,7 @@
                 val tmp0_return = Foo()
                 tmp0_return
               }
+              used(items)
               %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
                 Test(items, %composer, %changed or 0b0001)
               }
@@ -900,8 +904,8 @@
         """
     )
 
-    @Test
-    fun testOptimizationFailsIfDefaultsGroupIsUsed(): Unit = comparisonPropagation(
+    @Ignore("This test must pass before intrinsic remember can be turned on")
+    fun xtestOptimizationFailsIfDefaultsGroupIsUsed(): Unit = comparisonPropagation(
         """
             class Foo
             fun someInt(): Int = 123
@@ -910,8 +914,8 @@
             @Composable
             fun Test(a: Int = someInt()) {
                 val foo = remember { Foo() }
-                print(foo)
-                print(a)
+                used(foo)
+                used(a)
             }
         """,
         """
@@ -940,8 +944,8 @@
                   val tmp0_return = Foo()
                   tmp0_return
                 }, %composer, 0)
-                print(foo)
-                print(a)
+                used(foo)
+                used(a)
               } else {
                 %composer.skipToGroupEnd()
               }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RobolectricComposeTester.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RobolectricComposeTester.kt
index 024f822..9023705 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RobolectricComposeTester.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RobolectricComposeTester.kt
@@ -28,7 +28,6 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.NonCancellable
 import kotlinx.coroutines.launch
 import org.robolectric.Robolectric
@@ -115,7 +114,6 @@
     }
 
     companion object {
-        @OptIn(ExperimentalCoroutinesApi::class)
         private val recomposer = run {
             val mainScope = CoroutineScope(
                 NonCancellable + Dispatchers.Main
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 49c690a..09d2ab6 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -127,6 +127,7 @@
 import org.jetbrains.kotlin.ir.types.typeWith
 import org.jetbrains.kotlin.ir.util.ConstantValueGenerator
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET
 import org.jetbrains.kotlin.ir.util.TypeTranslator
 import org.jetbrains.kotlin.ir.util.functions
 import org.jetbrains.kotlin.ir.util.getArguments
@@ -920,17 +921,19 @@
         isVar: Boolean = false,
         origin: IrDeclarationOrigin = IrDeclarationOrigin.IR_TEMPORARY_VARIABLE
     ): IrVariableImpl {
+        val descriptor = WrappedVariableDescriptor()
         return IrVariableImpl(
             value.startOffset,
             value.endOffset,
             origin,
-            IrVariableSymbolImpl(WrappedVariableDescriptor()),
+            IrVariableSymbolImpl(descriptor),
             Name.identifier(name),
             irType,
             isVar,
             false,
             false
         ).apply {
+            descriptor.bind(this)
             initializer = value
         }
     }
@@ -1044,6 +1047,8 @@
 
     fun makeStabilityField(): IrField {
         return context.irFactory.buildField {
+            startOffset = SYNTHETIC_OFFSET
+            endOffset = SYNTHETIC_OFFSET
             name = KtxNameConventions.STABILITY_FLAG
             isStatic = true
             isFinal = true
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index a872344..4c86015 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -29,6 +29,7 @@
 import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
 import org.jetbrains.kotlin.backend.common.pop
 import org.jetbrains.kotlin.backend.common.push
+import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin
 import org.jetbrains.kotlin.builtins.PrimitiveType
 import org.jetbrains.kotlin.builtins.StandardNames
 import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
@@ -136,7 +137,9 @@
 import org.jetbrains.kotlin.ir.util.functions
 import org.jetbrains.kotlin.ir.util.getPropertyGetter
 import org.jetbrains.kotlin.ir.util.isInlined
+import org.jetbrains.kotlin.ir.util.isLocal
 import org.jetbrains.kotlin.ir.util.isVararg
+import org.jetbrains.kotlin.ir.util.parentClassOrNull
 import org.jetbrains.kotlin.ir.util.patchDeclarationParents
 import org.jetbrains.kotlin.ir.util.properties
 import org.jetbrains.kotlin.ir.util.statements
@@ -251,10 +254,11 @@
 }
 
 interface IrChangedBitMaskValue {
+    val used: Boolean
     fun irLowBit(): IrExpression
     fun irIsolateBitsAtSlot(slot: Int, includeStableBit: Boolean): IrExpression
     fun irSlotAnd(slot: Int, bits: Int): IrExpression
-    fun irHasDifferences(): IrExpression
+    fun irHasDifferences(usedParams: BooleanArray): IrExpression
     fun irCopyToTemporary(
         nameHint: String? = null,
         isVar: Boolean = false,
@@ -721,6 +725,10 @@
         if (body == null)
             return false
 
+        if (isLocal && parentClassOrNull?.origin != JvmLoweredDeclarationOrigin.LAMBDA_IMPL) {
+            return false
+        }
+
         val descriptor = descriptor
 
         // Do not insert observe scope in an inline function
@@ -730,6 +738,9 @@
         if (descriptor.hasNonRestartableComposableAnnotation())
             return false
 
+        if (descriptor.hasExplicitGroupsAnnotation())
+            return false
+
         // Do not insert an observe scope in an inline composable lambda
         descriptor.findPsi()?.let { psi ->
             (psi as? KtFunctionLiteral)?.let {
@@ -797,31 +808,24 @@
 
         scope.dirty = changedParam
 
-        val realParams = declaration.valueParameters.take(scope.realValueParamCount)
-
-        buildStatementsForSkippingAndDefaults(
-            body,
-            skipPreamble,
-            bodyPreamble,
-            false,
-            realParams,
-            scope,
-            changedParam,
-            changedParam,
-            defaultParam,
-            booleanArrayOf()
-        )
-
-        realParams.forEach {
-            // we want to remove the default expression from the function. This will prevent
-            // the kotlin compiler from doing its own default handling, which we don't need.
-            it.defaultValue = null
-        }
+        val defaultScope = transformDefaults(scope)
 
         var (transformed, returnVar) = body.asBodyAndResultVar()
 
         transformed = transformed.apply { transformChildrenVoid() }
 
+        buildPreambleStatementsAndReturnIfSkippingPossible(
+            body,
+            skipPreamble,
+            bodyPreamble,
+            false,
+            scope,
+            changedParam,
+            changedParam,
+            defaultParam,
+            defaultScope,
+        )
+
         if (!elideGroups) scope.realizeGroup(::irEndReplaceableGroup)
 
         declaration.body = IrBlockBodyImpl(
@@ -836,7 +840,6 @@
                     )
                 else
                     null,
-                *skipPreamble.statements.toTypedArray(),
                 *bodyPreamble.statements.toTypedArray(),
                 *transformed.statements.toTypedArray(),
                 if (!elideGroups) irEndReplaceableGroup() else null,
@@ -865,24 +868,14 @@
         val skipPreamble = mutableStatementContainer()
         val bodyPreamble = mutableStatementContainer()
 
-        val realParams = declaration.valueParameters.take(scope.realValueParamCount)
-
-        val realParamsIncludingThis = realParams + listOfNotNull(
-            declaration.extensionReceiverParameter
-        )
-
-        // boolean array mapped to parameters. true indicates that the type is unstable
-        val unstableMask = realParams.map {
-            stabilityOf(it.type).knownUnstable()
-        }.toBooleanArray()
-
         // we start off assuming that we *can* skip execution of the function
-        var canSkipExecution = unstableMask.none { it } && declaration.returnType.isUnit()
+        var canSkipExecution = declaration.returnType.isUnit() &&
+            scope.allTrackedParams.none { stabilityOf(it.type).knownUnstable() }
 
         // if the function can never skip, or there are no parameters to test, then we
         // don't need to have the dirty parameter locally since it will never be different from
         // the passed in `changed` parameter.
-        val dirty = if (canSkipExecution && realParamsIncludingThis.isNotEmpty())
+        val dirty = if (canSkipExecution && scope.allTrackedParams.isNotEmpty())
         // NOTE(lmr): Technically, dirty is a mutable variable, but we don't want to mark it
         // as one since that will cause a `Ref<Int>` to get created if it is captured. Since
         // we know we will never be mutating this variable _after_ it gets captured, we can
@@ -891,51 +884,40 @@
                 isVar = false,
                 nameHint = "\$dirty",
                 exactName = true
-            ).also {
-                skipPreamble.statements.addAll(it.asStatements())
-            }
+            )
         else
             changedParam
 
         scope.dirty = dirty
 
-        buildStatementsForSkippingAndDefaults(
-            body,
-            skipPreamble,
-            bodyPreamble,
-            canSkipExecution,
-            realParams,
-            scope,
-            dirty,
-            changedParam,
-            null,
-            unstableMask
-        )
-
         val (nonReturningBody, returnVar) = body.asBodyAndResultVar()
 
         // we must transform the body first, since that will allow us to see whether or not we
         // are using the dispatchReceiverParameter or the extensionReceiverParameter
         val transformed = nonReturningBody.apply { transformChildrenVoid() }
 
-        if (declaration.extensionReceiverParameter != null) {
-            canSkipExecution = buildStatementsForSkippingThisParameter(
-                declaration.extensionReceiverParameter!!,
-                scope.extensionReceiverUsed,
-                canSkipExecution,
-                skipPreamble,
-                changedParam,
-                dirty,
-                scope.realValueParamCount
-            )
-        }
+        canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible(
+            body,
+            skipPreamble,
+            bodyPreamble,
+            canSkipExecution,
+            scope,
+            dirty,
+            changedParam,
+            null,
+            Scope.ParametersScope(),
+        )
 
+        val dirtyForSkipping = if (dirty.used && dirty is IrChangedBitMaskVariable) {
+            skipPreamble.statements.addAll(0, dirty.asStatements())
+            dirty
+        } else changedParam
         if (canSkipExecution) {
             // We CANNOT skip if any of the following conditions are met
             // 1. if any of the stable parameters have *differences* from last execution.
             // 2. if the composer.skipping call returns false
             val shouldExecute = irOrOr(
-                scope.dirty!!.irHasDifferences(),
+                dirtyForSkipping.irHasDifferences(scope.usedParams),
                 irNot(irIsSkipping())
             )
 
@@ -998,75 +980,24 @@
         val skipPreamble = mutableStatementContainer()
         val bodyPreamble = mutableStatementContainer()
 
-        // these are the parameters excluding the synthetic ones that we generate for compose.
-        // These are the only parameters we want to consider in skipping calculations
-        val realParams = declaration.valueParameters.take(scope.realValueParamCount)
-
-        val thisParams = listOfNotNull(
-            declaration.extensionReceiverParameter,
-            declaration.dispatchReceiverParameter
-        )
-
-        val realParamsIncludingThis = realParams + thisParams
-
         // we start off assuming that we *can* skip execution of the function
         var canSkipExecution = true
 
-        // boolean array mapped to parameters. true indicates that the type is unstable
-        val unstableMask = realParams.map {
-            if (stabilityOf((it.varargElementType ?: it.type)).knownUnstable()) {
-                if (!it.hasDefaultValueSafe()) {
-                    // if it has non-optional unstable params, the function can never skip
-                    canSkipExecution = false
-                }
-                true
-            } else {
-                false
-            }
-        }.toBooleanArray()
-
-        // if the function can never skip, or there are no parameters to test, then we
-        // don't need to have the dirty parameter locally since it will never be different from
-        // the passed in `changed` parameter.
-        val dirty = if (canSkipExecution && realParamsIncludingThis.isNotEmpty())
         // NOTE(lmr): Technically, dirty is a mutable variable, but we don't want to mark it
         // as one since that will cause a `Ref<Int>` to get created if it is captured. Since
         // we know we will never be mutating this variable _after_ it gets captured, we can
         // safely mark this as `isVar = false`.
+        val dirty = if (scope.allTrackedParams.isNotEmpty())
             changedParam.irCopyToTemporary(
                 isVar = false,
                 nameHint = "\$dirty",
                 exactName = true
-            ).also {
-                skipPreamble.statements.addAll(it.asStatements())
-            }
+            )
         else
             changedParam
 
         scope.dirty = dirty
 
-        buildStatementsForSkippingAndDefaults(
-            body,
-            skipPreamble,
-            bodyPreamble,
-            canSkipExecution,
-            realParams,
-            scope,
-            dirty,
-            changedParam,
-            defaultParam,
-            unstableMask
-        )
-
-        realParams.forEach {
-            // we want to remove the default expression from the function. This will prevent
-            // the kotlin compiler from doing its own default handling, which we don't need.
-
-            // NOTE: we are doing this AFTER buildStatementsForSkipping, because the default
-            // value is used in those calculations
-            it.defaultValue = null
-        }
-
         val (nonReturningBody, returnVar) = body.asBodyAndResultVar()
 
         val end = {
@@ -1078,39 +1009,31 @@
             )
         }
 
+        val defaultScope = transformDefaults(scope)
+
         // we must transform the body first, since that will allow us to see whether or not we
         // are using the dispatchReceiverParameter or the extensionReceiverParameter
         val transformed = nonReturningBody.apply { transformChildrenVoid() }
 
-        var slotIndex = scope.realValueParamCount
-
-        if (declaration.extensionReceiverParameter != null) {
-            canSkipExecution = buildStatementsForSkippingThisParameter(
-                declaration.extensionReceiverParameter!!,
-                scope.extensionReceiverUsed,
-                canSkipExecution,
-                skipPreamble,
-                changedParam,
-                dirty,
-                slotIndex++
-            )
-        }
-
-        if (declaration.dispatchReceiverParameter != null) {
-            canSkipExecution = buildStatementsForSkippingThisParameter(
-                declaration.dispatchReceiverParameter!!,
-                scope.dispatchReceiverUsed,
-                canSkipExecution,
-                skipPreamble,
-                changedParam,
-                dirty,
-                slotIndex
-            )
-        }
+        canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible(
+            body,
+            skipPreamble,
+            bodyPreamble,
+            canSkipExecution,
+            scope,
+            dirty,
+            changedParam,
+            defaultParam,
+            defaultScope,
+        )
 
         // if it has non-optional unstable params, the function can never skip, so we always
         // execute the body. Otherwise, we wrap the body in an if and only skip when certain
         // conditions are met.
+        val dirtyForSkipping = if (dirty.used && dirty is IrChangedBitMaskVariable) {
+            skipPreamble.statements.addAll(0, dirty.asStatements())
+            dirty
+        } else changedParam
         val transformedBody = if (canSkipExecution) {
             // We CANNOT skip if any of the following conditions are met
             // 1. if any of the stable parameters have *differences* from last execution.
@@ -1119,11 +1042,20 @@
 
             // (3) is only necessary to check if we actually have unstable params, so we only
             // generate that check if we need to.
-
             var shouldExecute = irOrOr(
-                scope.dirty!!.irHasDifferences(),
+                dirtyForSkipping.irHasDifferences(scope.usedParams),
                 irNot(irIsSkipping())
             )
+
+            // boolean array mapped to parameters. true indicates that the type is unstable
+            // NOTE: the unstable mask is indexed by valueParameter index, which is different
+            // than the slotIndex but that is OKAY because we only care about defaults, which
+            // also use the value parameter index.
+            val realParams = declaration.valueParameters.take(scope.realValueParamCount)
+            val unstableMask = realParams.map {
+                stabilityOf((it.varargElementType ?: it.type)).knownUnstable()
+            }.toBooleanArray()
+
             val hasAnyUnstableParams = unstableMask.any { it }
 
             // if there are unstable params, then we fence the whole expression with a check to
@@ -1195,228 +1127,202 @@
         sourceFixups.clear()
     }
 
-    @ObsoleteDescriptorBasedAPI
-    private fun buildStatementsForSkippingThisParameter(
-        thisParam: IrValueParameter,
-        isUsed: Boolean,
-        canSkipExecution: Boolean,
-        preamble: IrStatementContainer,
-        changedParam: IrChangedBitMaskValue,
-        dirty: IrChangedBitMaskValue,
-        index: Int
-    ): Boolean {
-        val type = thisParam.type
-        val isStable = stabilityOf(type).knownStable()
-
-        return when {
-            !isStable && isUsed -> false
-            isStable && isUsed && canSkipExecution && dirty is IrChangedBitMaskVariable -> {
-                preamble.statements.add(
-                    irIf(
-                        // we only call `$composer.changed(...)` on a parameter if the value came in
-                        // with an "Uncertain" state AND the value was provided. This is safe to do
-                        // because this will remain true or false for *every* execution of the
-                        // function, so we will never get a slot table misalignment as a result.
-                        condition = irIsUncertainAndStable(changedParam, index),
-                        body = dirty.irOrSetBitsAtSlot(
-                            index,
-                            irIfThenElse(
-                                context.irBuiltIns.intType,
-                                irChanged(irGet(thisParam)),
-                                // if the value has changed, update the bits in the slot to be
-                                // "Different"
-                                thenPart = irConst(ParamState.Different.bitsForSlot(index)),
-                                // if the value has not changed, update the bits in the slot to
-                                // be "Same"
-                                elsePart = irConst(ParamState.Same.bitsForSlot(index))
-                            )
-                        )
-                    )
-                )
-                true
+    private fun transformDefaults(scope: Scope.FunctionScope): Scope.ParametersScope {
+        val parameters = scope.allTrackedParams
+        val parametersScope = Scope.ParametersScope()
+        parameters.forEach { param ->
+            val defaultValue = param.defaultValue
+            if (defaultValue != null) {
+                defaultValue.expression = inScope(parametersScope) {
+                    defaultValue.expression.transform(this, null)
+                }
             }
-            !isUsed && canSkipExecution && dirty is IrChangedBitMaskVariable -> {
-                // if the param isn't used we can safely ignore it, but if we can skip the
-                // execution of the function, then we need to make sure that we are only
-                // considering the not-ignored parameters. to do this, we set the changed slot bits
-                // to Static
-                preamble.statements.add(
-                    dirty.irOrSetBitsAtSlot(
-                        index,
-                        irConst(ParamState.Static.bitsForSlot(index))
-                    )
-                )
-            }
-            // nothing changes
-            else -> canSkipExecution
         }
+        return parametersScope
     }
 
     @ObsoleteDescriptorBasedAPI
-    private fun buildStatementsForSkippingAndDefaults(
+    private fun buildPreambleStatementsAndReturnIfSkippingPossible(
         sourceElement: IrElement,
         skipPreamble: IrStatementContainer,
         bodyPreamble: IrStatementContainer,
-        canSkipExecution: Boolean,
-        parameters: List<IrValueParameter>,
+        isSkippableDeclaration: Boolean,
         scope: Scope.FunctionScope,
         dirty: IrChangedBitMaskValue,
         changedParam: IrChangedBitMaskValue,
         defaultParam: IrDefaultBitMaskValue?,
-        unstableMask: BooleanArray
-    ) {
+        defaultScope: Scope.ParametersScope
+    ): Boolean {
+        val parameters = scope.allTrackedParams
         // we default to true because the absence of a default expression we want to consider as
         // "static"
         val defaultExprIsStatic = BooleanArray(parameters.size) { true }
         val defaultExpr = Array<IrExpression?>(parameters.size) { null }
+        val stabilities = Array(parameters.size) { Stability.Unstable }
+        var mightSkip = isSkippableDeclaration
 
-        // first we create the necessary local variables for default handling.
         val setDefaults = mutableStatementContainer()
         val skipDefaults = mutableStatementContainer()
-        val parametersScope = Scope.ParametersScope()
-        parameters.forEachIndexed { index, param ->
-            val defaultValue = param.defaultValue
+//        val parametersScope = Scope.ParametersScope()
+        parameters.forEachIndexed { slotIndex, param ->
+            val defaultIndex = scope.defaultIndexForSlotIndex(slotIndex)
+            val defaultValue = param.defaultValue?.expression
             if (defaultParam != null && defaultValue != null) {
-                val transformedDefault = inScope(parametersScope) {
-                    defaultValue.expression.transform(this, null)
-                }
+//                val transformedDefault = inScope(parametersScope) {
+//                    defaultValue.expression.transform(this, null)
+//                }
 
                 // we want to call this on the transformed version.
-                defaultExprIsStatic[index] = transformedDefault.isStatic()
-                defaultExpr[index] = transformedDefault
-
-                // Generate code to reassign parameter local for default arguments.
-                if (
-                    canSkipExecution &&
-                    !defaultExprIsStatic[index] &&
-                    dirty is IrChangedBitMaskVariable
-                ) {
-                    // If we are setting the parameter to the default expression and
-                    // running the default expression again, and the expression isn't
-                    // provably static, we can't be certain that the dirty value of
-                    // SAME is going to be valid. We must mark it as UNCERTAIN. In order
-                    // to avoid slot-table misalignment issues, we must mark it as
-                    // UNCERTAIN even when we skip the defaults, so that any child
-                    // function receives UNCERTAIN vs SAME/DIFFERENT deterministically.
-                    setDefaults.statements.add(
-                        irIf(
-                            condition = irGetBit(defaultParam, index),
-                            body = irBlock(
-                                statements = listOf(
-                                    irSet(param, transformedDefault),
-                                    dirty.irSetSlotUncertain(index)
+                defaultExprIsStatic[slotIndex] = defaultValue.isStatic()
+                defaultExpr[slotIndex] = defaultValue
+                val hasStaticDefaultExpr = defaultExprIsStatic[slotIndex]
+                when {
+                    isSkippableDeclaration && !hasStaticDefaultExpr &&
+                        dirty is IrChangedBitMaskVariable -> {
+                        // If we are setting the parameter to the default expression and
+                        // running the default expression again, and the expression isn't
+                        // provably static, we can't be certain that the dirty value of
+                        // SAME is going to be valid. We must mark it as UNCERTAIN. In order
+                        // to avoid slot-table misalignment issues, we must mark it as
+                        // UNCERTAIN even when we skip the defaults, so that any child
+                        // function receives UNCERTAIN vs SAME/DIFFERENT deterministically.
+                        setDefaults.statements.add(
+                            irIf(
+                                condition = irGetBit(defaultParam, defaultIndex),
+                                body = irBlock(
+                                    statements = listOf(
+                                        irSet(param, defaultValue),
+                                        dirty.irSetSlotUncertain(slotIndex)
+                                    )
                                 )
                             )
                         )
-                    )
-                    skipDefaults.statements.add(
-                        irIf(
-                            condition = irGetBit(defaultParam, index),
-                            body = dirty.irSetSlotUncertain(index)
+                        skipDefaults.statements.add(
+                            irIf(
+                                condition = irGetBit(defaultParam, defaultIndex),
+                                body = dirty.irSetSlotUncertain(slotIndex)
+                            )
                         )
-                    )
-                } else {
-                    setDefaults.statements.add(
-                        irIf(
-                            condition = irGetBit(defaultParam, index),
-                            body = irSet(param, transformedDefault)
+                    }
+                    else -> {
+                        setDefaults.statements.add(
+                            irIf(
+                                condition = irGetBit(defaultParam, defaultIndex),
+                                body = irSet(param, defaultValue)
+                            )
                         )
-                    )
+                    }
                 }
             }
-
-            // In order to propagate the change detection we might perform on this parameter,
-            // we need to know which "slot" it is in
-            scope.remappedParams[param] = param
-            scope.paramsToSlots[param] = index
         }
+
+        parameters.forEachIndexed { slotIndex, param ->
+            val stability = stabilityOf(param.varargElementType ?: param.type)
+
+            stabilities[slotIndex] = stability
+
+            val isRequired = param.defaultValue == null
+            val isUnstable = stability.knownUnstable()
+            val isUsed = scope.usedParams[slotIndex]
+
+            if (isUsed && isUnstable && isRequired) {
+                // if it is a used + unstable parameter with no default expression, the fn
+                // will _never_ skip
+                mightSkip = false
+            }
+        }
+
         // we start the skipPreamble with all of the changed calls. These need to go at the top
         // of the function's group. Note that these end up getting called *before* default
         // expressions, but this is okay because it will only ever get called on parameters that
         // are provided to the function
-        parameters.forEachIndexed { index, param ->
+        parameters.forEachIndexed { slotIndex, param ->
             // varargs get handled separately because they will require their own groups
             if (param.isVararg) return@forEachIndexed
+            val defaultIndex = scope.defaultIndexForSlotIndex(slotIndex)
             val defaultValue = param.defaultValue
-            if (canSkipExecution && dirty is IrChangedBitMaskVariable) {
-                if (unstableMask[index]) {
-                    if (defaultParam != null && defaultValue != null) {
-                        skipPreamble.statements.add(
-                            irIf(
-                                condition = irGetBit(defaultParam, index),
-                                body = dirty.irOrSetBitsAtSlot(
-                                    index,
-                                    irConst(ParamState.Same.bitsForSlot(index))
+            val isUnstable = stabilities[slotIndex].knownUnstable()
+            val isUsed = scope.usedParams[slotIndex]
+
+            when {
+                !mightSkip || !isUsed -> {
+                    // nothing to do
+                }
+                dirty !is IrChangedBitMaskVariable -> {
+                    // this will only ever be true when mightSkip is false, but we put this
+                    // branch here so that `dirty` gets smart cast in later branches
+                }
+                isUnstable && defaultParam != null && defaultValue != null -> {
+                    // if it has a default parameter then the function can still potentially skip
+                    skipPreamble.statements.add(
+                        irIf(
+                            condition = irGetBit(defaultParam, defaultIndex),
+                            body = dirty.irOrSetBitsAtSlot(
+                                slotIndex,
+                                irConst(ParamState.Same.bitsForSlot(slotIndex))
+                            )
+                        )
+                    )
+                }
+                !isUnstable -> {
+                    val defaultValueIsStatic = defaultExprIsStatic[slotIndex]
+                    val callChanged = irChanged(irGet(param))
+                    val isChanged = if (defaultParam != null && !defaultValueIsStatic)
+                        irAndAnd(irIsProvided(defaultParam, slotIndex), callChanged)
+                    else
+                        callChanged
+                    val modifyDirtyFromChangedResult = dirty.irOrSetBitsAtSlot(
+                        slotIndex,
+                        irIfThenElse(
+                            context.irBuiltIns.intType,
+                            isChanged,
+                            // if the value has changed, update the bits in the slot to be
+                            // "Different"
+                            thenPart = irConst(ParamState.Different.bitsForSlot(slotIndex)),
+                            // if the value has not changed, update the bits in the slot to
+                            // be "Same"
+                            elsePart = irConst(ParamState.Same.bitsForSlot(slotIndex))
+                        )
+                    )
+
+                    val stmt = if (defaultParam != null && defaultValueIsStatic) {
+                        // if the default expression is "static", then we know that if we are using the
+                        // default expression, the parameter can be considered "static".
+                        irWhen(
+                            origin = IrStatementOrigin.IF,
+                            branches = listOf(
+                                irBranch(
+                                    condition = irGetBit(defaultParam, defaultIndex),
+                                    result = dirty.irOrSetBitsAtSlot(
+                                        slotIndex,
+                                        irConst(ParamState.Static.bitsForSlot(slotIndex))
+                                    )
+                                ),
+                                irBranch(
+                                    condition = irIsUncertainAndStable(changedParam, slotIndex),
+                                    result = modifyDirtyFromChangedResult
                                 )
                             )
                         )
+                    } else {
+                        // we only call `$composer.changed(...)` on a parameter if the value came in
+                        // with an "Uncertain" state AND the value was provided. This is safe to do
+                        // because this will remain true or false for *every* execution of the
+                        // function, so we will never get a slot table misalignment as a result.
+                        irIf(
+                            condition = irIsUncertainAndStable(changedParam, slotIndex),
+                            body = modifyDirtyFromChangedResult
+                        )
                     }
-
-                    // if the value is unstable, there is no reason for us to store it in the slot table
-                    return@forEachIndexed
+                    skipPreamble.statements.add(stmt)
                 }
-
-                val defaultValueIsStatic = defaultExprIsStatic[index]
-                val callChanged = irChanged(irGet(scope.remappedParams[param]!!))
-                val isChanged = if (defaultParam != null && !defaultValueIsStatic)
-                    irAndAnd(irIsProvided(defaultParam, index), callChanged)
-                else
-                    callChanged
-                val modifyDirtyFromChangedResult = dirty.irOrSetBitsAtSlot(
-                    index,
-                    irIfThenElse(
-                        context.irBuiltIns.intType,
-                        isChanged,
-                        // if the value has changed, update the bits in the slot to be
-                        // "Different"
-                        thenPart = irConst(ParamState.Different.bitsForSlot(index)),
-                        // if the value has not changed, update the bits in the slot to
-                        // be "Same"
-                        elsePart = irConst(ParamState.Same.bitsForSlot(index))
-                    )
-                )
-
-                val stmt = if (defaultParam != null && defaultValueIsStatic) {
-                    // if the default expression is "static", then we know that if we are using the
-                    // default expression, the parameter can be considered "static".
-                    irWhen(
-                        origin = IrStatementOrigin.IF,
-                        branches = listOf(
-                            irBranch(
-                                condition = irGetBit(defaultParam, index),
-                                result = dirty.irOrSetBitsAtSlot(
-                                    index,
-                                    irConst(ParamState.Static.bitsForSlot(index))
-                                )
-                            ),
-                            irBranch(
-                                condition = irIsUncertainAndStable(changedParam, index),
-                                result = modifyDirtyFromChangedResult
-                            )
-                        )
-                    )
-                } else {
-                    // we only call `$composer.changed(...)` on a parameter if the value came in
-                    // with an "Uncertain" state AND the value was provided. This is safe to do
-                    // because this will remain true or false for *every* execution of the
-                    // function, so we will never get a slot table misalignment as a result.
-                    irIf(
-                        condition = irIsUncertainAndStable(changedParam, index),
-                        body = modifyDirtyFromChangedResult
-                    )
-                }
-                skipPreamble.statements.add(stmt)
             }
         }
-        // now we handle the vararg parameters specially since it needs to create a group
-        parameters.forEachIndexed { index, param ->
-            val varargElementType = param.varargElementType ?: return@forEachIndexed
-            if (canSkipExecution && dirty is IrChangedBitMaskVariable) {
-                if (unstableMask[index]) {
-                    // if the value is unstable, there is no reason for us to store it in the slot table
-                    return@forEachIndexed
-                }
 
+        // now we handle the vararg parameters specially since it needs to create a group
+        parameters.forEachIndexed { slotIndex, param ->
+            val varargElementType = param.varargElementType ?: return@forEachIndexed
+            if (mightSkip && dirty is IrChangedBitMaskVariable) {
                 // for vararg parameters of stable type, we can store each value in the slot
                 // table, but need to generate a group since the size of the array could change
                 // over time. In the future, we may want to make an optimization where whether or
@@ -1431,7 +1337,7 @@
                 skipPreamble.statements.add(
                     irStartReplaceableGroup(
                         param,
-                        parametersScope,
+                        defaultScope,
                         irGetParamSize
                     )
                 )
@@ -1445,16 +1351,16 @@
                         irGet(param)
                     ) { loopVar ->
                         dirty.irOrSetBitsAtSlot(
-                            index,
+                            slotIndex,
                             irIfThenElse(
                                 context.irBuiltIns.intType,
                                 irChanged(irGet(loopVar)),
                                 // if the value has changed, update the bits in the slot to be
                                 // "Different".
-                                thenPart = irConst(ParamState.Different.bitsForSlot(index)),
+                                thenPart = irConst(ParamState.Different.bitsForSlot(slotIndex)),
                                 // if the value has not changed, we are still uncertain if the entire
                                 // list of values has gone unchanged or not, so we use Uncertain
-                                elsePart = irConst(ParamState.Uncertain.bitsForSlot(index))
+                                elsePart = irConst(ParamState.Uncertain.bitsForSlot(slotIndex))
                             )
                         )
                     }
@@ -1468,18 +1374,23 @@
                 // }
                 skipPreamble.statements.add(
                     irIf(
-                        condition = irIsUncertainAndStable(dirty, index),
+                        condition = irIsUncertainAndStable(dirty, slotIndex),
                         body = dirty.irOrSetBitsAtSlot(
-                            index,
-                            irConst(ParamState.Same.bitsForSlot(index))
+                            slotIndex,
+                            irConst(ParamState.Same.bitsForSlot(slotIndex))
                         )
                     )
                 )
             }
         }
+        parameters.forEach {
+            // we want to remove the default expression from the function. This will prevent
+            // the kotlin compiler from doing its own default handling, which we don't need.
+            it.defaultValue = null
+        }
         // after all of this, we need to potentially wrap the default setters in a group and if
         // statement, to make sure that defaults are only executed when they need to be.
-        if (!canSkipExecution || defaultExprIsStatic.all { it }) {
+        if (!mightSkip || defaultExprIsStatic.all { it }) {
             // if we don't skip execution ever, then we don't need these groups at all.
             // Additionally, if all of the defaults are static, we can avoid creating the groups
             // as well.
@@ -1515,6 +1426,8 @@
                 )
             )
         }
+
+        return mightSkip
     }
 
     @OptIn(ObsoleteDescriptorBasedAPI::class)
@@ -1627,11 +1540,6 @@
                 context.irBuiltIns.intType
             )
             fn.body = localIrBuilder.irBlockBody {
-
-                fun remappedParam(index: Int) = function.valueParameters[index].let {
-                    scope.remappedParams[it] ?: it
-                }
-
                 // Call the function again with the same parameters
                 +irReturn(
                     irCall(function.symbol).apply {
@@ -1650,7 +1558,7 @@
                                                 IrSpreadElementImpl(
                                                     UNDEFINED_OFFSET,
                                                     UNDEFINED_OFFSET,
-                                                    irGet(remappedParam(index))
+                                                    irGet(param)
                                                 )
                                             )
                                         )
@@ -1658,7 +1566,7 @@
                                 } else {
                                     // NOTE(lmr): should we be using the parameter here, or the temporary
                                     // with the default value?
-                                    putValueArgument(index, irGet(remappedParam(index)))
+                                    putValueArgument(index, irGet(param))
                                 }
                             }
 
@@ -2366,15 +2274,15 @@
             arg.isStatic() -> meta.isStatic = true
             arg is IrGetValue -> {
                 val owner = arg.symbol.owner
-                val found = extractParamMetaFromScopes(meta, owner)
-                if (!found) {
-                    when (owner) {
-                        is IrVariable -> {
-                            if (owner.isConst) {
-                                meta.isStatic = true
-                            } else if (!owner.isVar && owner.initializer != null) {
-                                populateParamMeta(owner.initializer!!, meta)
-                            }
+                when (owner) {
+                    is IrValueParameter -> {
+                        extractParamMetaFromScopes(meta, owner)
+                    }
+                    is IrVariable -> {
+                        if (owner.isConst) {
+                            meta.isStatic = true
+                        } else if (!owner.isVar && owner.initializer != null) {
+                            populateParamMeta(owner.initializer!!, meta)
                         }
                     }
                 }
@@ -2670,8 +2578,12 @@
                 arg is IrVararg -> {
                     inputArgs.addAll(
                         arg.elements.mapNotNull {
-                            if (it is IrSpreadElement) hasSpreadArgs = true
-                            it as? IrExpression
+                            if (it is IrSpreadElement) {
+                                hasSpreadArgs = true
+                                arg
+                            } else {
+                                it as? IrExpression
+                            }
                         }
                     )
                 }
@@ -2873,13 +2785,19 @@
 
     private fun extractParamMetaFromScopes(meta: ParamMeta, param: IrValueDeclaration): Boolean {
         var scope: Scope? = currentScope
+        val fn = param.parent
         while (scope != null) {
             when (scope) {
                 is Scope.FunctionScope -> {
-                    if (scope.remappedParams.containsValue(param)) {
-                        meta.isCertain = true
-                        meta.maskParam = scope.dirty
-                        meta.maskSlot = scope.paramsToSlots[param]!!
+                    if (scope.function == fn) {
+                        if (scope.isComposable) {
+                            val slotIndex = scope.allTrackedParams.indexOf(param)
+                            if (slotIndex != -1) {
+                                meta.isCertain = true
+                                meta.maskParam = scope.dirty
+                                meta.maskSlot = slotIndex
+                            }
+                        }
                         return true
                     }
                 }
@@ -2894,10 +2812,9 @@
         extensionParam: ParamMeta?,
         dispatchParam: ParamMeta?
     ): List<IrExpression> {
-        val thisParams = listOfNotNull(extensionParam, dispatchParam)
-        val allParams = valueParams + thisParams
+        val allParams = listOfNotNull(extensionParam) + valueParams + listOfNotNull(dispatchParam)
         // passing in 0 for thisParams since they should be included in the params list
-        val changedCount = changedParamCount(valueParams.size, thisParams.size)
+        val changedCount = changedParamCount(valueParams.size, allParams.size - valueParams.size)
         val result = mutableListOf<IrExpression>()
         for (i in 0 until changedCount) {
             val start = i * SLOTS_PER_INT
@@ -2926,7 +2843,7 @@
         // (the shift amount represented here by `x`, `y`, and `z`).
 
         // TODO: we could make some small optimization here if we have multiple values passed
-        //  from one function into another in the same order. This may not happen commonly enough
+        //  from one function into another in the same order. This may not happen commonly eugh
         //  to be worth the complication though.
 
         // NOTE: we start with 0b0 because it is important that the low bit is always 0
@@ -3022,7 +2939,8 @@
                             for (it in fn.valueParameters) {
                                 val classifier = it.type.classifierOrNull
                                 if (classifier == param.symbol) {
-                                    val parentSlot = scope.paramsToSlots[it] ?: return null
+                                    val parentSlot = scope.allTrackedParams.indexOf(it)
+                                    if (parentSlot == -1) return null
                                     return irAnd(
                                         irConst(StabilityBits.UNSTABLE.bitsForSlot(0)),
                                         maskParam.irShiftBits(parentSlot, 0)
@@ -3046,20 +2964,20 @@
     override fun visitGetValue(expression: IrGetValue): IrExpression {
         val declaration = expression.symbol.owner
         var scope: Scope? = currentScope
-        while (scope != null) {
-            if (scope is Scope.FunctionScope) {
-                if (scope.function.extensionReceiverParameter == declaration) {
-                    scope.markGetExtensionReceiver()
+        if (declaration is IrValueParameter) {
+            val fn = declaration.parent
+            while (scope != null) {
+                if (scope is Scope.FunctionScope) {
+                    if (scope.function == fn) {
+                        val index = scope.allTrackedParams.indexOf(declaration)
+                        if (index != -1) {
+                            scope.usedParams[index] = true
+                        }
+                        return expression
+                    }
                 }
-                if (scope.function.dispatchReceiverParameter == declaration) {
-                    scope.markGetDispatchReceiver()
-                }
-                val remapped = scope.remappedParams[declaration]
-                if (remapped != null) {
-                    return irGet(remapped)
-                }
+                scope = scope.parent
             }
-            scope = scope.parent
         }
         return expression
     }
@@ -3279,8 +3197,6 @@
             val function: IrFunction,
             private val transformer: ComposableFunctionBodyTransformer
         ) : BlockScope("fun ${function.name.asString()}") {
-            val remappedParams = mutableMapOf<IrValueDeclaration, IrValueDeclaration>()
-            val paramsToSlots = mutableMapOf<IrValueDeclaration, Int>()
             val isInlinedLambda = with(transformer) { function.isInlinedLambda() }
 
             private var lastTemporaryIndex: Int = 0
@@ -3311,20 +3227,6 @@
 
             var dirty: IrChangedBitMaskValue? = null
 
-            var dispatchReceiverUsed: Boolean = false
-                private set
-
-            var extensionReceiverUsed: Boolean = false
-                private set
-
-            fun markGetDispatchReceiver() {
-                dispatchReceiverUsed = true
-            }
-
-            fun markGetExtensionReceiver() {
-                extensionReceiverUsed = true
-            }
-
             // Parameter information is an index from the sorted order of the parameters to the
             // actual order. This is used to reorder the fields of the lambda class generated for
             // restart lambdas into parameter order. If all the parameters are in sorted order
@@ -3498,6 +3400,29 @@
 
             val isComposable = composerParameter != null
 
+            val allTrackedParams = listOfNotNull(
+                function.extensionReceiverParameter
+            ) + function.valueParameters.take(realValueParamCount) + listOfNotNull(
+                function.dispatchReceiverParameter
+            )
+
+            fun defaultIndexForSlotIndex(index: Int): Int {
+                return if (function.extensionReceiverParameter != null) index - 1 else index
+            }
+
+            val usedParams = BooleanArray(slotCount) { false }
+
+            init {
+                if (
+                    isComposable &&
+                    function.origin == IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA
+                ) {
+                    // in the case of a composable lambda, we want to make sure the dispatch
+                    // receiver is always marked as "used"
+                    usedParams[slotCount - 1] = true
+                }
+            }
+
             fun getNameForTemporary(nameHint: String?): String {
                 val index = nextTemporaryIndex()
                 return if (nameHint != null) "tmp${index}_$nameHint" else "tmp$index"
@@ -3792,7 +3717,10 @@
             }
         }
 
+        override var used: Boolean = false
+
         override fun irLowBit(): IrExpression {
+            used = true
             return irAnd(
                 irGet(params[0]),
                 irConst(0b1)
@@ -3800,6 +3728,7 @@
         }
 
         override fun irIsolateBitsAtSlot(slot: Int, includeStableBit: Boolean): IrExpression {
+            used = true
             // %changed and 0b11
             return irAnd(
                 irGet(params[paramIndexForSlot(slot)]),
@@ -3814,6 +3743,7 @@
         }
 
         override fun irSlotAnd(slot: Int, bits: Int): IrExpression {
+            used = true
             // %changed and 0b11
             return irAnd(
                 irGet(params[paramIndexForSlot(slot)]),
@@ -3821,7 +3751,9 @@
             )
         }
 
-        override fun irHasDifferences(): IrExpression {
+        override fun irHasDifferences(usedParams: BooleanArray): IrExpression {
+            used = true
+            require(usedParams.size == count)
             if (count == 0) {
                 // for 0 slots (no params), we can create a shortcut expression of just checking the
                 // low-bit for non-zero. Since all of the higher bits will also be 0, we can just
@@ -3836,16 +3768,23 @@
                 val start = index * SLOTS_PER_INT
                 val end = min(start + SLOTS_PER_INT, count)
 
-                // makes an int with each slot having 0b01 mask and the low bit being 0.
-                // so for 3 slots, we would get 0b 01 01 01 0.
+                // makes an int with each slot having 0b101 mask and the low bit being 0.
+                // so for 3 slots, we would get 0b 101 101 101 0.
                 // This pattern is useful because we can and + xor it with our $changed bitmask and it
-                // will only be non-zero if any of the slots were DIFFERENT or UNCERTAIN.
+                // will only be non-zero if any of the slots were DIFFERENT or UNCERTAIN or
+                // UNSTABLE.
+                // we _only_ use this pattern for the slots where the body of the function
+                // actually uses that parameter, otherwise we pass in 0b000 which will transfer
+                // none of the bits to the rhs
                 val lhs = (start until end).fold(0) { mask, slot ->
-                    mask or bitsForSlot(0b101, slot)
+                    if (usedParams[slot]) mask or bitsForSlot(0b101, slot) else mask
                 }
 
+                // we _only_ use this pattern for the slots where the body of the function
+                // actually uses that parametser, otherwise we pass in 0b000 which will transfer
+                // none of the bits to the rhs
                 val rhs = (start until end).fold(0) { mask, slot ->
-                    mask or bitsForSlot(0b001, slot)
+                    if (usedParams[slot]) mask or bitsForSlot(0b001, slot) else mask
                 }
 
                 // we use this pattern with the low bit set to 1 in the "and", and the low bit set to 0
@@ -3853,17 +3792,30 @@
                 // low bit. Since we use this calculation to determine if we need to run the body of the
                 // function, this is exactly what we want.
 
-                // $dirty and (0b 01 ... 01 1) xor (0b 01 ... 01 0)
-                irNotEqual(
-                    irXor(
+                // if the rhs is 0, that means that none of the parameters ended up getting used
+                // in the body of the function which means we can simplify the expression quite a
+                // bit. In this case we just care about if the low bit is non-zero
+                if (rhs == 0) {
+                    irNotEqual(
                         irAnd(
                             irGet(param),
-                            irConst(lhs or 0b1)
+                            irConst(1)
                         ),
-                        irConst(rhs or 0b0)
-                    ),
-                    irConst(0) // anything non-zero means we have differences
-                )
+                        irConst(0)
+                    )
+                } else {
+                    // $dirty and (0b 101 ... 101 1) xor (0b 001 ... 001 0)
+                    irNotEqual(
+                        irXor(
+                            irAnd(
+                                irGet(param),
+                                irConst(lhs or 0b1)
+                            ),
+                            irConst(rhs or 0b0)
+                        ),
+                        irConst(0) // anything non-zero means we have differences
+                    )
+                }
             }
             return if (expressions.size == 1)
                 expressions.single()
@@ -3876,6 +3828,7 @@
             isVar: Boolean,
             exactName: Boolean
         ): IrChangedBitMaskVariable {
+            used = true
             val temps = params.mapIndexed { index, param ->
                 irTemporary(
                     irGet(param),
@@ -3893,6 +3846,7 @@
             startIndex: Int,
             lowBit: Boolean
         ) {
+            used = true
             params.forEachIndexed { index, param ->
                 fn.putValueArgument(
                     startIndex + index,
@@ -3906,6 +3860,7 @@
         }
 
         override fun irShiftBits(fromSlot: Int, toSlot: Int): IrExpression {
+            used = true
             val fromSlotAdjusted = fromSlot.rem(SLOTS_PER_INT)
             val toSlotAdjusted = toSlot.rem(SLOTS_PER_INT)
             val bitsToShiftLeft = (toSlotAdjusted - fromSlotAdjusted) * BITS_PER_SLOT
@@ -3941,6 +3896,7 @@
         }
 
         override fun irOrSetBitsAtSlot(slot: Int, value: IrExpression): IrExpression {
+            used = true
             val temp = temps[paramIndexForSlot(slot)]
             return irSet(
                 temp,
@@ -3952,6 +3908,7 @@
         }
 
         override fun irSetSlotUncertain(slot: Int): IrExpression {
+            used = true
             val temp = temps[paramIndexForSlot(slot)]
             return irSet(
                 temp,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index fd62c1c..a598c6b 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -77,6 +77,7 @@
 import org.jetbrains.kotlin.ir.symbols.IrSymbol
 import org.jetbrains.kotlin.ir.types.IrType
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET
 import org.jetbrains.kotlin.ir.util.defaultType
 import org.jetbrains.kotlin.ir.util.isLocal
 import org.jetbrains.kotlin.ir.util.patchDeclarationParents
@@ -266,6 +267,8 @@
         val filePath = declaration.fileEntry.name
         val fileName = filePath.split('/').last()
         val current = context.irFactory.buildClass {
+            startOffset = SYNTHETIC_OFFSET
+            endOffset = SYNTHETIC_OFFSET
             kind = ClassKind.OBJECT
             visibility = DescriptorVisibilities.INTERNAL
             val shortName = PackagePartClassUtils.getFilePartShortName(fileName)
@@ -549,6 +552,8 @@
             visibility = DescriptorVisibilities.INTERNAL
         }.also { p ->
             p.backingField = context.irFactory.buildField {
+                startOffset = SYNTHETIC_OFFSET
+                endOffset = SYNTHETIC_OFFSET
                 name = Name.identifier(lambdaName)
                 type = lambdaType
                 visibility = DescriptorVisibilities.INTERNAL
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
index f0d2cd9..179ceee 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
@@ -1160,7 +1160,37 @@
     }
 
     override fun visitFunctionReference(expression: IrFunctionReference) {
-        print("<<FUNCTIONREF>>")
+        val function = expression.symbol.owner
+        val dispatchReceiver = expression.dispatchReceiver
+        val extensionReceiver = expression.extensionReceiver
+        val dispatchIsSpecial = dispatchReceiver.let {
+            it is IrGetValue && it.symbol.owner.name.isSpecial
+        }
+        val extensionIsSpecial = extensionReceiver.let {
+            it is IrGetValue && it.symbol.owner.name.isSpecial
+        }
+
+        if (dispatchReceiver != null && !dispatchIsSpecial) {
+            dispatchReceiver.print()
+            print("::")
+        } else if (extensionReceiver != null && !extensionIsSpecial) {
+            extensionReceiver.print()
+            print("::")
+        }
+
+        val prop = (function as? IrSimpleFunction)?.correspondingPropertySymbol?.owner
+
+        if (prop != null) {
+            val propName = prop.name.asString()
+            print(propName)
+            if (function == prop.setter) {
+                print("::set")
+            } else if (function == prop.getter) {
+                print("::get")
+            }
+        } else {
+            print(function.name.asString())
+        }
     }
 
     override fun visitInstanceInitializerCall(expression: IrInstanceInitializerCall) {
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
index 9eae1d7..7108861 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
@@ -19,6 +19,7 @@
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.desktop.AppWindow
 import androidx.compose.desktop.DesktopMaterialTheme
+import androidx.compose.desktop.LocalAppWindow
 import androidx.compose.desktop.Window
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Image
@@ -179,8 +180,10 @@
     val amount = remember { mutableStateOf(0) }
     val animation = remember { mutableStateOf(true) }
     Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
+        val window = LocalAppWindow.current.window
+        val info = "${window.renderApi} (${window.windowHandle})"
         Text(
-            text = "Привет! 你好! Desktop Compose ${amount.value}",
+            text = "Привет! 你好! Desktop Compose use $info: ${amount.value}",
             color = Color.Black,
             modifier = Modifier
                 .background(Color.Blue)
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
index a7c7475..4342167 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
@@ -55,6 +55,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import org.junit.Assert.assertNotEquals
 import java.util.concurrent.CountDownLatch
@@ -1807,6 +1808,7 @@
     }
 
     @Test
+    @FlakyTest(bugId = 183713100)
     fun testModifiers_doNotCauseUnnecessaryRemeasure() {
         var first by mutableStateOf(true)
         var totalMeasures = 0
diff --git a/compose/foundation/foundation/api/1.0.0-beta04.txt b/compose/foundation/foundation/api/1.0.0-beta04.txt
index bff54f1..f85c7ad 100644
--- a/compose/foundation/foundation/api/1.0.0-beta04.txt
+++ b/compose/foundation/foundation/api/1.0.0-beta04.txt
@@ -425,19 +425,6 @@
 
 }
 
-package androidx.compose.foundation.legacygestures {
-
-  public final class DragGestureFilterKt {
-  }
-
-  public final class PressIndicatorGestureFilterKt {
-  }
-
-  public final class TapGestureFilterKt {
-  }
-
-}
-
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
@@ -648,6 +635,9 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class LongPressTextDragObserverKt {
+  }
+
   public final class MaxLinesHeightModifierKt {
   }
 
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
new file mode 100644
index 0000000..dd3d988
--- /dev/null
+++ b/compose/foundation/foundation/api/current.ignore
@@ -0,0 +1,10 @@
+// Baseline format: 1.0
+RemovedClass: androidx.compose.foundation.legacygestures.PressIndicatorGestureFilterKt:
+    Removed class androidx.compose.foundation.legacygestures.PressIndicatorGestureFilterKt
+RemovedClass: androidx.compose.foundation.legacygestures.TapGestureFilterKt:
+    Removed class androidx.compose.foundation.legacygestures.TapGestureFilterKt
+RemovedClass: androidx.compose.foundation.legacygestures.DragGestureFilterKt:
+    Removed class androidx.compose.foundation.legacygestures.DragGestureFilterKt
+RemovedPackage: androidx.compose.foundation.legacygestures:
+    Removed package androidx.compose.foundation.legacygestures
+
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index bff54f1..f85c7ad 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -425,19 +425,6 @@
 
 }
 
-package androidx.compose.foundation.legacygestures {
-
-  public final class DragGestureFilterKt {
-  }
-
-  public final class PressIndicatorGestureFilterKt {
-  }
-
-  public final class TapGestureFilterKt {
-  }
-
-}
-
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
@@ -648,6 +635,9 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class LongPressTextDragObserverKt {
+  }
+
   public final class MaxLinesHeightModifierKt {
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta04.txt b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta04.txt
index d9e6a73..7453b13 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta04.txt
@@ -458,19 +458,6 @@
 
 }
 
-package androidx.compose.foundation.legacygestures {
-
-  public final class DragGestureFilterKt {
-  }
-
-  public final class PressIndicatorGestureFilterKt {
-  }
-
-  public final class TapGestureFilterKt {
-  }
-
-}
-
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
@@ -684,6 +671,9 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class LongPressTextDragObserverKt {
+  }
+
   public final class MaxLinesHeightModifierKt {
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index d9e6a73..7453b13 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -458,19 +458,6 @@
 
 }
 
-package androidx.compose.foundation.legacygestures {
-
-  public final class DragGestureFilterKt {
-  }
-
-  public final class PressIndicatorGestureFilterKt {
-  }
-
-  public final class TapGestureFilterKt {
-  }
-
-}
-
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
@@ -684,6 +671,9 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class LongPressTextDragObserverKt {
+  }
+
   public final class MaxLinesHeightModifierKt {
   }
 
diff --git a/compose/foundation/foundation/api/restricted_1.0.0-beta04.txt b/compose/foundation/foundation/api/restricted_1.0.0-beta04.txt
index bff54f1..f85c7ad 100644
--- a/compose/foundation/foundation/api/restricted_1.0.0-beta04.txt
+++ b/compose/foundation/foundation/api/restricted_1.0.0-beta04.txt
@@ -425,19 +425,6 @@
 
 }
 
-package androidx.compose.foundation.legacygestures {
-
-  public final class DragGestureFilterKt {
-  }
-
-  public final class PressIndicatorGestureFilterKt {
-  }
-
-  public final class TapGestureFilterKt {
-  }
-
-}
-
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
@@ -648,6 +635,9 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class LongPressTextDragObserverKt {
+  }
+
   public final class MaxLinesHeightModifierKt {
   }
 
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
new file mode 100644
index 0000000..1db0871
--- /dev/null
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+RemovedClass: androidx.compose.foundation.legacygestures.PressIndicatorGestureFilterKt:
+    Removed class androidx.compose.foundation.legacygestures.PressIndicatorGestureFilterKt
+RemovedClass: androidx.compose.foundation.legacygestures.TapGestureFilterKt:
+    Removed class androidx.compose.foundation.legacygestures.TapGestureFilterKt
+RemovedClass: androidx.compose.foundation.legacygestures.DragGestureFilterKt:
+    Removed class androidx.compose.foundation.legacygestures.DragGestureFilterKt
+RemovedPackage: androidx.compose.foundation.legacygestures:
+    Removed package androidx.compose.foundation.legacygestures
\ No newline at end of file
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index bff54f1..f85c7ad 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -425,19 +425,6 @@
 
 }
 
-package androidx.compose.foundation.legacygestures {
-
-  public final class DragGestureFilterKt {
-  }
-
-  public final class PressIndicatorGestureFilterKt {
-  }
-
-  public final class TapGestureFilterKt {
-  }
-
-}
-
 package androidx.compose.foundation.selection {
 
   public final class SelectableGroupKt {
@@ -648,6 +635,9 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class LongPressTextDragObserverKt {
+  }
+
   public final class MaxLinesHeightModifierKt {
   }
 
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextBasicBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
similarity index 98%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextBasicBenchmark.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
index 02b991d..6ddf510 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextBasicBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
 
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkDrawPerf
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
index 3e758b9..14f1941 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.test.filters.SmallTest
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.RuleChain
@@ -77,7 +76,6 @@
      * given input. This is the time taken to call the [BasicTextField] composable function.
      */
     @Test
-    @Ignore("b/170960653")
     fun first_compose() {
         benchmarkRule.benchmarkFirstComposeFast(caseFactory)
     }
@@ -88,7 +86,6 @@
      * [BasicTextField] composable.
      */
     @Test
-    @Ignore("b/170960653")
     fun first_measure() {
         benchmarkRule.benchmarkFirstMeasureFast(caseFactory)
     }
@@ -98,7 +95,6 @@
      * given input.
      */
     @Test
-    @Ignore("b/170960653")
     fun first_layout() {
         benchmarkRule.benchmarkFirstLayoutFast(caseFactory)
     }
@@ -108,7 +104,6 @@
      * input.
      */
     @Test
-    @Ignore("b/170960653")
     fun first_draw() {
         benchmarkRule.benchmarkFirstDrawFast(caseFactory)
     }
@@ -118,7 +113,6 @@
      * constrains changed. This is mainly the time used to re-measure and re-layout the composable.
      */
     @Test
-    @Ignore("b/170960653")
     fun layout() {
         benchmarkRule.benchmarkLayoutPerf(caseFactory)
     }
@@ -127,7 +121,6 @@
      * Measure the time taken by redrawing the [BasicTextField] composable.
      */
     @Test
-    @Ignore("b/170960653")
     fun draw() {
         benchmarkRule.benchmarkDrawPerf(caseFactory)
     }
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
index 960dc16..56ed53c 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
@@ -21,7 +21,9 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.mutableStateOf
@@ -72,7 +74,7 @@
     @Composable
     override fun ContentWrappers(content: @Composable () -> Unit) {
         Column(
-            modifier = Modifier.width(width)
+            modifier = Modifier.width(width).verticalScroll(rememberScrollState())
         ) {
             CompositionLocalProvider(LocalTextInputService provides textInputService) {
                 content()
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextInColumnTestCase.kt
similarity index 90%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnTestCase.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextInColumnTestCase.kt
index 6e40cd6..e0662cc 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextInColumnTestCase.kt
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
 
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
@@ -53,6 +55,7 @@
     override fun ContentWrappers(content: @Composable () -> Unit) {
         Column(
             modifier = Modifier.wrapContentSize(Alignment.Center).width(width)
+                .verticalScroll(rememberScrollState())
         ) {
             content()
         }
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextMultiStyleBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextMultiStyleBenchmark.kt
similarity index 98%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextMultiStyleBenchmark.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextMultiStyleBenchmark.kt
index ce699dd..5995d6d 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextMultiStyleBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextMultiStyleBenchmark.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
 
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.benchmarkDrawPerf
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextBenchmark.kt
similarity index 97%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextBenchmark.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextBenchmark.kt
index 01310a5..918e3a1 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextBenchmark.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
 
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextTestCase.kt
similarity index 90%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextTestCase.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextTestCase.kt
index a8b99cb..c4ea2ab 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextTestCase.kt
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
 
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
@@ -47,6 +49,7 @@
     override fun Content() {
         Column(
             modifier = Modifier.wrapContentSize(Alignment.Center).width(width)
+                .verticalScroll(rememberScrollState())
         ) {
             for (text in texts) {
                 Text(text = text.value, color = Color.Black, fontSize = fontSize)
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerBenchmark.kt
new file mode 100644
index 0000000..599098f
--- /dev/null
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerBenchmark.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.benchmark.text.selection
+
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkDrawPerf
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
+import androidx.test.filters.SmallTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+class SelectionContainerBenchmark(private val childrenCount: Int) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(
+            name = "childrenCount={0}"
+        )
+        fun initParameters() = arrayOf(1, 10, 20)
+    }
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+    private val caseFactory = { SelectionContainerTestCase(childrenCount) }
+
+    @Test
+    fun first_compose() {
+        benchmarkRule.benchmarkFirstCompose(caseFactory)
+    }
+
+    @Test
+    fun first_measure() {
+        benchmarkRule.benchmarkFirstMeasure(caseFactory)
+    }
+
+    @Test
+    fun first_layout() {
+        benchmarkRule.benchmarkFirstLayout(caseFactory)
+    }
+
+    @Test
+    fun first_draw() {
+        benchmarkRule.benchmarkFirstDraw(caseFactory)
+    }
+
+    @Test
+    fun layout() {
+        benchmarkRule.benchmarkLayoutPerf(caseFactory)
+    }
+
+    @Test
+    fun draw() {
+        benchmarkRule.benchmarkDrawPerf(caseFactory)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
new file mode 100644
index 0000000..e94b226
--- /dev/null
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.benchmark.text.selection
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.sp
+
+class SelectionContainerTestCase(private val childrenCount: Int) : ComposeTestCase {
+    @Composable
+    override fun Content() {
+        SelectionContainer {
+            Column {
+                repeat(childrenCount) {
+                    Text(
+                        text = "Hello World Hello World Hello W",
+                        style = TextStyle(fontSize = 20.sp)
+                    )
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/CapitalizationAutoCorrectDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/CapitalizationAutoCorrectDemo.kt
index 56f1900..816b805 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/CapitalizationAutoCorrectDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/CapitalizationAutoCorrectDemo.kt
@@ -97,7 +97,7 @@
         modifier = demoTextFieldModifiers.defaultMinSize(100.dp),
         value = state,
         keyboardOptions = data.keyboardOptions,
-        keyboardActions = KeyboardActions { keyboardController?.hideSoftwareKeyboard() },
+        keyboardActions = KeyboardActions { keyboardController?.hide() },
         onValueChange = { state = it },
         textStyle = TextStyle(fontSize = fontSize8),
         cursorBrush = SolidColor(Color.Red)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
index e908c57..edc57c1 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
@@ -92,7 +92,7 @@
             keyboardType = keyboardType,
             imeAction = imeAction
         ),
-        keyboardActions = KeyboardActions { keyboardController?.hideSoftwareKeyboard() },
+        keyboardActions = KeyboardActions { keyboardController?.hide() },
         onValueChange = { state.value = it },
         textStyle = TextStyle(fontSize = fontSize8),
     )
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/KeyboardSingleLineDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/KeyboardSingleLineDemo.kt
index 24cd4c9..0113667 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/KeyboardSingleLineDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/KeyboardSingleLineDemo.kt
@@ -125,7 +125,7 @@
         modifier = demoTextFieldModifiers.defaultMinSize(100.dp),
         value = state.value,
         keyboardOptions = data.keyboardOptions,
-        keyboardActions = KeyboardActions { keyboardController?.hideSoftwareKeyboard() },
+        keyboardActions = KeyboardActions { keyboardController?.hide() },
         singleLine = data.singleLine,
         onValueChange = { state.value = it },
         textStyle = TextStyle(fontSize = fontSize8),
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index b53e3ce..f3e0470 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -41,6 +41,7 @@
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.doubleClick
 import androidx.compose.ui.test.down
+import androidx.compose.ui.test.cancel
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.longClick
 import androidx.compose.ui.test.onNodeWithTag
@@ -467,6 +468,57 @@
     }
 
     @Test
+    fun clickableTest_interactionSource_cancelledGesture() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                BasicText(
+                    "ClickableText",
+                    modifier = Modifier
+                        .testTag("myClickable")
+                        .combinedClickable(
+                            interactionSource = interactionSource,
+                            indication = null
+                        ) {}
+                )
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope!!.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.onNodeWithTag("myClickable")
+            .performGesture { down(center) }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java)
+        }
+
+        rule.onNodeWithTag("myClickable")
+            .performGesture { cancel() }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java)
+            assertThat(interactions[1]).isInstanceOf(PressInteraction.Cancel::class.java)
+            assertThat((interactions[1] as PressInteraction.Cancel).press)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    @Test
     fun clickableTest_interactionSource_resetWhenDisposed() {
         val interactionSource = MutableInteractionSource()
         var emitClickableText by mutableStateOf(true)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
index 9f259df..b24e18c 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
@@ -126,14 +127,14 @@
         }
     }
 
+    @Composable
+    fun ObservingFun(state: LazyListState, currentInfo: StableRef<LazyListLayoutInfo?>) {
+        currentInfo.value = state.layoutInfo
+    }
     @Test
     fun visibleItemsAreObservableWhenWeScroll() {
         lateinit var state: LazyListState
-        var currentInfo: LazyListLayoutInfo? = null
-        @Composable
-        fun observingFun() {
-            currentInfo = state.layoutInfo
-        }
+        val currentInfo = StableRef<LazyListLayoutInfo?>(null)
         rule.setContent {
             LazyColumn(
                 state = rememberLazyListState().also { state = it },
@@ -144,20 +145,20 @@
                     Box(Modifier.requiredSize(itemSizeDp))
                 }
             }
-            observingFun()
+            ObservingFun(state, currentInfo)
         }
 
         rule.runOnIdle {
             // empty it here and scrolling should invoke observingFun again
-            currentInfo = null
+            currentInfo.value = null
             runBlocking {
                 state.scrollToItem(1, 0)
             }
         }
 
         rule.runOnIdle {
-            assertThat(currentInfo).isNotNull()
-            currentInfo!!.assertVisibleItems(count = 4, startIndex = 1)
+            assertThat(currentInfo.value).isNotNull()
+            currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
         }
     }
 
@@ -291,3 +292,6 @@
         }
     }
 }
+
+@Stable
+class StableRef<T>(var value: T)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt
new file mode 100644
index 0000000..f282c7d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.text.selection.BASIC_MEASURE_FONT
+import androidx.compose.foundation.text.selection.TestFontResourceLoader
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(InternalFoundationTextApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TextDelegateWidthWithLetterSpacingTest {
+    private val fontFamily = BASIC_MEASURE_FONT.toFontFamily()
+
+    /**
+     * values are exact values for the repro case (on Pixel4, Android 11)
+     */
+    private val density = Density(3.051f, 1.15f)
+    private val letterSpacing = 0.4.sp
+    private val lineHeight = 16.sp
+    private val fontSize = 12.sp
+    private val context = InstrumentationRegistry.getInstrumentation().context
+    private val resourceLoader = TestFontResourceLoader(context)
+
+    @Test
+    fun letterSpacing_and_lineHeight() {
+        assertLineCount(
+            TextStyle(letterSpacing = letterSpacing, lineHeight = lineHeight)
+        )
+    }
+
+    @Test
+    fun only_letterSpacing() {
+        assertLineCount(TextStyle(letterSpacing = letterSpacing))
+    }
+
+    @Test
+    fun only_lineHeight() {
+        assertLineCount(TextStyle(lineHeight = lineHeight))
+    }
+
+    @Test
+    fun no_lineHeight_or_letterSpacing() {
+        assertLineCount(TextStyle())
+    }
+
+    private fun assertLineCount(style: TextStyle) {
+        val textDelegate = TextDelegate(
+            text = AnnotatedString(text = "This is a callout message"),
+            style = style.copy(
+                fontFamily = fontFamily,
+                fontSize = fontSize
+            ),
+            softWrap = true,
+            overflow = TextOverflow.Clip,
+            density = density,
+            resourceLoader = resourceLoader
+        )
+        val layoutResult = textDelegate.layout(Constraints(), LayoutDirection.Ltr)
+        assertThat(layoutResult.lineCount).isEqualTo(1)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt
new file mode 100644
index 0000000..1e643bc
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.text.selection.BaseTextPreparedSelection
+import androidx.compose.foundation.text.selection.TextFieldPreparedSelection
+import androidx.compose.foundation.text.selection.TextPreparedSelection
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.test.R
+import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class TextPreparedSelectionTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun textSelection_leftRightMovements() {
+        selectionTest("abc") {
+            it.moveCursorRight()
+            expectedSelection(cursorAt('b'))
+            it.moveCursorRight()
+            expectedSelection(cursorAt('c'))
+            it.moveCursorRight()
+            expectedSelection(cursorAfter('c'))
+            it.moveCursorRight()
+            expectedSelection(cursorAfter('c'))
+            it.moveCursorLeft()
+            expectedSelection(cursorAt('c'))
+        }
+    }
+
+    @Test
+    fun textSelection_leftRightMovements_rtl() {
+        selectionTest("\u0671\u0679\u0683", rtl = true) {
+            expectedSelection(cursorAt('\u0671'))
+            it.moveCursorLeft()
+            expectedSelection(cursorAt('\u0679'))
+            it.moveCursorLeft()
+            expectedSelection(cursorAt('\u0683'))
+            it.moveCursorRight()
+            expectedSelection(cursorAt('\u0679'))
+        }
+    }
+
+    @Test
+    fun textSelection_leftRightMovements_bidi() {
+        selectionTest("ab \u0671\u0679\u0683 cd") {
+            it.moveCursorRight()
+            expectedSelection(TextRange(1))
+            it.moveCursorRight()
+            expectedSelection(TextRange(2))
+            it.moveCursorRight()
+            expectedSelection(TextRange(3))
+            it.moveCursorRight()
+            expectedSelection(TextRange(4))
+        }
+    }
+
+    @Test
+    fun textSelection_byWordMovements() {
+        selectionTest("abc def\n\ngi") {
+            it.moveCursorRightByWord()
+            expectedSelection(cursorAfter('c'))
+            it.moveCursorRightByWord()
+            expectedSelection(cursorAfter('f'))
+            it.moveCursorLeftByWord()
+            expectedSelection(cursorAt('d'))
+            it.moveCursorRightByWord()
+            expectedSelection(cursorAfter('f'))
+            it.moveCursorRightByWord()
+            expectedSelection(cursorAfter('i'))
+        }
+    }
+
+    @Test
+    fun textSelection_lineMovements() {
+        selectionTest("ab\ncde\n\ngi", initSelection = TextRange(1)) {
+            it.moveCursorDownByLine()
+            expectedSelection(cursorAt('d'))
+            it.moveCursorDownByLine()
+            // at empty line
+            expectedSelection(TextRange(7))
+            it.moveCursorDownByLine()
+            // cursor should be at "cached" x-position
+            expectedSelection(cursorAt('i'))
+            it.moveCursorDownByLine()
+            expectedSelection(cursorAfter('i'))
+            it.moveCursorUpByLine()
+            it.moveCursorUpByLine()
+            // and again, it should be recovered at "cached" x-position
+            expectedSelection(cursorAt('d'))
+            it.moveCursorLeft()
+            it.moveCursorUpByLine()
+            // after horizontal move, "cached" x-position should be reset
+            expectedSelection(cursorAt('a'))
+        }
+    }
+
+    private inner class SelectionScope<T : BaseTextPreparedSelection<T>>(
+        val prepared: BaseTextPreparedSelection<T>
+    ) {
+        fun expectedText(text: String) {
+            rule.runOnIdle {
+                Truth.assertThat(prepared.text).isEqualTo(text)
+            }
+        }
+
+        fun expectedSelection(selection: TextRange) {
+            rule.runOnIdle {
+                Truth.assertThat(prepared.selection).isEqualTo(selection)
+            }
+        }
+
+        fun cursorAt(char: Char) =
+            TextRange(prepared.text.indexOf(char))
+
+        fun cursorAfter(char: Char) =
+            TextRange(prepared.text.indexOf(char) + 1)
+    }
+
+    private fun selectionTest(
+        initText: String = "",
+        initSelection: TextRange = TextRange(0),
+        rtl: Boolean = false,
+        test: SelectionScope<TextPreparedSelection>.(TextPreparedSelection) -> Unit
+    ) {
+        var textLayout: TextLayoutResult? = null
+        val direction = if (rtl) {
+            LayoutDirection.Rtl
+        } else {
+            LayoutDirection.Ltr
+        }
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides direction) {
+                BasicText(
+                    text = initText,
+                    style = TextStyle(
+                        fontFamily = Font(
+                            R.font.sample_font,
+                            FontWeight.Normal,
+                            FontStyle.Normal
+                        ).toFontFamily()
+                    ),
+                    onTextLayout = { textLayout = it }
+                )
+            }
+        }
+
+        val prepared = TextPreparedSelection(
+            originalText = AnnotatedString(initText),
+            originalSelection = initSelection,
+            layoutResult = textLayout!!
+        )
+
+        test(SelectionScope(prepared), prepared)
+    }
+
+    private fun textFieldSelectionTest(
+        initText: String = "",
+        initSelection: TextRange = TextRange(0),
+        test: SelectionScope<TextFieldPreparedSelection>.(TextFieldPreparedSelection) -> Unit
+    ) {
+        var textLayout: TextLayoutResult? = null
+        rule.setContent {
+            BasicText(
+                text = initText,
+                style = TextStyle(
+                    fontFamily = Font(
+                        R.font.sample_font,
+                        FontWeight.Normal,
+                        FontStyle.Normal
+                    ).toFontFamily()
+                ),
+                onTextLayout = { textLayout = it }
+            )
+        }
+
+        val prepared = TextFieldPreparedSelection(
+            currentValue = TextFieldValue(initText, initSelection),
+            layoutResultProxy = TextLayoutResultProxy(textLayout!!)
+        )
+
+        test(SelectionScope(prepared), prepared)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
index db16b94..a55c0cf 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
@@ -21,6 +21,7 @@
 import android.view.KeyEvent.META_SHIFT_ON
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.MutableState
@@ -36,8 +37,16 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.performKeyPress
 import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.test.R
+import androidx.compose.ui.text.font.toFontFamily
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth
@@ -89,6 +98,17 @@
     }
 
     @Test
+    fun textField_linesNavigation_cache() {
+        keysSequenceTest(initText = "hello\n\nworld") {
+            Key.DirectionRight.downAndUp()
+            Key.DirectionDown.downAndUp()
+            Key.DirectionDown.downAndUp()
+            Key.Zero.downAndUp()
+            expectedText("hello\n\nw0orld")
+        }
+    }
+
+    @Test
     fun textField_newLine() {
         keysSequenceTest(initText = "hello") {
             Key.Enter.downAndUp()
@@ -228,6 +248,17 @@
         }
     }
 
+    @Test
+    fun textField_pageNavigation() {
+        keysSequenceTest(
+            initText = "1\n2\n3\n4\n5",
+            modifier = Modifier.requiredSize(30.dp)
+        ) {
+            Key.PageDown.downAndUp()
+            expectedSelection(TextRange(4))
+        }
+    }
+
     private inner class SequenceScope(
         val state: MutableState<TextFieldValue>,
         val nodeGetter: () -> SemanticsNodeInteraction
@@ -260,6 +291,7 @@
 
     private fun keysSequenceTest(
         initText: String = "",
+        modifier: Modifier = Modifier.fillMaxSize(),
         sequence: SequenceScope.() -> Unit
     ) {
         val inputService = TextInputService(mock())
@@ -272,7 +304,15 @@
             ) {
                 BasicTextField(
                     value = state.value,
-                    modifier = Modifier.fillMaxSize().focusRequester(focusFequester),
+                    textStyle = TextStyle(
+                        fontFamily = Font(
+                            R.font.sample_font,
+                            FontWeight.Normal,
+                            FontStyle.Normal
+                        ).toFontFamily(),
+                        fontSize = 10.sp
+                    ),
+                    modifier = modifier.focusRequester(focusFequester),
                     onValueChange = {
                         state.value = it
                     }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
index e8a0677..9dddcdbe 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
@@ -18,8 +18,6 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
-import androidx.compose.ui.AbsoluteAlignment
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.Offset
@@ -27,12 +25,7 @@
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.LayoutModifier
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.text.style.ResolvedTextDirection
-import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
@@ -41,7 +34,6 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.window.Popup
 import androidx.compose.ui.window.PopupPositionProvider
-import kotlin.math.max
 import kotlin.math.roundToInt
 
 @Composable
@@ -175,59 +167,33 @@
     content: @Composable () -> Unit
 ) {
     val offset = (if (isStartHandle) startHandlePosition else endHandlePosition) ?: return
+    val left = isLeft(
+        isStartHandle = isStartHandle,
+        directions = directions,
+        handlesCrossed = handlesCrossed
+    )
 
-    SimpleLayout(AllowZeroSize) {
-        val left = isLeft(
-            isStartHandle = isStartHandle,
-            directions = directions,
-            handlesCrossed = handlesCrossed
-        )
-        val alignment = if (left) AbsoluteAlignment.TopRight else AbsoluteAlignment.TopLeft
+    val intOffset = IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
 
-        val intOffset = IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
-
-        val popupPositioner = remember(alignment, intOffset) {
-            SelectionHandlePositionProvider(alignment, intOffset)
-        }
-
-        Popup(
-            popupPositionProvider = popupPositioner,
-            content = content
-        )
+    val popupPositioner = remember(left, intOffset) {
+        SelectionHandlePositionProvider(left, intOffset)
     }
+
+    Popup(
+        popupPositionProvider = popupPositioner,
+        content = content
+    )
 }
 
 /**
- * This modifier allows the content to measure at its desired size without regard for the incoming
- * measurement [minimum width][Constraints.minWidth] or [minimum height][Constraints.minHeight]
- * constraints.
- *
- * The same as "wrapContentSize" in foundation-layout, which we cannot use in this module.
- */
-private object AllowZeroSize : LayoutModifier {
-    override fun MeasureScope.measure(
-        measurable: Measurable,
-        constraints: Constraints
-    ): MeasureResult {
-        val placeable = measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
-        return layout(
-            max(constraints.minWidth, placeable.width),
-            max(constraints.minHeight, placeable.height)
-        ) {
-            placeable.place(0, 0)
-        }
-    }
-}
-
-/**
- * This is a copy of "AlignmentOffsetPositionProvider" class in Popup, with some
- * change at "resolvedOffset" value.
- *
- * This is for [SelectionHandlePopup] only.
+ * This [PopupPositionProvider] for [SelectionHandlePopup]. It will position the selection handle
+ * to the [offset] in its anchor layout. For left selection handle, the right top corner will be
+ * positioned to [offset]. For right selection handle, the left top corner will be positioned to
+ * [offset].
  */
 /*@VisibleForTesting*/
 internal class SelectionHandlePositionProvider(
-    val alignment: Alignment,
+    val isLeft: Boolean,
     val offset: IntOffset
 ) : PopupPositionProvider {
     override fun calculatePosition(
@@ -236,36 +202,17 @@
         layoutDirection: LayoutDirection,
         popupContentSize: IntSize
     ): IntOffset {
-        // TODO: Decide which is the best way to round to result without reimplementing Alignment.align
-        var popupPosition = IntOffset(0, 0)
-
-        // Get the aligned point inside the parent
-        val parentAlignmentPoint = alignment.align(
-            IntSize.Zero,
-            IntSize(anchorBounds.width, anchorBounds.height),
-            layoutDirection
-        )
-        // Get the aligned point inside the child
-        val relativePopupPos = alignment.align(
-            IntSize.Zero,
-            IntSize(popupContentSize.width, popupContentSize.height),
-            layoutDirection
-        )
-
-        // Add the position of the parent
-        popupPosition += IntOffset(anchorBounds.left, anchorBounds.top)
-
-        // Add the distance between the parent's top left corner and the alignment point
-        popupPosition += parentAlignmentPoint
-
-        // Subtract the distance between the children's top left corner and the alignment point
-        popupPosition -= IntOffset(relativePopupPos.x, relativePopupPos.y)
-
-        // Add the user offset
-        val resolvedOffset = IntOffset(offset.x, offset.y)
-        popupPosition += resolvedOffset
-
-        return popupPosition
+        return if (isLeft) {
+            IntOffset(
+                x = anchorBounds.left + offset.x - popupContentSize.width,
+                y = anchorBounds.top + offset.y
+            )
+        } else {
+            IntOffset(
+                x = anchorBounds.left + offset.x,
+                y = anchorBounds.top + offset.y
+            )
+        }
     }
 }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index 95c6335..04c9bddb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -16,19 +16,20 @@
 
 package androidx.compose.foundation
 
+import androidx.compose.foundation.gestures.PressGestureScope
+import androidx.compose.foundation.gestures.detectTapAndPress
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
-import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
-import androidx.compose.foundation.legacygestures.tapGestureFilter
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.Role
@@ -37,7 +38,6 @@
 import androidx.compose.ui.semantics.onLongClick
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
-import kotlinx.coroutines.launch
 
 /**
  * Configure component to receive clicks via input or accessibility "click" event.
@@ -109,7 +109,6 @@
  * to describe the element or do customizations
  * @param onClick will be called when user clicks on the element
  */
-@Suppress("DEPRECATION")
 fun Modifier.clickable(
     interactionSource: MutableInteractionSource,
     indication: Indication?,
@@ -119,59 +118,24 @@
     onClick: () -> Unit
 ) = composed(
     factory = {
-        val scope = rememberCoroutineScope()
+        val onClickState = rememberUpdatedState(onClick)
         val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
-        val interactionUpdate =
-            if (enabled) {
-                Modifier.pressIndicatorGestureFilter(
-                    onStart = {
-                        scope.launch {
-                            // Remove any old interactions if we didn't fire stop / cancel properly
-                            pressedInteraction.value?.let { oldValue ->
-                                val interaction = PressInteraction.Cancel(oldValue)
-                                interactionSource.emit(interaction)
-                                pressedInteraction.value = null
-                            }
-                            val interaction = PressInteraction.Press(it)
-                            interactionSource.emit(interaction)
-                            pressedInteraction.value = interaction
-                        }
+        val gesture = if (enabled) {
+            PressedInteractionSourceDisposableEffect(interactionSource, pressedInteraction)
+            Modifier.pointerInput(interactionSource) {
+                detectTapAndPress(
+                    onPress = { offset ->
+                        handlePressInteraction(offset, interactionSource, pressedInteraction)
                     },
-                    onStop = {
-                        scope.launch {
-                            pressedInteraction.value?.let {
-                                val interaction = PressInteraction.Release(it)
-                                interactionSource.emit(interaction)
-                                pressedInteraction.value = null
-                            }
-                        }
-                    },
-                    onCancel = {
-                        scope.launch {
-                            pressedInteraction.value?.let {
-                                val interaction = PressInteraction.Cancel(it)
-                                interactionSource.emit(interaction)
-                                pressedInteraction.value = null
-                            }
-                        }
-                    }
+                    onTap = { onClickState.value.invoke() }
                 )
-            } else {
-                Modifier
             }
-        val tap = if (enabled) tapGestureFilter(onTap = { onClick() }) else Modifier
-        DisposableEffect(interactionSource) {
-            onDispose {
-                pressedInteraction.value?.let { oldValue ->
-                    val interaction = PressInteraction.Cancel(oldValue)
-                    interactionSource.tryEmit(interaction)
-                    pressedInteraction.value = null
-                }
-            }
+        } else {
+            Modifier
         }
         Modifier
             .genericClickableWithoutGesture(
-                gestureModifiers = Modifier.then(interactionUpdate).then(tap),
+                gestureModifiers = gesture,
                 interactionSource = interactionSource,
                 indication = indication,
                 enabled = enabled,
@@ -292,12 +256,11 @@
     onClick: () -> Unit
 ) = composed(
     factory = {
-        val scope = rememberCoroutineScope()
         val onClickState = rememberUpdatedState(onClick)
-        val interactionSourceState = rememberUpdatedState(interactionSource)
         val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
         val gesture = if (enabled) {
-            Modifier.pointerInput(onDoubleClick, onLongClick) {
+            PressedInteractionSourceDisposableEffect(interactionSource, pressedInteraction)
+            Modifier.pointerInput(onDoubleClick, onLongClick, interactionSource) {
                 detectTapGestures(
                     onDoubleTap = if (onDoubleClick != null) {
                         { onDoubleClick() }
@@ -309,26 +272,8 @@
                     } else {
                         null
                     },
-                    onPress = {
-                        scope.launch {
-                            // Remove any old interactions if we didn't fire stop / cancel properly
-                            pressedInteraction.value?.let { oldValue ->
-                                val interaction = PressInteraction.Cancel(oldValue)
-                                interactionSourceState.value.emit(interaction)
-                                pressedInteraction.value = null
-                            }
-                            val interaction = PressInteraction.Press(it)
-                            interactionSourceState.value.emit(interaction)
-                            pressedInteraction.value = interaction
-                        }
-                        tryAwaitRelease()
-                        scope.launch {
-                            pressedInteraction.value?.let { oldValue ->
-                                val interaction = PressInteraction.Release(oldValue)
-                                interactionSourceState.value.emit(interaction)
-                                pressedInteraction.value = null
-                            }
-                        }
+                    onPress = { offset ->
+                        handlePressInteraction(offset, interactionSource, pressedInteraction)
                     },
                     onTap = { onClickState.value.invoke() }
                 )
@@ -336,17 +281,6 @@
         } else {
             Modifier
         }
-        DisposableEffect(interactionSource) {
-            onDispose {
-                scope.launch {
-                    pressedInteraction.value?.let { oldValue ->
-                        val interaction = PressInteraction.Cancel(oldValue)
-                        interactionSourceState.value.emit(interaction)
-                        pressedInteraction.value = null
-                    }
-                }
-            }
-        }
         Modifier
             .genericClickableWithoutGesture(
                 gestureModifiers = gesture,
@@ -375,6 +309,41 @@
 )
 
 @Composable
+internal fun PressedInteractionSourceDisposableEffect(
+    interactionSource: MutableInteractionSource,
+    pressedInteraction: MutableState<PressInteraction.Press?>
+) {
+    DisposableEffect(interactionSource) {
+        onDispose {
+            pressedInteraction.value?.let { oldValue ->
+                val interaction = PressInteraction.Cancel(oldValue)
+                interactionSource.tryEmit(interaction)
+                pressedInteraction.value = null
+            }
+        }
+    }
+}
+
+internal suspend fun PressGestureScope.handlePressInteraction(
+    pressPoint: Offset,
+    interactionSource: MutableInteractionSource,
+    pressedInteraction: MutableState<PressInteraction.Press?>
+) {
+    val pressInteraction = PressInteraction.Press(pressPoint)
+    interactionSource.emit(pressInteraction)
+    pressedInteraction.value = pressInteraction
+    val success = tryAwaitRelease()
+    val endInteraction =
+        if (success) {
+            PressInteraction.Release(pressInteraction)
+        } else {
+            PressInteraction.Cancel(pressInteraction)
+        }
+    interactionSource.emit(endInteraction)
+    pressedInteraction.value = null
+}
+
+@Composable
 @Suppress("ComposableModifierFactory")
 internal fun Modifier.genericClickableWithoutGesture(
     gestureModifiers: Modifier,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index f12d7de..91c9ce7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -23,8 +23,10 @@
 import androidx.compose.ui.input.pointer.PointerId
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.changedToUp
 import androidx.compose.ui.input.pointer.positionChangeConsumed
 import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.input.pointer.consumeDownChange
 import androidx.compose.ui.input.pointer.isOutOfBounds
 import androidx.compose.ui.input.pointer.positionChange
 import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
@@ -33,6 +35,7 @@
 import androidx.compose.ui.util.fastAll
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastForEach
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.withTimeout
@@ -247,10 +250,16 @@
                 onDragStart.invoke(drag.position)
 
                 awaitPointerEventScope {
-                    if (!drag(drag.id) { onDrag(it, it.positionChange()) }) {
-                        onDragCancel()
-                    } else {
+                    if (drag(drag.id) { onDrag(it, it.positionChange()) }) {
+                        // consume up if we quit drag gracefully with the up
+                        currentEvent.changes.fastForEach {
+                            if (it.changedToUp()) {
+                                it.consumeDownChange()
+                            }
+                        }
                         onDragEnd()
+                    } else {
+                        onDragCancel()
                     }
                 }
             }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
index d05b451c..c2e33b6d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
@@ -19,15 +19,14 @@
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.util.fastAny
-import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.NonCancellable.isActive
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.isActive
 import kotlin.coroutines.cancellation.CancellationException
 
 /**
  * A gesture was canceled and cannot continue, likely because another gesture has taken
  * over the pointer input stream.
  */
-@OptIn(ExperimentalStdlibApi::class)
 class GestureCancellationException(message: String? = null) : CancellationException(message)
 
 /**
@@ -35,9 +34,9 @@
  * it will wait until all pointers are raised before another gesture is detected, or it
  * exits if [isActive] is `false`.
  */
-@OptIn(InternalCoroutinesApi::class, ExperimentalStdlibApi::class)
 suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit) {
-    while (isActive) {
+    val currentContext = currentCoroutineContext()
+    while (currentContext.isActive) {
         try {
             block()
 
@@ -45,7 +44,7 @@
             awaitAllPointersUp()
         } catch (e: CancellationException) {
             // The gesture was canceled. Wait for all fingers to be "up" before looping again.
-            if (isActive) {
+            if (currentContext.isActive) {
                 awaitAllPointersUp()
                 throw e
             }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index 6235252..7739b9b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -16,26 +16,38 @@
 
 package androidx.compose.foundation.gestures
 
+import androidx.compose.foundation.gestures.TapGestureEvent.AllUp
+import androidx.compose.foundation.gestures.TapGestureEvent.Cancel
+import androidx.compose.foundation.gestures.TapGestureEvent.Down
+import androidx.compose.foundation.gestures.TapGestureEvent.Up
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.positionChangeConsumed
 import androidx.compose.ui.input.pointer.changedToDown
 import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
 import androidx.compose.ui.input.pointer.changedToUp
 import androidx.compose.ui.input.pointer.consumeAllChanges
 import androidx.compose.ui.input.pointer.consumeDownChange
 import androidx.compose.ui.input.pointer.isOutOfBounds
+import androidx.compose.ui.input.pointer.positionChangeConsumed
 import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastAll
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastForEach
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.channels.SendChannel
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.withTimeout
@@ -87,59 +99,57 @@
     onLongPress: ((Offset) -> Unit)? = null,
     onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
     onTap: ((Offset) -> Unit)? = null
-) {
-    val pressScope = PressGestureScopeImpl(this)
-    forEachGesture {
-        coroutineScope {
+) = coroutineScope {
+    // special signal to indicate to the sending side that it needs to consume(!) all the events
+    // until all the pointers will be up
+    val consumeAllUntilUpSignal = mutableStateOf(false)
+    // special signal to indicate to the sending side that it shouldn't intercept and consume
+    // cancel/up events as we're only require down events
+    val consumeOnlyDownsSignal = mutableStateOf(false)
+    val channel = Channel<TapGestureEvent>(capacity = Channel.UNLIMITED)
+    val pressScope = PressGestureScopeImpl(this@detectTapGestures)
+
+    launch {
+        while (isActive) {
             pressScope.reset()
-            val down = awaitPointerEventScope {
-                awaitFirstDown().also {
-                    it.consumeDownChange()
-                }
-            }
-            if (onPress !== NoPressGesture) {
-                launch { pressScope.onPress(down.position) }
-            }
-
-            val longPressTimeout =
-                if (onLongPress == null) {
-                    Int.MAX_VALUE.toLong()
-                } else {
-                    viewConfiguration.longPressTimeoutMillis
-                }
-
-            var up: PointerInputChange? = null
+            consumeAllUntilUpSignal.value = false
+            val down = awaitChannelDown(consumeOnlyDownsSignal, channel)
+            if (onPress !== NoPressGesture) launch { pressScope.onPress(down.position) }
+            val longPressTimeout = onLongPress?.let { viewConfiguration.longPressTimeoutMillis }
+            var upOrCancel: TapGestureEvent? = null
             try {
                 // wait for first tap up or long press
-                up = withTimeout(longPressTimeout) {
-                    awaitPointerEventScope {
-                        waitForUpOrCancellation()?.also { it.consumeDownChange() }
-                    }
+                upOrCancel = withNullableTimeout(this, longPressTimeout) {
+                    awaitChannelUpOrCancel(channel)
                 }
-                if (up == null) {
+                if (upOrCancel is Cancel) {
                     pressScope.cancel() // tap-up was canceled
                 } else {
                     pressScope.release()
                 }
             } catch (_: TimeoutCancellationException) {
                 onLongPress?.invoke(down.position)
-                consumeAllEventsUntilUp()
+                awaitChannelAllUp(consumeAllUntilUpSignal, channel)
                 pressScope.release()
             }
 
-            if (up != null) {
+            if (upOrCancel != null && upOrCancel is Up) {
                 // tap was successful.
                 if (onDoubleTap == null) {
-                    onTap?.invoke(up.position) // no need to check for double-tap.
+                    onTap?.invoke(upOrCancel.position) // no need to check for double-tap.
                 } else {
                     // check for second tap
-                    val secondDown = detectSecondTapDown(up.uptimeMillis)
+                    val secondDown = awaitChannelSecondDown(
+                        channel,
+                        consumeOnlyDownsSignal,
+                        viewConfiguration,
+                        upOrCancel
+                    )
 
                     if (secondDown == null) {
-                        onTap?.invoke(up.position) // no valid second tap started
+                        onTap?.invoke(upOrCancel.position) // no valid second tap started
                     } else {
                         // Second tap down detected
-                        secondDown.consumeDownChange()
                         pressScope.reset()
                         if (onPress !== NoPressGesture) {
                             launch { pressScope.onPress(secondDown.position) }
@@ -147,27 +157,24 @@
 
                         try {
                             // Might have a long second press as the second tap
-                            withTimeout(longPressTimeout) {
-                                awaitPointerEventScope {
-                                    val secondUp = waitForUpOrCancellation()
-                                    if (secondUp == null) {
-                                        pressScope.cancel()
-                                        onTap?.invoke(up.position)
-                                    } else {
-                                        secondUp.consumeDownChange()
-                                        pressScope.release()
-                                        onDoubleTap(secondUp.position)
-                                    }
+                            withNullableTimeout(this, longPressTimeout) {
+                                val secondUp = awaitChannelUpOrCancel(channel)
+                                if (secondUp is Up) {
+                                    pressScope.release()
+                                    onDoubleTap(secondUp.position)
+                                } else {
+                                    pressScope.cancel()
+                                    onTap?.invoke(upOrCancel.position)
                                 }
                             }
                         } catch (e: TimeoutCancellationException) {
                             // The first tap was valid, but the second tap is a long press.
                             // notify for the first tap
-                            onTap?.invoke(up.position)
+                            onTap?.invoke(upOrCancel.position)
 
                             // notify for the long press
                             onLongPress?.invoke(secondDown.position)
-                            consumeAllEventsUntilUp()
+                            awaitChannelAllUp(consumeAllUntilUpSignal, channel)
                             pressScope.release()
                         }
                     }
@@ -175,6 +182,202 @@
             }
         }
     }
+    forEachGesture {
+        awaitPointerEventScope {
+            translatePointerEventsToChannel(
+                this@coroutineScope,
+                channel,
+                consumeOnlyDownsSignal,
+                consumeAllUntilUpSignal
+            )
+        }
+    }
+}
+
+private suspend fun <T> withNullableTimeout(
+    scope: CoroutineScope,
+    timeout: Long?,
+    block: suspend CoroutineScope.() -> T
+): T {
+    return if (timeout != null) {
+        withTimeout(timeout, block)
+    } else {
+        with(scope) {
+            block()
+        }
+    }
+}
+
+/**
+ * Await down from the channel and return it when it happens
+ */
+private suspend fun awaitChannelDown(
+    onlyDownsSignal: MutableState<Boolean>,
+    channel: ReceiveChannel<TapGestureEvent>
+): Down {
+    onlyDownsSignal.value = true
+    var event = channel.receive()
+    while (event !is Down) {
+        event = channel.receive()
+    }
+    onlyDownsSignal.value = false
+    return event
+}
+
+/**
+ * Reads input for second tap down event from the [channel]. If the second tap is within
+ * [ViewConfiguration.doubleTapMinTimeMillis] of [firstUp] uptime, the event is discarded. If the
+ * second down is not detected within [ViewConfiguration.doubleTapTimeoutMillis] of [firstUp],
+ * `null` is returned. Otherwise, the down event is returned.
+ */
+private suspend fun awaitChannelSecondDown(
+    channel: ReceiveChannel<TapGestureEvent>,
+    onlyDownsSignal: MutableState<Boolean>,
+    viewConfiguration: ViewConfiguration,
+    firstUp: Up
+): Down? {
+    return withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {
+        val minUptime = firstUp.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis
+        var change: Down
+        // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap
+        do {
+            change = awaitChannelDown(onlyDownsSignal, channel)
+        } while (change.uptimeMillis < minUptime)
+        change
+    }
+}
+
+/**
+ * Special case to wait for all ups after long press has been fired. This sets a state value to
+ * true, indicating to the channel producer to consume all events until it will send an [AllUp]
+ * event. When all up happens and producer itself flips the value back to false, this method
+ * returns.
+ */
+private suspend fun awaitChannelAllUp(
+    consumeAllSignal: MutableState<Boolean>,
+    channel: ReceiveChannel<TapGestureEvent>
+) {
+    consumeAllSignal.value = true
+    var event = channel.receive()
+    while (event != AllUp) {
+        event = channel.receive()
+    }
+}
+
+/**
+ * Await up or cancel event from the channel and return either [Up] or [Cancel]
+ */
+private suspend fun awaitChannelUpOrCancel(
+    channel: ReceiveChannel<TapGestureEvent>
+): TapGestureEvent {
+    var event = channel.receive()
+    while (event !is Up && event !is Cancel) {
+        event = channel.receive()
+    }
+    return event
+}
+
+private sealed class TapGestureEvent {
+    class Down(val position: Offset, val uptimeMillis: Long) : TapGestureEvent()
+    class Up(val position: Offset, val uptimeMillis: Long) : TapGestureEvent()
+
+    // special case, the notification sent when we were consuming all previous events before all
+    // the pointers are up. AllUp means that we can restart the cycle after long press fired
+    object AllUp : TapGestureEvent()
+    object Cancel : TapGestureEvent()
+}
+
+/**
+ * Method to await domain specific [TapGestureEvent] from the [AwaitPointerEventScope] and send
+ * them to the specified [channel].
+ *
+ * Note: [consumeAllUntilUp] is a switch for a special case which happens when the long press has
+ * been fired, after which we want to block all the events until all fingers are up. This methods
+ * stars to consume all the events when [consumeAllUntilUp] is `true` and when all pointers are
+ * up it flips the [consumeAllUntilUp] itself, so it can suspend on the [AwaitPointerEventScope
+ * .awaitPointerEvent] again.
+ */
+private suspend fun AwaitPointerEventScope.translatePointerEventsToChannel(
+    scope: CoroutineScope,
+    channel: SendChannel<TapGestureEvent>,
+    detectDownsOnly: State<Boolean>,
+    consumeAllUntilUp: MutableState<Boolean>
+) {
+    while (scope.isActive) {
+        // operate normally, scan all downs / ups / cancels and push them to the channel
+        val event = awaitPointerEvent()
+        if (consumeAllUntilUp.value) {
+            event.changes.fastForEach { it.consumeAllChanges() }
+            // check the signal if we just need to consume everything on the initial pass for
+            // cases when the long press has fired and we block everything before all pointers
+            // are up
+            if (!allPointersUp()) {
+                do {
+                    val initialEvent = awaitPointerEvent(PointerEventPass.Initial)
+                    initialEvent.changes.fastForEach { it.consumeAllChanges() }
+                } while (initialEvent.changes.fastAny { it.pressed })
+                // wait for the main pass of the initial event we already have eaten above
+                awaitPointerEvent()
+            }
+            channel.offer(AllUp)
+            consumeAllUntilUp.value = false
+        } else if (event.changes.fastAll { it.changedToDown() }) {
+            val change = event.changes[0]
+            change.consumeDownChange()
+            channel.offer(Down(change.position, change.uptimeMillis))
+        } else if (!detectDownsOnly.value) {
+            if (event.changes.fastAll { it.changedToUp() }) {
+                // All pointers are up
+                val change = event.changes[0]
+                change.consumeDownChange()
+                channel.offer(Up(change.position, change.uptimeMillis))
+            } else if (
+                event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }
+            ) {
+                channel.offer(Cancel)
+            } else {
+                // Check for cancel by position consumption. We can look on the Final pass of the
+                // existing pointer event because it comes after the Main pass we checked above.
+                val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
+                if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) {
+                    channel.offer(Cancel)
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Shortcut for cases when we only need to get press/click logic, as for cases without long press
+ * and double click we don't require channelling or any other complications.
+ */
+internal suspend fun PointerInputScope.detectTapAndPress(
+    onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
+    onTap: ((Offset) -> Unit)? = null
+) {
+    val pressScope = PressGestureScopeImpl(this)
+    forEachGesture {
+        coroutineScope {
+            pressScope.reset()
+            awaitPointerEventScope {
+
+                val down = awaitFirstDown().also { it.consumeDownChange() }
+
+                if (onPress !== NoPressGesture) {
+                    launch { pressScope.onPress(down.position) }
+                }
+
+                val up = waitForUpOrCancellation()
+                if (up == null) {
+                    pressScope.cancel() // tap-up was canceled
+                } else {
+                    up.consumeDownChange()
+                    pressScope.release()
+                    onTap?.invoke(up.position)
+                }
+            }
+        }
+    }
 }
 
 /**
@@ -224,42 +427,6 @@
 }
 
 /**
- * Consumes all event changes in the [PointerEventPass.Initial] until all pointers are up.
- */
-private suspend fun PointerInputScope.consumeAllEventsUntilUp() {
-    awaitPointerEventScope {
-        if (!allPointersUp()) {
-            do {
-                val event = awaitPointerEvent(PointerEventPass.Initial)
-                event.changes.fastForEach { it.consumeAllChanges() }
-            } while (event.changes.fastAny { it.pressed })
-        }
-    }
-}
-
-/**
- * Reads input for second tap down event. If the second tap is within
- * [ViewConfiguration.doubleTapMinTimeMillis] of [upTime], the event is discarded. If the second
- * down is not detected within [ViewConfiguration.doubleTapTimeoutMillis] of [upTime], `null` is
- * returned. Otherwise, the down event is returned.
- */
-private suspend fun PointerInputScope.detectSecondTapDown(
-    upTime: Long
-): PointerInputChange? {
-    return withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {
-        awaitPointerEventScope {
-            val minUptime = upTime + viewConfiguration.doubleTapMinTimeMillis
-            var change: PointerInputChange
-            // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap
-            do {
-                change = awaitFirstDown()
-            } while (change.uptimeMillis < minUptime)
-            change
-        }
-    }
-}
-
-/**
  * [detectTapGestures]'s implementation of [PressGestureScope].
  */
 private class PressGestureScopeImpl(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/DragGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/DragGestureFilter.kt
deleted file mode 100644
index b9e4ec3..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/DragGestureFilter.kt
+++ /dev/null
@@ -1,1047 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file: Suppress("DEPRECATION")
-
-package androidx.compose.foundation.legacygestures
-
-import androidx.compose.foundation.fastFilter
-import androidx.compose.foundation.fastFold
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputFilter
-import androidx.compose.ui.input.pointer.PointerInputModifier
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
-import androidx.compose.ui.input.pointer.changedToUp
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.consumeDownChange
-import androidx.compose.ui.input.pointer.consumePositionChange
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeConsumed
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlin.math.abs
-
-internal enum class Direction {
-    LEFT, UP, RIGHT, DOWN
-}
-
-/**
- * Defines the callbacks associated with dragging.
- */
-@Deprecated("Use Modifier.pointerInput { detectDragGestures(...) }")
-internal interface DragObserver {
-
-    /**
-     * Override to be notified when a drag has started.
-     *
-     * This will be called as soon as the DragGestureDetector is allowed to start (canStartDragging
-     * is null or returns true) and the average distance the pointers have moved are not 0 on
-     * both the x and y axes.
-     *
-     * Only called if the last called if the most recent call among [onStart], [onStop], and
-     * [onCancel] was [onStop] or [onCancel].
-     *
-     * @param downPosition The average position of all pointer positions when they first touched
-     * down.
-     */
-    fun onStart(downPosition: Offset) {}
-
-    /**
-     * Override to be notified when a distance has been dragged.
-     *
-     * When overridden, return the amount of the [dragDistance] that has been consumed.
-     *
-     * Called immediately after [onStart] and for every subsequent pointer movement, as long as the
-     * movement was enough to constitute a drag (the average movement on the x or y axis is not
-     * equal to 0).
-     *
-     * Note: This may be called multiple times in a single pass and the values should be accumulated
-     * for each call.
-     *
-     * @param dragDistance The distance that has been dragged.  Reflects the average drag distance
-     * of all pointers.
-     */
-    fun onDrag(dragDistance: Offset) = Offset.Zero
-
-    /**
-     * Override to be notified when a drag has stopped.
-     *
-     * This is called once all pointers have stopped interacting with this DragGestureDetector.
-     *
-     * Only called if the last called if the most recent call among [onStart], [onStop], and
-     * [onCancel] was [onStart].
-     *
-     * @param velocity The velocity of the drag in both orientations at the point in time when all
-     * pointers have released the relevant PointerInputFilter. In pixels per second.
-     */
-    fun onStop(velocity: Offset) {}
-
-    /**
-     * Override to be notified when the drag has been cancelled.
-     *
-     * This is called in response to a cancellation event such as the associated
-     * PointerInputFilter having been removed from the hierarchy.
-     *
-     * Only called if the last called if the most recent call among [onStart], [onStop], and
-     * [onCancel] was [onStart].
-     */
-    fun onCancel() {}
-}
-
-/**
- * Modeled after Android's ViewConfiguration:
- * https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/ViewConfiguration.java
- */
-
-/** The time before a long press gesture attempts to win. */
-internal const val LongPressTimeoutMillis: Long = 500L
-
-/**
- * The maximum time from the start of the first tap to the start of the second
- * tap in a double-tap gesture.
- */
-// TODO(shepshapard): In Android, this is actually the time from the first's up event
-// to the second's down event, according to the ViewConfiguration docs.
-internal const val DoubleTapTimeoutMillis: Long = 300L
-
-/**
- * The distance a touch has to travel for the framework to be confident that
- * the gesture is a scroll gesture, or, inversely, the maximum distance that a
- * touch can travel before the framework becomes confident that it is not a
- * tap.
- */
-// This value was empirically derived. We started at 8.0 and increased it to
-// 18.0 after getting complaints that it was too difficult to hit targets.
-internal val TouchSlop = 18.dp
-
-// TODO(b/146133703): Likely rename to PanGestureDetector as per b/146133703
-/**
- * This gesture detector detects dragging in any direction.
- *
- * Dragging normally begins when the touch slop distance (currently defined by [TouchSlop]) is
- * surpassed in a supported direction (see [DragObserver.onDrag]).  When dragging begins in this
- * manner, [DragObserver.onStart] is called, followed immediately by a call to
- * [DragObserver.onDrag]. [DragObserver.onDrag] is then continuously called whenever pointers
- * have moved. The gesture ends with either a call to [DragObserver.onStop] or
- * [DragObserver.onCancel], only after [DragObserver.onStart] is called. [DragObserver.onStop] is
- * called when the dragging ends due to all of the pointers no longer interacting with the
- * DragGestureDetector (for example, the last pointer has been lifted off of the
- * DragGestureDetector). [DragObserver.onCancel] is called when the dragging ends due to a system
- * cancellation event.
- *
- * If [startDragImmediately] is set to true, dragging will begin as soon as soon as a pointer comes
- * in contact with it, effectively ignoring touch slop and blocking any descendants from reacting
- * the "down" change.  When dragging begins in this manner, [DragObserver.onStart] is called
- * immediately and is followed by [DragObserver.onDrag] when some drag distance has occurred.
- *
- * When multiple pointers are touching the detector, the drag distance is taken as the average of
- * all of the pointers.
- *
- * @param dragObserver The callback interface to report all events related to dragging.
- * @param startDragImmediately Set to true to have dragging begin immediately when a pointer is
- * "down", preventing children from responding to the "down" change.  Generally, this parameter
- * should be set to true when the child of the GestureDetector is animating, such that when a finger
- * touches it, dragging is immediately started so the animation stops and dragging can occur.
- */
-@Deprecated("Use Modifier.pointerInput{ detectDragGestures(... )} instead")
-internal fun Modifier.dragGestureFilter(
-    dragObserver: DragObserver,
-    startDragImmediately: Boolean = false
-): Modifier = composed(
-    inspectorInfo = debugInspectorInfo {
-        name = "dragGestureFilter"
-        properties["dragObserver"] = dragObserver
-        properties["startDragImmediately"] = startDragImmediately
-    }
-) {
-    val glue = remember { TouchSlopDragGestureDetectorGlue() }
-    glue.touchSlopDragObserver = dragObserver
-
-    // TODO(b/146427920): There is a gap here where RawPressStartGestureDetector can cause a call to
-    //  DragObserver.onStart but if the pointer doesn't move and releases, (or if cancel is called)
-    //  The appropriate callbacks to DragObserver will not be called.
-    rawDragGestureFilter(glue.rawDragObserver, glue::enabledOrStarted)
-        .dragSlopExceededGestureFilter(glue::enableDrag)
-        .rawPressStartGestureFilter(
-            glue::startDrag,
-            startDragImmediately,
-            PointerEventPass.Initial
-        )
-}
-
-// TODO(shepshapard): Convert to functional component with effects once effects are ready.
-// TODO(shepshapard): Should this calculate the drag distance as the average of all fingers
-//  (Shep thinks this is better), or should it only track the most recent finger to have
-//  touched the screen over the detector (this is how Android currently does it)?
-// TODO(b/139020678): Probably has shared functionality with other movement based detectors.
-/**
- * This gesture detector detects dragging in any direction.
- *
- * Note: By default, this gesture detector only waits for a single pointer to have moved to start
- * dragging.  It is extremely likely that you don't want to use this gesture detector directly, but
- * instead use a drag gesture detector that does wait for some other condition to have occurred
- * (such as [dragGestureFilter] which waits for a single pointer to have passed touch
- * slop before dragging starts).
- *
- * Dragging begins when the a single pointer has moved and either [canStartDragging] is null or
- * returns true.  When dragging begins, [DragObserver.onStart] is called.  [DragObserver.onDrag] is
- * then continuously called whenever the average movement of all pointers has movement along the x
- * or y axis.  The gesture ends with either a call to [DragObserver.onStop] or
- * [DragObserver.onCancel], only after [DragObserver.onStart] is called. [DragObserver.onStop] is
- * called when the dragging ends due to all of the pointers no longer interacting with the
- * DragGestureDetector (for example, the last pointer has been lifted off of the
- * DragGestureDetector). [DragObserver.onCancel] is called when the dragging ends due to a system
- * cancellation event.
- *
- * When multiple pointers are touching the detector, the drag distance is taken as the average of
- * all of the pointers.
- *
- * @param dragObserver The callback interface to report all events related to dragging.
- * @param canStartDragging If set, Before dragging is started ([DragObserver.onStart] is called),
- *                         canStartDragging is called to check to see if it is allowed to start.
- */
-
-// TODO(b/129784010): Consider also allowing onStart, onDrag, and onStop to be set individually
-//  (instead of all being set via DragObserver).
-@Deprecated("use Modifier.pointerInput { } with awaitFirstDown() and drag() functions")
-internal fun Modifier.rawDragGestureFilter(
-    dragObserver: DragObserver,
-    canStartDragging: (() -> Boolean)? = null,
-): Modifier = composed(
-    inspectorInfo = debugInspectorInfo {
-        name = "rawDragGestureFilter"
-        properties["dragObserver"] = dragObserver
-        properties["canStartDragging"] = canStartDragging
-    }
-) {
-    val filter = remember { RawDragGestureFilter() }
-    filter.dragObserver = dragObserver
-    filter.canStartDragging = canStartDragging
-    DragPointerInputModifierImpl(filter)
-}
-
-internal class RawDragGestureFilter : PointerInputFilter() {
-    private val velocityTrackers: MutableMap<PointerId, VelocityTracker> = mutableMapOf()
-    private val downPositions: MutableMap<PointerId, Offset> = mutableMapOf()
-
-    internal lateinit var dragObserver: DragObserver
-    internal var canStartDragging: (() -> Boolean)? = null
-
-    private var started = false
-
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize
-    ) {
-        val changes = pointerEvent.changes
-
-        if (pass == PointerEventPass.Initial) {
-            if (started) {
-                // If we are have started we want to prevent any descendants from reacting to
-                // any down change.
-                changes.fastForEach {
-                    if (it.changedToDown()) {
-                        it.consumeDownChange()
-                    }
-                }
-            }
-        }
-
-        if (pass == PointerEventPass.Main) {
-
-            // Get the changes for pointers that are relevant to us due to orientation locking.
-
-            // Handle up changes, which includes removing individual pointer VelocityTrackers
-            // and potentially calling onStop().
-            if (changes.fastAny { it.changedToUpIgnoreConsumed() }) {
-
-                // TODO(b/162269614): Should be update to only have one velocity tracker that
-                //  tracks the average change overtime, instead of one for each finger.
-
-                var velocityTracker: VelocityTracker? = null
-
-                changes.fastForEach {
-                    // This pointer is up (consumed or not), so we should stop tracking
-                    // information about it.  If the pointer is not locked out of our
-                    // orientation, get the velocity tracker because this might be a fling.
-                    if (it.changedToUp() && changes.contains(it)) {
-                        velocityTracker = velocityTrackers.remove(it.id)
-                    } else if (it.changedToUpIgnoreConsumed()) {
-                        velocityTrackers.remove(it.id)
-                    }
-                    // removing stored down position for the pointer.
-                    if (it.changedToUp()) {
-                        downPositions.remove(it.id)
-                    }
-                }
-
-                if (changes.fastAll { it.changedToUpIgnoreConsumed() }) {
-                    // All of the pointers are up, so reset and call onStop.  If we have a
-                    // velocityTracker at this point, that means at least one of the up events
-                    // was not consumed so we should send velocity for flinging.
-                    if (started) {
-                        val velocity: Offset? =
-                            if (velocityTracker != null) {
-                                changes.fastForEach {
-                                    it.consumeDownChange()
-                                }
-                                val velocity = velocityTracker!!.calculateVelocity()
-                                Offset(velocity.x, velocity.y)
-                            } else {
-                                null
-                            }
-                        started = false
-                        dragObserver.onStop(velocity ?: Offset.Zero)
-                        reset()
-                    }
-                }
-            }
-
-            // Handle down changes: for each new pointer that has been added, start tracking
-            // information about it.
-            if (changes.fastAny { it.changedToDownIgnoreConsumed() }) {
-                changes.fastForEach {
-                    // If a pointer has changed to down, we should start tracking information
-                    // about it.
-                    if (it.changedToDownIgnoreConsumed()) {
-                        velocityTrackers[it.id] = VelocityTracker()
-                            .apply {
-                                addPosition(
-                                    it.uptimeMillis,
-                                    it.position
-                                )
-                            }
-                        downPositions[it.id] = it.position
-                    }
-                }
-            }
-
-            // Handle moved changes.
-
-            val movedChanges = changes.fastFilter {
-                it.pressed && !it.changedToDownIgnoreConsumed()
-            }
-
-            movedChanges.fastForEach {
-                // TODO(shepshapard): handle the case that the pointerTrackingData is null,
-                //  either with an exception or a logged error, or something else.
-                // TODO(shepshapard): VelocityTracker needs to be updated to not accept
-                //  position information, but rather vector information about movement.
-                // TODO(b/162269614): Should be update to only have one velocity tracker that
-                //  tracks the average change overtime, instead of one for each finger.
-                velocityTrackers[it.id]?.addPosition(
-                    it.uptimeMillis,
-                    it.position
-                )
-            }
-
-            // Check to see if we are already started so we don't have to call canStartDragging
-            // again.
-            val canStart = !started && canStartDragging?.invoke() ?: true
-
-            // At this point, check to see if we have started, and if we have, we may
-            // be calling onDrag and updating change information on the PointerInputChanges.
-            if (started || canStart) {
-
-                var totalDx = 0f
-                var totalDy = 0f
-
-                movedChanges.fastForEach {
-                    if (movedChanges.contains(it)) {
-                        totalDx += it.positionChange().x
-                    }
-                    if (movedChanges.contains(it)) {
-                        totalDy += it.positionChange().y
-                    }
-                }
-
-                if (totalDx != 0f || totalDy != 0f) {
-
-                    // At this point, if we have not started, check to see if we should start
-                    // and if we should, update our state and call onStart().
-                    if (!started) {
-                        started = true
-                        dragObserver.onStart(downPositions.values.averagePosition())
-                        downPositions.clear()
-                    }
-
-                    val consumed = dragObserver.onDrag(
-                        Offset(
-                            totalDx / changes.size,
-                            totalDy / changes.size
-                        )
-                    )
-
-                    if (consumed.x != 0f || consumed.y != 0f) {
-                        movedChanges.fastForEach { it.consumePositionChange() }
-                    }
-                }
-            }
-        }
-    }
-
-    override fun onCancel() {
-        downPositions.clear()
-        velocityTrackers.clear()
-        if (started) {
-            started = false
-            dragObserver.onCancel()
-        }
-        reset()
-    }
-
-    private fun reset() {
-        downPositions.clear()
-        velocityTrackers.clear()
-    }
-}
-
-private fun Iterable<Offset>.averagePosition(): Offset {
-    var x = 0f
-    var y = 0f
-    this.forEach {
-        x += it.x
-        y += it.y
-    }
-    return Offset(x / count(), y / count())
-}
-
-/**
- * Glues together the logic of RawDragGestureDetector, TouchSlopExceededGestureDetector, and
- * InterruptFlingGestureDetector.
- */
-private class TouchSlopDragGestureDetectorGlue {
-
-    lateinit var touchSlopDragObserver: DragObserver
-    var started = false
-    var enabled = false
-    val enabledOrStarted
-        get() = started || enabled
-
-    fun enableDrag() {
-        enabled = true
-    }
-
-    fun startDrag(downPosition: Offset) {
-        started = true
-        touchSlopDragObserver.onStart(downPosition)
-    }
-
-    val rawDragObserver: DragObserver =
-        object : DragObserver {
-            override fun onStart(downPosition: Offset) {
-                if (!started) {
-                    touchSlopDragObserver.onStart(downPosition)
-                }
-            }
-
-            override fun onDrag(dragDistance: Offset): Offset {
-                return touchSlopDragObserver.onDrag(dragDistance)
-            }
-
-            override fun onStop(velocity: Offset) {
-                started = false
-                enabled = false
-                touchSlopDragObserver.onStop(velocity)
-            }
-
-            override fun onCancel() {
-                started = false
-                enabled = false
-                touchSlopDragObserver.onCancel()
-            }
-        }
-}
-
-/**
- * Reacts if the first pointer input change it sees is an unconsumed down change, and if it reacts,
- * consumes all further down changes.
- *
- * This GestureDetector is not generally intended to be used directly, but is instead intended to be
- * used as a building block to create more complex GestureDetectors.
- *
- * This GestureDetector is a bit more experimental then the other GestureDetectors (the number and
- * types of GestureDetectors is still very much a work in progress) and is intended to be a
- * generically useful building block for more complicated GestureDetectors.
- *
- * The theory is that this GestureDetector can be reused in PressIndicatorGestureDetector, and there
- * could be a corresponding RawPressReleasedGestureDetector.
- *
- * @param onPressStart Called when the first pointer "presses" on the GestureDetector.  [Offset]
- * is the position of that first pointer on press.
- * @param enabled If false, this GestureDetector will effectively act as if it is not in the
- * hierarchy.
- * @param executionPass The [PointerEventPass] during which this GestureDetector will attempt to
- * react to and consume down changes.  Defaults to [PointerEventPass.Main].
- */
-@Deprecated("Use Modifier.pointerInput{} with custom gesture detection code")
-internal fun Modifier.rawPressStartGestureFilter(
-    onPressStart: (Offset) -> Unit,
-    enabled: Boolean = false,
-    executionPass: PointerEventPass = PointerEventPass.Main
-): Modifier = composed(
-    inspectorInfo = debugInspectorInfo {
-        name = "rawPressStartGestureFilter"
-        properties["onPressStart"] = onPressStart
-        properties["enabled"] = enabled
-        properties["executionPass"] = executionPass
-    }
-) {
-    val filter = remember { RawPressStartGestureFilter() }
-    filter.onPressStart = onPressStart
-    filter.setEnabled(enabled = enabled)
-    filter.setExecutionPass(executionPass)
-    DragPointerInputModifierImpl(filter)
-}
-
-internal class RawPressStartGestureFilter : PointerInputFilter() {
-
-    lateinit var onPressStart: (Offset) -> Unit
-    private var enabled: Boolean = true
-    private var executionPass = PointerEventPass.Initial
-
-    private var active = false
-
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize
-    ) {
-        val changes = pointerEvent.changes
-
-        if (pass == executionPass) {
-            if (enabled && changes.fastAll { it.changedToDown() }) {
-                // If we have not yet started and all of the changes changed to down, we are
-                // starting.
-                active = true
-                onPressStart(changes.first().position)
-            } else if (changes.fastAll { it.changedToUp() }) {
-                // If we have started and all of the changes changed to up, we are stopping.
-                active = false
-            }
-
-            if (active) {
-                // If we have started, we should consume the down change on all changes.
-                changes.fastForEach {
-                    it.consumeDownChange()
-                }
-            }
-        }
-    }
-
-    override fun onCancel() {
-        active = false
-    }
-
-    fun setEnabled(enabled: Boolean) {
-        this.enabled = enabled
-        // Whenever we are disabled, we can just go ahead and become inactive (which is the state we
-        // should be in if we are to pretend that we aren't in the hierarchy.
-        if (!enabled) {
-            onCancel()
-        }
-    }
-
-    fun setExecutionPass(executionPass: PointerEventPass) {
-        this.executionPass = executionPass
-    }
-}
-
-internal fun Modifier.dragSlopExceededGestureFilter(
-    onDragSlopExceeded: () -> Unit
-): Modifier = composed(
-    inspectorInfo = debugInspectorInfo {
-        name = "dragSlopExceededGestureFilter"
-        properties["onDragSlopExceeded"] = onDragSlopExceeded
-    }
-) {
-    val touchSlop = with(LocalDensity.current) { TouchSlop.toPx() }
-    val filter = remember {
-        DragSlopExceededGestureFilter(touchSlop)
-    }
-    filter.onDragSlopExceeded = onDragSlopExceeded
-    DragPointerInputModifierImpl(filter)
-}
-
-internal class DragSlopExceededGestureFilter(
-    private val touchSlop: Float
-) : PointerInputFilter() {
-    private var dxForPass = 0f
-    private var dyForPass = 0f
-    private var dxUnderSlop = 0f
-    private var dyUnderSlop = 0f
-    private var passedSlop = false
-
-    private var canDrag: ((Direction) -> Boolean)? = null
-
-    var onDragSlopExceeded: () -> Unit = {}
-
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize
-    ) {
-
-        val changes = pointerEvent.changes
-
-        if (pass == PointerEventPass.Main || pass == PointerEventPass.Final) {
-
-            // Filter changes for those that we can interact with due to our orientation.
-
-            if (!passedSlop) {
-
-                // Get current average change.
-                val averagePositionChange = getAveragePositionChange(changes)
-                val dx = averagePositionChange.x
-                val dy = averagePositionChange.y
-
-                // Track changes during main and during final.  This allows for fancy dragging
-                // due to a parent being dragged and will likely be removed.
-                // TODO(b/157087973): Likely remove this two pass complexity.
-                if (pass == PointerEventPass.Main) {
-                    dxForPass = dx
-                    dyForPass = dy
-                    dxUnderSlop += dx
-                    dyUnderSlop += dy
-                } else {
-                    dxUnderSlop += dx - dxForPass
-                    dyUnderSlop += dy - dyForPass
-                }
-
-                // Map the distance to the direction enum for a call to canDrag.
-                val directionX = averagePositionChange.horizontalDirection()
-                val directionY = averagePositionChange.verticalDirection()
-
-                val canDragX = directionX != null && canDrag?.invoke(directionX) ?: true
-                val canDragY = directionY != null && canDrag?.invoke(directionY) ?: true
-
-                val passedSlopX = canDragX && abs(dxUnderSlop) > touchSlop
-                val passedSlopY = canDragY && abs(dyUnderSlop) > touchSlop
-
-                if (passedSlopX || passedSlopY) {
-                    passedSlop = true
-                    onDragSlopExceeded.invoke()
-                } else {
-                    // If we have passed slop in a direction that we can't drag in, we should reset
-                    // our tracking back to zero so that a user doesn't have to later scroll the slop
-                    // + the extra distance they scrolled in the wrong direction.
-                    if (!canDragX &&
-                        (
-                            (directionX == Direction.LEFT && dxUnderSlop < 0) ||
-                                (directionX == Direction.RIGHT && dxUnderSlop > 0)
-                            )
-                    ) {
-                        dxUnderSlop = 0f
-                    }
-                    if (!canDragY &&
-                        (
-                            (directionY == Direction.UP && dyUnderSlop < 0) ||
-                                (directionY == Direction.DOWN && dyUnderSlop > 0)
-                            )
-                    ) {
-                        dyUnderSlop = 0f
-                    }
-                }
-            }
-
-            if (pass == PointerEventPass.Final &&
-                changes.fastAll { it.changedToUpIgnoreConsumed() }
-            ) {
-                // On the final pass, check to see if all pointers have changed to up, and if they
-                // have, reset.
-                reset()
-            }
-        }
-    }
-
-    override fun onCancel() {
-        reset()
-    }
-
-    private fun reset() {
-        passedSlop = false
-        dxForPass = 0f
-        dyForPass = 0f
-        dxUnderSlop = 0f
-        dyUnderSlop = 0f
-    }
-}
-
-/**
- * Gets the average distance change of all pointers as an Offset.
- */
-private fun getAveragePositionChange(changes: List<PointerInputChange>): Offset {
-    if (changes.isEmpty()) {
-        return Offset.Zero
-    }
-
-    val sum = changes.fastFold(Offset.Zero) { sum, change ->
-        sum + change.positionChange()
-    }
-    val sizeAsFloat = changes.size.toFloat()
-    // TODO(b/148980115): Once PxPosition is removed, sum will be an Offset, and this line can
-    //  just be straight division.
-    return Offset(sum.x / sizeAsFloat, sum.y / sizeAsFloat)
-}
-
-/**
- * Maps an [Offset] value to a horizontal [Direction].
- */
-private fun Offset.horizontalDirection() =
-    when {
-        this.x < 0f -> Direction.LEFT
-        this.x > 0f -> Direction.RIGHT
-        else -> null
-    }
-
-/**
- * Maps a [Offset] value to a vertical [Direction].
- */
-private fun Offset.verticalDirection() =
-    when {
-        this.y < 0f -> Direction.UP
-        this.y > 0f -> Direction.DOWN
-        else -> null
-    }
-
-private data class DragPointerInputModifierImpl(
-    override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
-
-@Deprecated("Use Modifier.pointerInput {detectDragGesturesAfterLongPress(...)} instead")
-internal interface LongPressDragObserver {
-
-    /**
-     * Override to be notified when a long press has occurred and thus dragging can start.
-     *
-     * Note that when this is called, dragging hasn't actually started, but rather, dragging can start.  When dragging
-     * has actually started, [onDragStart] will be called.  It is possible for [onDragStart] to be called immediately
-     * after this synchronously in the same event stream.
-     *
-     * This won't be called again until after [onStop] has been called.
-     *
-     * @see onDragStart
-     * @see onDrag
-     * @see onStop
-     */
-    fun onLongPress(pxPosition: Offset) {}
-
-    /**
-     * Override to be notified when dragging has actually begun.
-     *
-     * Dragging has begun when both [onLongPress] has been called, and the average pointer distance change is not 0.
-     *
-     * This will not be called until after [onLongPress] has been called, and may be called synchronously,
-     * immediately afterward [onLongPress], as a result of the same pointer input event.
-     *
-     * This will not be called again until [onStop] has been called.
-     *
-     * @see onLongPress
-     * @see onDrag
-     * @see onStop
-     */
-    fun onDragStart() {}
-
-    /**
-     * Override to be notified when a distance has been dragged.
-     *
-     * When overridden, return the amount of the [dragDistance] that has been consumed.
-     *
-     * Called after [onDragStart] and for every subsequent pointer movement, as long as the movement
-     * was enough to constitute a drag (the average movement on the x or y axis is not equal to
-     * 0).  This may be called synchronously, immediately afterward [onDragStart], as a result of
-     * the same pointer input event.
-     *
-     * Note: This will be called multiple times for a single pointer input event and the values
-     * provided in each call should be considered unique.
-     *
-     * @param dragDistance The distance that has been dragged.  Reflects the average drag distance
-     * of all pointers.
-     */
-    fun onDrag(dragDistance: Offset) = Offset.Zero
-
-    /**
-     * Override to be notified when a drag has stopped.
-     *
-     * This is called once all pointers have stopped interacting with this DragGestureDetector and
-     * [onLongPress] was previously called.
-     */
-    fun onStop(velocity: Offset) {}
-
-    /**
-     * Override to be notified when the drag has been cancelled.
-     *
-     * This is called if [onLongPress] has ben called and then a cancellation event has occurs
-     * (for example, due to the gesture detector being removed from the tree) before [onStop] is
-     * called.
-     */
-    fun onCancel() {}
-}
-
-/**
- * This gesture detector detects dragging in any direction, but only after a long press has first
- * occurred.
- *
- * Dragging begins once a long press has occurred and then dragging occurs.  When long press occurs,
- * [LongPressDragObserver.onLongPress] is called. Once dragging has occurred,
- * [LongPressDragObserver.onDragStart] will be called. [LongPressDragObserver.onDrag] is then
- * continuously called whenever pointer movement results in a drag. The gesture will end
- * with either a call to [LongPressDragObserver.onStop] or [LongPressDragObserver.onCancel]. Either
- * will be called after [LongPressDragObserver.onLongPress] is called.
- * [LongPressDragObserver.onStop] is called when the gesture ends due to all of the pointers
- * no longer interacting with the LongPressDragGestureDetector (for example, the last finger has
- * been lifted off of the LongPressDragGestureDetector). [LongPressDragObserver.onCancel] is
- * called in response to a system cancellation event.
- *
- * When multiple pointers are touching the detector, the drag distance is taken as the average of
- * all of the pointers.
- *
- * @param longPressDragObserver The callback interface to report all events.
- * @see LongPressDragObserver
- */
-@Deprecated("Use Modifier.pointerInput { detectDragGesturesAfterLongPress(...)} instead.")
-internal fun Modifier.longPressDragGestureFilter(
-    longPressDragObserver: LongPressDragObserver
-): Modifier = composed(
-    inspectorInfo = debugInspectorInfo {
-        name = "longPressDragGestureFilter"
-        properties["longPressDragObserver"] = longPressDragObserver
-    }
-) {
-    val glue = remember { LongPressDragGestureDetectorGlue() }
-    glue.longPressDragObserver = longPressDragObserver
-
-    rawDragGestureFilter(glue.dragObserver, glue::dragEnabled)
-        .then(LongPointerInputModifierImpl(glue))
-        .longPressGestureFilter(glue.onLongPress)
-}
-
-private class LongPressDragGestureDetectorGlue : PointerInputFilter() {
-    lateinit var longPressDragObserver: LongPressDragObserver
-    private var dragStarted: Boolean = false
-    var dragEnabled: Boolean = false
-
-    val dragObserver: DragObserver =
-
-        object : DragObserver {
-
-            override fun onStart(downPosition: Offset) {
-                longPressDragObserver.onDragStart()
-                dragStarted = true
-            }
-
-            override fun onDrag(dragDistance: Offset): Offset {
-                return longPressDragObserver.onDrag(dragDistance)
-            }
-
-            override fun onStop(velocity: Offset) {
-                dragEnabled = false
-                dragStarted = false
-                longPressDragObserver.onStop(velocity)
-            }
-
-            override fun onCancel() {
-                dragEnabled = false
-                dragStarted = false
-                longPressDragObserver.onCancel()
-            }
-        }
-
-    // This handler ensures that onStop will be called after long press happened, but before
-    // dragging actually has begun.
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize
-    ) {
-        if (pass == PointerEventPass.Main &&
-            dragEnabled &&
-            !dragStarted &&
-            pointerEvent.changes.fastAll { it.changedToUpIgnoreConsumed() }
-        ) {
-            dragEnabled = false
-            longPressDragObserver.onStop(Offset.Zero)
-        }
-    }
-
-    // This handler ensures that onCancel is called if onLongPress was previously called but
-    // dragging has not yet started.
-    override fun onCancel() {
-        if (dragEnabled && !dragStarted) {
-            dragEnabled = false
-            longPressDragObserver.onCancel()
-        }
-    }
-
-    val onLongPress = { pxPosition: Offset ->
-        dragEnabled = true
-        longPressDragObserver.onLongPress(pxPosition)
-    }
-}
-
-internal fun Modifier.longPressGestureFilter(
-    onLongPress: (Offset) -> Unit
-): Modifier = composed(
-    inspectorInfo = debugInspectorInfo {
-        name = "longPressGestureFilter"
-        properties["onLongPress"] = onLongPress
-    }
-) {
-    @Suppress("DEPRECATION")
-    val scope = rememberCoroutineScope()
-    val filter = remember { LongPressGestureFilter(scope) }
-    filter.onLongPress = onLongPress
-    LongPointerInputModifierImpl(filter)
-}
-
-internal class LongPressGestureFilter(
-    private val coroutineScope: CoroutineScope
-) : PointerInputFilter() {
-    lateinit var onLongPress: (Offset) -> Unit
-
-    /*@VisibleForTesting*/
-    internal var longPressTimeout = LongPressTimeoutMillis
-
-    private enum class State {
-        Idle, Primed, Fired
-    }
-
-    private var state = State.Idle
-    private val pointerPositions = linkedMapOf<PointerId, Offset>()
-    private var job: Job? = null
-
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize
-    ) {
-        val changes = pointerEvent.changes
-
-        if (pass == PointerEventPass.Initial) {
-            if (state == State.Fired) {
-                // If we fired and have not reset, we should prevent other pointer input nodes from
-                // responding to up, so consume it early on.
-                changes.fastForEach {
-                    if (it.changedToUp()) {
-                        it.consumeDownChange()
-                    }
-                }
-            }
-            return
-        }
-
-        if (pass == PointerEventPass.Main) {
-            if (state == State.Idle && changes.fastAll { it.changedToDown() }) {
-                // If we are idle and all of the changes changed to down, we are prime to fire
-                // the event.
-                primeToFire()
-            } else if (state != State.Idle && changes.fastAll { it.changedToUpIgnoreConsumed() }) {
-                // If we have started and all of the changes changed to up, reset to idle.
-                resetToIdle()
-            } else if (!changes.anyPointersInBounds(bounds)) {
-                // If all pointers have gone out of bounds, reset to idle.
-                resetToIdle()
-            }
-
-            if (state == State.Primed) {
-                // If we are primed, keep track of all down pointer positions so we can pass
-                // pointer position information to the event we will fire.
-                changes.fastForEach {
-                    if (it.pressed) {
-                        pointerPositions[it.id] = it.position
-                    } else {
-                        pointerPositions.remove(it.id)
-                    }
-                }
-            }
-        }
-
-        if (
-            pass == PointerEventPass.Final &&
-            state != State.Idle &&
-            changes.fastAny { it.positionChangeConsumed() }
-        ) {
-            // If we are anything but Idle and something consumed movement, reset.
-            resetToIdle()
-        }
-    }
-
-    // TODO(shepshapard): This continues to be very confusing to use.  Have to come up with a better
-//  way of easily expressing this.
-    /**
-     * Utility method that determines if any pointers are currently in [bounds].
-     *
-     * A pointer is considered in bounds if it is currently down and it's current
-     * position is within the provided [bounds]
-     *
-     * @return True if at least one pointer is in bounds.
-     */
-    private fun List<PointerInputChange>.anyPointersInBounds(bounds: IntSize) =
-        fastAny {
-            it.pressed &&
-                it.position.x >= 0 &&
-                it.position.x < bounds.width &&
-                it.position.y >= 0 &&
-                it.position.y < bounds.height
-        }
-
-    override fun onCancel() {
-        resetToIdle()
-    }
-
-    private fun fireLongPress() {
-        state = State.Fired
-        onLongPress.invoke(pointerPositions.asIterable().first().value)
-    }
-
-    private fun primeToFire() {
-        state = State.Primed
-        job = coroutineScope.launch {
-            delay(longPressTimeout)
-            fireLongPress()
-        }
-    }
-
-    private fun resetToIdle() {
-        state = State.Idle
-        job?.cancel()
-        pointerPositions.clear()
-    }
-}
-
-private data class LongPointerInputModifierImpl(
-    override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/PressIndicatorGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/PressIndicatorGestureFilter.kt
deleted file mode 100644
index ccf2dbf..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/PressIndicatorGestureFilter.kt
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.legacygestures
-
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputFilter
-import androidx.compose.ui.input.pointer.PointerInputModifier
-import androidx.compose.ui.input.pointer.positionChangeConsumed
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.consumeDownChange
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-
-/**
- * This gesture detector has callbacks for when a press gesture starts and ends for the purposes of
- * displaying visual feedback for those two states.
- *
- * More specifically:
- * - It will call [onStart] if the first pointer down it receives during the
- * [PointerEventPass.Main] pass is not consumed.
- * - It will call [onStop] if [onStart] has been called and the last [PointerInputChange] it
- * receives during the [PointerEventPass.Main] pass has an up change, consumed or not, indicating
- * the press gesture indication should end.
- * - It will call [onCancel] if movement has been consumed by the time of the
- * [PointerEventPass.Final] pass, indicating that the press gesture indication should end because
- * something moved.
- *
- * This gesture detector always consumes the down change during the [PointerEventPass.Main] pass.
- */
-// TODO(b/139020678): Probably has shared functionality with other press based detectors.
-@Deprecated(
-    "Gesture filters are deprecated. Use Modifier.clickable or Modifier.pointerInput and " +
-        "detectTapGestures instead",
-    replaceWith = ReplaceWith(
-        """
-            pointerInput {
-                detectTapGestures(onPress = {
-                    onStart?.invoke(it)
-                    val success = tryAwaitRelease()
-                    if (success) {
-                       onStop?.invoke()
-                    } else {
-                       onCancel?.invoke()
-                    }
-                })
-            }""",
-        "androidx.compose.ui.input.pointer.pointerInput",
-        "androidx.compose.foundation.gestures.detectTapGestures"
-    )
-)
-internal fun Modifier.pressIndicatorGestureFilter(
-    onStart: ((Offset) -> Unit)? = null,
-    onStop: (() -> Unit)? = null,
-    onCancel: (() -> Unit)? = null,
-    enabled: Boolean = true
-): Modifier = composed(
-    inspectorInfo = debugInspectorInfo {
-        name = "pressIndicatorGestureFilter"
-        properties["onStart"] = onStart
-        properties["onStop"] = onStop
-        properties["onCancel"] = onCancel
-        properties["enabled"] = enabled
-    }
-) {
-    val filter = remember { PressIndicatorGestureFilter() }
-    filter.onStart = onStart
-    filter.onStop = onStop
-    filter.onCancel = onCancel
-    filter.setEnabled(enabled)
-    PressPointerInputModifierImpl(filter)
-}
-
-internal class PressIndicatorGestureFilter : PointerInputFilter() {
-    /**
-     * Called if the first pointer's down change was not consumed by the time this gesture
-     * filter receives it in the [PointerEventPass.Main] pass.
-     *
-     * This callback should be used to indicate that the press state should be shown.  An [Offset]
-     * is provided to indicate where the first pointer made contact with this gesrure detector.
-     */
-    var onStart: ((Offset) -> Unit)? = null
-
-    /**
-     * Called if onStart was attempted to be called (it may have been null), no pointer movement
-     * was consumed, and the last pointer went up (consumed or not).
-     *
-     * This should be used for removing visual feedback that indicates that the press has ended with
-     * a completed press released gesture.
-     */
-    var onStop: (() -> Unit)? = null
-
-    /**
-     * Called if onStart was attempted to be called (it may have been null), and either:
-     * 1. Pointer movement was consumed by the time [PointerEventPass.Final] reaches this
-     * gesture filter.
-     * 2. [setEnabled] is called with false.
-     * 3. This [PointerInputFilter] is removed from the hierarchy, or it has no descendants
-     * to define it's position or size.
-     * 4. The Compose root is notified that it will no longer receive input, and thus onStop
-     * will never be reached (For example, the Android View that hosts compose receives
-     * MotionEvent.ACTION_CANCEL).
-     *
-     * This should be used for removing visual feedback that indicates that the press gesture was
-     * cancelled.
-     */
-    var onCancel: (() -> Unit)? = null
-
-    private var state = State.Idle
-
-    /**
-     * Sets whether this [PointerInputFilter] is enabled.  True by default.
-     *
-     * When enabled, this [PointerInputFilter] will act normally.
-     *
-     * When disabled, this [PointerInputFilter] will not process any input.  No aspects
-     * of any [PointerInputChange]s will be consumed and no callbacks will be called.
-     *
-     * If the last callback that was attempted to be called was [onStart] ([onStart] may have
-     * been false) and [enabled] is false, [onCancel] will be called.
-     */
-    // TODO(shepshapard): Remove 'setEnabled'.  It serves no purpose anymore.
-    fun setEnabled(enabled: Boolean) {
-        if (state == State.Started) {
-            // If the state is Started and we were passed true, we don't want to change it to
-            // Enabled.
-            // If the state is Started and we were passed false, we can set to Disabled and
-            // call the cancel callback.
-            if (!enabled) {
-                state = State.Disabled
-                onCancel?.invoke()
-            }
-        } else {
-            // If the state is anything but Started, just set the state according to the value
-            // we were passed.
-            state =
-                if (enabled) {
-                    State.Idle
-                } else {
-                    State.Disabled
-                }
-        }
-    }
-
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize
-    ) {
-        val changes = pointerEvent.changes
-
-        if (pass == PointerEventPass.Initial && state == State.Started) {
-            changes.fastForEach {
-                if (it.changedToDown()) {
-                    it.consumeDownChange()
-                }
-            }
-        }
-
-        if (pass == PointerEventPass.Main) {
-
-            if (state == State.Idle && changes.fastAll { it.changedToDown() }) {
-                // If we have not yet started and all of the changes changed to down, we are
-                // starting.
-                state = State.Started
-                onStart?.invoke(changes.first().position)
-            } else if (state == State.Started) {
-                if (changes.fastAll { it.changedToUpIgnoreConsumed() }) {
-                    // If we have started and all of the changes changed to up, we are stopping.
-                    state = State.Idle
-                    onStop?.invoke()
-                } else if (!changes.anyPointersInBounds(bounds)) {
-                    // If all of the down pointers are currently out of bounds, we should cancel
-                    // as this indicates that the user does not which to trigger a press based
-                    // event.
-                    state = State.Idle
-                    onCancel?.invoke()
-                }
-            }
-
-            if (state == State.Started) {
-                changes.fastForEach {
-                    it.consumeDownChange()
-                }
-            }
-        }
-
-        if (
-            pass == PointerEventPass.Final &&
-            state == State.Started &&
-            changes.fastAny { it.positionChangeConsumed() }
-        ) {
-            // On the final pass, if we have started and any of the changes had consumed
-            // position changes, we cancel.
-            state = State.Idle
-            onCancel?.invoke()
-        }
-    }
-
-    // TODO(shepshapard): This continues to be very confusing to use.  Have to come up with a better
-//  way of easily expressing this.
-    /**
-     * Utility method that determines if any pointers are currently in [bounds].
-     *
-     * A pointer is considered in bounds if it is currently down and it's current
-     * position is within the provided [bounds]
-     *
-     * @return True if at least one pointer is in bounds.
-     */
-    private fun List<PointerInputChange>.anyPointersInBounds(bounds: IntSize) =
-        fastAny {
-            it.pressed &&
-                it.position.x >= 0 &&
-                it.position.x < bounds.width &&
-                it.position.y >= 0 &&
-                it.position.y < bounds.height
-        }
-
-    override fun onCancel() {
-        if (state == State.Started) {
-            state = State.Idle
-            onCancel?.invoke()
-        }
-    }
-
-    private enum class State {
-        Disabled, Idle, Started
-    }
-}
-
-private data class PressPointerInputModifierImpl(
-    override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/TapGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/TapGestureFilter.kt
deleted file mode 100644
index 06e73d2..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/TapGestureFilter.kt
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.legacygestures
-
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputFilter
-import androidx.compose.ui.input.pointer.PointerInputModifier
-import androidx.compose.ui.input.pointer.positionChangeConsumed
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.changedToUp
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.consumeDownChange
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-
-/**
- * This gesture detector fires a callback when a traditional press is being released.  This is
- * generally the same thing as "onTap" or "onClick".
- *
- * [onTap] is called with the position of the last pointer to go "up".
- *
- * More specifically, it will call [onTap] if:
- * - All of the first [PointerInputChange]s it receives during the [PointerEventPass.Main] pass
- *   have unconsumed down changes, thus representing new set of pointers, none of which have had
- *   their down events consumed.
- * - The last [PointerInputChange] it receives during the [PointerEventPass.Main] pass has
- *   an unconsumed up change.
- * - While it has at least one pointer touching it, no [PointerInputChange] has had any
- *   movement consumed (as that would indicate that something in the heirarchy moved and this a
- *   press should be cancelled.
- *
- *   @param onTap Called when a tap has occurred.
- */
-// TODO(b/139020678): Probably has shared functionality with other press based detectors.
-@Deprecated(
-    "Gesture filters are deprecated. Use Modifier.clickable or Modifier.pointerInput and " +
-        "detectTapGestures instead",
-    replaceWith = ReplaceWith(
-        """pointerInput { detectTapGestures(onTap = onTap)}""",
-        "androidx.compose.ui.input.pointer.pointerInput",
-        "androidx.compose.foundation.gestures.detectTapGestures"
-    )
-)
-internal fun Modifier.tapGestureFilter(
-    onTap: (Offset) -> Unit
-): Modifier = composed(
-    inspectorInfo = debugInspectorInfo {
-        name = "tapGestureFilter"
-        this.properties["onTap"] = onTap
-    }
-) {
-    val filter = remember { TapGestureFilter() }
-    filter.onTap = onTap
-    TapPointerInputModifierImpl(filter)
-}
-
-internal class TapGestureFilter : PointerInputFilter() {
-    /**
-     * Called to indicate that a press gesture has successfully completed.
-     *
-     * This should be used to fire a state changing event as if a button was pressed.
-     */
-    lateinit var onTap: (Offset) -> Unit
-
-    /**
-     * Whether or not to consume changes.
-     */
-    var consumeChanges: Boolean = true
-
-    /**
-     * True when we are primed to call [onTap] and may be consuming all down changes.
-     */
-    private var primed = false
-
-    private var downPointers: MutableSet<PointerId> = mutableSetOf()
-    private var upBlockedPointers: MutableSet<PointerId> = mutableSetOf()
-    private var lastPxPosition: Offset? = null
-
-    override fun onPointerEvent(
-        pointerEvent: PointerEvent,
-        pass: PointerEventPass,
-        bounds: IntSize
-    ) {
-        val changes = pointerEvent.changes
-
-        if (pass == PointerEventPass.Main) {
-
-            if (primed &&
-                changes.fastAll { it.changedToUp() }
-            ) {
-                val pointerPxPosition: Offset = changes[0].previousPosition
-                if (changes.fastAny { !upBlockedPointers.contains(it.id) }) {
-                    // If we are primed, all pointers went up, and at least one of the pointers is
-                    // not blocked, we can fire, reset, and consume all of the up events.
-                    reset()
-                    onTap.invoke(pointerPxPosition)
-                    if (consumeChanges) {
-                        changes.fastForEach {
-                            it.consumeDownChange()
-                        }
-                    }
-                    return
-                } else {
-                    lastPxPosition = pointerPxPosition
-                }
-            }
-
-            if (changes.fastAll { it.changedToDown() }) {
-                // Reset in case we were incorrectly left waiting on a delayUp message.
-                reset()
-                // If all of the changes are down, can become primed.
-                primed = true
-            }
-
-            if (primed) {
-                changes.fastForEach {
-                    if (it.changedToDown()) {
-                        downPointers.add(it.id)
-                    }
-                    if (it.changedToUpIgnoreConsumed()) {
-                        downPointers.remove(it.id)
-                    }
-                }
-            }
-        }
-
-        if (pass == PointerEventPass.Final && primed) {
-
-            val anyPositionChangeConsumed = changes.fastAny { it.positionChangeConsumed() }
-
-            val noPointersInBounds =
-                upBlockedPointers.isEmpty() && !changes.anyPointersInBounds(bounds)
-
-            if (anyPositionChangeConsumed || noPointersInBounds) {
-                // If we are on the final pass, we are primed, and either we aren't blocked and
-                // all pointers are out of bounds.
-                reset()
-            }
-        }
-    }
-
-    // TODO(shepshapard): This continues to be very confusing to use.  Have to come up with a better
-//  way of easily expressing this.
-    /**
-     * Utility method that determines if any pointers are currently in [bounds].
-     *
-     * A pointer is considered in bounds if it is currently down and it's current
-     * position is within the provided [bounds]
-     *
-     * @return True if at least one pointer is in bounds.
-     */
-    private fun List<PointerInputChange>.anyPointersInBounds(bounds: IntSize) =
-        fastAny {
-            it.pressed &&
-                it.position.x >= 0 &&
-                it.position.x < bounds.width &&
-                it.position.y >= 0 &&
-                it.position.y < bounds.height
-        }
-
-    override fun onCancel() {
-        reset()
-    }
-
-    private fun reset() {
-        primed = false
-        upBlockedPointers.clear()
-        downPointers.clear()
-        lastPxPosition = null
-    }
-}
-
-private data class TapPointerInputModifierImpl(
-    override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
index 9e500518..fb7ce95 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
@@ -17,19 +17,20 @@
 package androidx.compose.foundation.selection
 
 import androidx.compose.foundation.Indication
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.PressedInteractionSourceDisposableEffect
 import androidx.compose.foundation.LocalIndication
-import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.Strings
+import androidx.compose.foundation.gestures.detectTapAndPress
+import androidx.compose.foundation.handlePressInteraction
 import androidx.compose.foundation.indication
-import androidx.compose.runtime.DisposableEffect
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
-import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
-import androidx.compose.foundation.legacygestures.tapGestureFilter
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.disabled
@@ -42,7 +43,6 @@
 import androidx.compose.ui.state.ToggleableState.Indeterminate
 import androidx.compose.ui.state.ToggleableState.Off
 import androidx.compose.ui.state.ToggleableState.On
-import kotlinx.coroutines.launch
 
 /**
  * Configure component to make it toggleable via input and accessibility events
@@ -233,7 +233,7 @@
     }
 )
 
-@Suppress("ModifierInspectorInfo", "DEPRECATION")
+@Suppress("ModifierInspectorInfo")
 private fun Modifier.toggleableImpl(
     state: ToggleableState,
     enabled: Boolean,
@@ -242,7 +242,6 @@
     indication: Indication?,
     onClick: () -> Unit
 ): Modifier = composed {
-    val scope = rememberCoroutineScope()
     val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
     // TODO(pavlis): Handle multiple states for Semantics
     val semantics = Modifier.semantics(mergeDescendants = true) {
@@ -261,58 +260,22 @@
             disabled()
         }
     }
-    val interactionUpdate =
-        if (enabled) {
-            Modifier.pressIndicatorGestureFilter(
-                onStart = {
-                    scope.launch {
-                        // Remove any old interactions if we didn't fire stop / cancel properly
-                        pressedInteraction.value?.let { oldValue ->
-                            val interaction = PressInteraction.Cancel(oldValue)
-                            interactionSource.emit(interaction)
-                            pressedInteraction.value = null
-                        }
-                        val interaction = PressInteraction.Press(it)
-                        interactionSource.emit(interaction)
-                        pressedInteraction.value = interaction
-                    }
+    val onClickState = rememberUpdatedState(onClick)
+    val gestures = if (enabled) {
+        PressedInteractionSourceDisposableEffect(interactionSource, pressedInteraction)
+        Modifier.pointerInput(interactionSource) {
+            detectTapAndPress(
+                onPress = { offset ->
+                    handlePressInteraction(offset, interactionSource, pressedInteraction)
                 },
-                onStop = {
-                    scope.launch {
-                        pressedInteraction.value?.let {
-                            val interaction = PressInteraction.Release(it)
-                            interactionSource.emit(interaction)
-                            pressedInteraction.value = null
-                        }
-                    }
-                },
-                onCancel = {
-                    scope.launch {
-                        pressedInteraction.value?.let {
-                            val interaction = PressInteraction.Cancel(it)
-                            interactionSource.emit(interaction)
-                            pressedInteraction.value = null
-                        }
-                    }
-                }
+                onTap = { onClickState.value.invoke() }
             )
-        } else {
-            Modifier
         }
-    val click = if (enabled) Modifier.tapGestureFilter { onClick() } else Modifier
-
-    DisposableEffect(interactionSource) {
-        onDispose {
-            pressedInteraction.value?.let { oldValue ->
-                val interaction = PressInteraction.Cancel(oldValue)
-                interactionSource.tryEmit(interaction)
-                pressedInteraction.value = null
-            }
-        }
+    } else {
+        Modifier
     }
     this
         .then(semantics)
         .indication(interactionSource, indication)
-        .then(interactionUpdate)
-        .then(click)
+        .then(gestures)
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index 4b2e134..2f03a87 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -18,21 +18,26 @@
 package androidx.compose.foundation.text
 
 import androidx.compose.foundation.fastMapIndexedNotNull
-import androidx.compose.foundation.legacygestures.DragObserver
+import androidx.compose.foundation.text.selection.LocalSelectionRegistrar
+import androidx.compose.foundation.text.selection.LocalTextSelectionColors
 import androidx.compose.foundation.text.selection.MultiWidgetSelectionDelegate
+import androidx.compose.foundation.text.selection.Selectable
+import androidx.compose.foundation.text.selection.SelectionRegistrar
+import androidx.compose.foundation.text.selection.hasSelection
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.DisposableEffectResult
 import androidx.compose.runtime.DisposableEffectScope
+import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.foundation.legacygestures.LongPressDragObserver
-import androidx.compose.foundation.legacygestures.longPressDragGestureFilter
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.FirstBaseline
 import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.IntrinsicMeasureScope
@@ -47,13 +52,6 @@
 import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalFontLoader
-import androidx.compose.foundation.text.selection.LocalSelectionRegistrar
-import androidx.compose.foundation.text.selection.LocalTextSelectionColors
-import androidx.compose.foundation.text.selection.Selectable
-import androidx.compose.foundation.text.selection.SelectionRegistrar
-import androidx.compose.foundation.text.selection.hasSelection
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.semantics.getTextLayoutResult
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.AnnotatedString
@@ -96,7 +94,6 @@
  */
 @Composable
 @OptIn(InternalFoundationTextApi::class)
-@Suppress("DEPRECATION") // longPressDragGestureFilter
 internal fun CoreText(
     text: AnnotatedString,
     modifier: Modifier = Modifier,
@@ -170,20 +167,13 @@
             .then(
                 if (selectionRegistrar != null) {
                     if (isInTouchMode) {
-                        Modifier.longPressDragGestureFilter(
-                            longPressDragObserver(
-                                state = state,
-                                selectionRegistrar = selectionRegistrar
+                        Modifier.pointerInput(controller.longPressDragObserver) {
+                            detectDragGesturesAfterLongPressWithObserver(
+                                controller.longPressDragObserver
                             )
-                        )
+                        }
                     } else {
-                        Modifier.mouseDragGestureFilter(
-                            mouseSelectionObserver(
-                                state = state,
-                                selectionRegistrar = selectionRegistrar
-                            ),
-                            enabled = true
-                        )
+                        Modifier.mouseDragGestureFilter(controller.longPressDragObserver, true)
                     }
                 } else {
                     Modifier
@@ -213,7 +203,8 @@
 }
 
 @OptIn(InternalFoundationTextApi::class)
-private class TextController(val state: TextState) {
+/*@VisibleForTesting*/
+internal class TextController(val state: TextState) {
     var selectionRegistrar: SelectionRegistrar? = null
 
     fun update(selectionRegistrar: SelectionRegistrar?) {
@@ -227,7 +218,7 @@
         if (selectionRegistrar.hasSelection(state.selectableId)) {
             val newGlobalPosition = it.positionInWindow()
             if (newGlobalPosition != state.previousGlobalPosition) {
-                selectionRegistrar?.notifyPositionChange()
+                selectionRegistrar?.notifyPositionChange(state.selectableId)
             }
             state.previousGlobalPosition = newGlobalPosition
         }
@@ -356,6 +347,65 @@
         }
     }
 
+    val longPressDragObserver: TextDragObserver = object : TextDragObserver {
+        /**
+         * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
+         * recalculated.
+         */
+        var dragBeginPosition = Offset.Zero
+
+        /**
+         * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
+         * it will be zeroed out.
+         */
+        var dragTotalDistance = Offset.Zero
+
+        override fun onStart(startPoint: Offset) {
+            state.layoutCoordinates?.let {
+                if (!it.isAttached) return
+
+                selectionRegistrar?.notifySelectionUpdateStart(
+                    layoutCoordinates = it,
+                    startPosition = startPoint
+                )
+
+                dragBeginPosition = startPoint
+            }
+            // selection never started
+            if (!selectionRegistrar.hasSelection(state.selectableId)) return
+            // Zero out the total distance that being dragged.
+            dragTotalDistance = Offset.Zero
+        }
+
+        override fun onDrag(delta: Offset) {
+            state.layoutCoordinates?.let {
+                if (!it.isAttached) return
+                // selection never started, did not consume any drag
+                if (!selectionRegistrar.hasSelection(state.selectableId)) return
+
+                dragTotalDistance += delta
+
+                selectionRegistrar?.notifySelectionUpdate(
+                    layoutCoordinates = it,
+                    startPosition = dragBeginPosition,
+                    endPosition = dragBeginPosition + dragTotalDistance
+                )
+            }
+        }
+
+        override fun onStop() {
+            if (selectionRegistrar.hasSelection(state.selectableId)) {
+                selectionRegistrar?.notifySelectionUpdateEnd()
+            }
+        }
+
+        override fun onCancel() {
+            if (selectionRegistrar.hasSelection(state.selectableId)) {
+                selectionRegistrar?.notifySelectionUpdateEnd()
+            }
+        }
+    }
+
     /**
      * Draw the given selection on the canvas.
      */
@@ -407,10 +457,13 @@
 
     /** The last layout coordinates for the Text's layout, used by selection */
     var layoutCoordinates: LayoutCoordinates? = null
+
     /** The latest TextLayoutResult calculated in the measure block */
     var layoutResult: TextLayoutResult? = null
+
     /** The global position calculated during the last notifyPosition callback */
     var previousGlobalPosition: Offset = Offset.Zero
+
     /** The paint used to draw highlight background for selected text. */
     val selectionPaint: Paint = Paint()
 }
@@ -488,115 +541,3 @@
     }
     return Pair(placeholders, inlineComposables)
 }
-
-/*@VisibleForTesting*/
-@Suppress("DEPRECATION") // LongPressDragObserver
-internal fun longPressDragObserver(
-    state: TextState,
-    selectionRegistrar: SelectionRegistrar?
-): LongPressDragObserver {
-    /**
-     * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
-     * recalculated.
-     */
-    var dragBeginPosition = Offset.Zero
-
-    /**
-     * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
-     * it will be zeroed out.
-     */
-    var dragTotalDistance = Offset.Zero
-    return object : LongPressDragObserver {
-        override fun onLongPress(pxPosition: Offset) {
-            state.layoutCoordinates?.let {
-                if (!it.isAttached) return
-
-                selectionRegistrar?.notifySelectionUpdateStart(
-                    layoutCoordinates = it,
-                    startPosition = pxPosition
-                )
-
-                dragBeginPosition = pxPosition
-            }
-        }
-
-        override fun onDragStart() {
-            // selection never started
-            if (!selectionRegistrar.hasSelection(state.selectableId)) return
-            // Zero out the total distance that being dragged.
-            dragTotalDistance = Offset.Zero
-        }
-
-        override fun onDrag(dragDistance: Offset): Offset {
-            state.layoutCoordinates?.let {
-                if (!it.isAttached) return Offset.Zero
-                // selection never started, did not consume any drag
-                if (!selectionRegistrar.hasSelection(state.selectableId)) return Offset.Zero
-
-                dragTotalDistance += dragDistance
-
-                selectionRegistrar?.notifySelectionUpdate(
-                    layoutCoordinates = it,
-                    startPosition = dragBeginPosition,
-                    endPosition = dragBeginPosition + dragTotalDistance
-                )
-            }
-            return dragDistance
-        }
-
-        override fun onStop(velocity: Offset) {
-            if (selectionRegistrar.hasSelection(state.selectableId)) {
-                selectionRegistrar?.notifySelectionUpdateEnd()
-            }
-        }
-
-        override fun onCancel() {
-            if (selectionRegistrar.hasSelection(state.selectableId)) {
-                selectionRegistrar?.notifySelectionUpdateEnd()
-            }
-        }
-    }
-}
-
-@Suppress("DEPRECATION") // DragObserver
-internal fun mouseSelectionObserver(
-    state: TextState,
-    selectionRegistrar: SelectionRegistrar?
-): DragObserver {
-    var dragBeginPosition = Offset.Zero
-
-    var dragTotalDistance = Offset.Zero
-    return object : DragObserver {
-        override fun onStart(downPosition: Offset) {
-            state.layoutCoordinates?.let {
-                if (!it.isAttached) return
-
-                selectionRegistrar?.notifySelectionUpdateStart(
-                    layoutCoordinates = it,
-                    startPosition = downPosition
-                )
-
-                dragBeginPosition = downPosition
-            }
-
-            if (!selectionRegistrar.hasSelection(state.selectableId)) return
-            dragTotalDistance = Offset.Zero
-        }
-
-        override fun onDrag(dragDistance: Offset): Offset {
-            state.layoutCoordinates?.let {
-                if (!it.isAttached) return Offset.Zero
-                if (!selectionRegistrar.hasSelection(state.selectableId)) return Offset.Zero
-
-                dragTotalDistance += dragDistance
-
-                selectionRegistrar?.notifySelectionUpdate(
-                    layoutCoordinates = it,
-                    startPosition = dragBeginPosition,
-                    endPosition = dragBeginPosition + dragTotalDistance
-                )
-            }
-            return dragDistance
-        }
-    }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 58f75c4..e86679f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -13,12 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@file:Suppress("DEPRECATION_ERROR")
 
 package androidx.compose.foundation.text
 
-import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -269,9 +268,11 @@
         if (!it.isFocused) manager.deselect()
     }
 
-    val focusRequestTapModifier = Modifier.focusRequestTapModifier(
-        enabled = enabled,
-        onTap = { offset ->
+    val selectionModifier =
+        Modifier.longPressDragGestureFilter(manager.touchSelectionObserver, enabled)
+
+    val pointerModifier = if (isInTouchMode) {
+        Modifier.tapPressTextFieldModifier(interactionSource, enabled) { offset ->
             tapToFocus(state, focusRequester, !readOnly)
             if (state.hasFocus) {
                 if (!state.selectionIsOn) {
@@ -288,16 +289,7 @@
                     manager.deselect(offset)
                 }
             }
-        }
-    )
-
-    val selectionModifier =
-        Modifier.longPressDragGestureFilter(manager.touchSelectionObserver, enabled)
-
-    val pointerModifier = if (isInTouchMode) {
-        Modifier.pressGestureFilter(interactionSource = interactionSource, enabled = enabled)
-            .then(selectionModifier)
-            .then(focusRequestTapModifier)
+        }.then(selectionModifier)
     } else {
         Modifier.mouseDragGestureDetector(manager::mouseSelectionDetector, enabled = enabled)
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
index 822bd71..871e49a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
@@ -40,7 +40,7 @@
      * indicates that the default implementation should be executed. The default implementation
      * moves focus to the next item in the focus traversal order.
      *
-     * @see [Modifier.focusOrder()][androidx.compose.ui.focus.focusOrder] for more details on how
+     * See [Modifier.focusOrder()][androidx.compose.ui.focus.focusOrder] for more details on how
      * to specify a custom focus order if needed.
      */
     val onNext: (KeyboardActionScope.() -> Unit)? = null,
@@ -50,7 +50,7 @@
      * indicates that the default implementation should be executed. The default implementation
      * moves focus to the previous item in the focus traversal order.
      *
-     * @see [Modifier.focusOrder()][androidx.compose.ui.focus.focusOrder] for more details on how
+     * See [Modifier.focusOrder()][androidx.compose.ui.focus.focusOrder] for more details on how
      * to specify a custom focus order if needed.
      */
     val onPrevious: (KeyboardActionScope.() -> Unit)? = null,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/LongPressTextDragObserver.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/LongPressTextDragObserver.kt
new file mode 100644
index 0000000..81c0edc
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/LongPressTextDragObserver.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.consumeAllChanges
+
+internal interface TextDragObserver {
+    fun onStart(startPoint: Offset)
+
+    fun onDrag(delta: Offset)
+
+    fun onStop()
+
+    fun onCancel()
+}
+
+internal suspend fun PointerInputScope.detectDragGesturesAfterLongPressWithObserver(
+    observer: TextDragObserver
+) = detectDragGesturesAfterLongPress(
+    onDragEnd = { observer.onStop() },
+    onDrag = { change, offset ->
+        change.consumeAllChanges()
+        observer.onDrag(offset)
+    },
+    onDragStart = {
+        observer.onStart(it)
+    },
+    onDragCancel = { observer.onCancel() }
+)
+
+internal suspend fun PointerInputScope.detectDragGesturesWithObserver(
+    observer: TextDragObserver
+) = detectDragGestures(
+    onDragEnd = { observer.onStop() },
+    onDrag = { change, offset ->
+        change.consumeAllChanges()
+        observer.onDrag(offset)
+    },
+    onDragStart = {
+        observer.onStart(it)
+    },
+    onDragCancel = { observer.onCancel() }
+)
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
index b1f6e32..258f2b0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
@@ -14,34 +14,33 @@
  * limitations under the License.
  */
 
-@file:Suppress("DEPRECATION") // gestures
-
 package androidx.compose.foundation.text
 
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.drag
+import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.foundation.legacygestures.DragObserver
-import androidx.compose.foundation.legacygestures.LongPressDragObserver
-import androidx.compose.foundation.legacygestures.dragGestureFilter
-import androidx.compose.foundation.legacygestures.longPressDragGestureFilter
-import androidx.compose.foundation.legacygestures.tapGestureFilter
 import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.consumeAllChanges
+import androidx.compose.ui.input.pointer.consumeDownChange
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
 
 // Touch selection
 internal fun Modifier.longPressDragGestureFilter(
-    observer: LongPressDragObserver,
+    observer: TextDragObserver,
     enabled: Boolean
-) = if (enabled) this.then(longPressDragGestureFilter(observer)) else this
-
-internal fun Modifier.focusRequestTapModifier(onTap: (Offset) -> Unit, enabled: Boolean) =
-    if (enabled) this.tapGestureFilter(onTap) else this
+) = if (enabled) {
+    this.pointerInput(observer) { detectDragGesturesAfterLongPressWithObserver(observer) }
+} else {
+    this
+}
 
 // Focus modifiers
 internal fun Modifier.textFieldFocusModifier(
@@ -55,10 +54,25 @@
     .focusable(interactionSource = interactionSource, enabled = enabled)
 
 // Mouse
-internal fun Modifier.mouseDragGestureFilter(
-    dragObserver: DragObserver,
-    enabled: Boolean
-) = if (enabled) this.dragGestureFilter(dragObserver, startDragImmediately = true) else this
+internal fun Modifier.mouseDragGestureFilter(dragObserver: TextDragObserver, enabled: Boolean) =
+    if (enabled) {
+        this.pointerInput(dragObserver) {
+            forEachGesture {
+                awaitPointerEventScope {
+                    val down = awaitFirstDown(requireUnconsumed = false)
+                    down.consumeDownChange()
+                    dragObserver.onStart(down.position)
+                    drag(down.id) { event ->
+                        dragObserver.onDrag(event.positionChange())
+                        event.consumeAllChanges()
+                    }
+                    // specifically don't call observer.onStop/onCancel for mouse case
+                }
+            }
+        }
+    } else {
+        this
+    }
 
 internal fun Modifier.mouseDragGestureDetector(
     detector: suspend PointerInputScope.() -> Unit,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
index b792da1..9281ab6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
@@ -18,7 +18,10 @@
 
 import androidx.compose.foundation.text.selection.TextFieldPreparedSelection
 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
+import androidx.compose.foundation.text.selection.TextPreparedSelectionState
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyEventType
 import androidx.compose.ui.input.key.onKeyEvent
@@ -47,6 +50,7 @@
     val value: TextFieldValue = TextFieldValue(),
     val editable: Boolean = true,
     val singleLine: Boolean = false,
+    val preparedSelectionState: TextPreparedSelectionState,
     val offsetMapping: OffsetMapping = OffsetMapping.Identity,
     private val keyMapping: KeyMapping = platformDefaultKeyMapping,
 ) {
@@ -67,6 +71,7 @@
         typedCommand(event)?.let {
             return if (editable) {
                 it.apply()
+                preparedSelectionState.resetCachedX()
                 true
             } else {
                 false
@@ -165,7 +170,8 @@
         val preparedSelection = TextFieldPreparedSelection(
             currentValue = value,
             offsetMapping = offsetMapping,
-            layoutResultProxy = state.layoutResult
+            layoutResultProxy = state.layoutResult,
+            state = preparedSelectionState
         )
         block(preparedSelection)
         if (preparedSelection.selection != value.selection ||
@@ -176,6 +182,7 @@
     }
 }
 
+@Suppress("ModifierInspectorInfo")
 internal fun Modifier.textFieldKeyInput(
     state: TextFieldState,
     manager: TextFieldSelectionManager,
@@ -183,14 +190,16 @@
     editable: Boolean,
     singleLine: Boolean,
     offsetMapping: OffsetMapping
-): Modifier {
+) = composed {
+    val preparedSelectionState = remember { TextPreparedSelectionState() }
     val processor = TextFieldKeyInput(
         state = state,
         selectionManager = manager,
         value = value,
         editable = editable,
         singleLine = singleLine,
-        offsetMapping = offsetMapping
+        offsetMapping = offsetMapping,
+        preparedSelectionState = preparedSelectionState
     )
-    return Modifier.onKeyEvent(processor::process)
+    Modifier.onKeyEvent(processor::process)
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
index 493e81c..24af865 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
@@ -16,27 +16,32 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.gestures.detectTapAndPress
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.pointerInput
 import kotlinx.coroutines.launch
-import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
 
 /**
- * Required for the press [MutableInteractionSource] consistency for TextField.
+ * Required for the press and tap [MutableInteractionSource] consistency for TextField.
  */
-@Suppress("ModifierInspectorInfo", "DEPRECATION")
-internal fun Modifier.pressGestureFilter(
+@Suppress("ModifierInspectorInfo")
+internal fun Modifier.tapPressTextFieldModifier(
     interactionSource: MutableInteractionSource?,
-    enabled: Boolean = true
+    enabled: Boolean = true,
+    onTap: (Offset) -> Unit
 ): Modifier = if (enabled) composed {
     val scope = rememberCoroutineScope()
     val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
+    val onTapState = rememberUpdatedState(onTap)
     DisposableEffect(interactionSource) {
         onDispose {
             pressedInteraction.value?.let { oldValue ->
@@ -46,37 +51,35 @@
             }
         }
     }
-    pressIndicatorGestureFilter(
-        onStart = {
-            scope.launch {
-                // Remove any old interactions if we didn't fire stop / cancel properly
-                pressedInteraction.value?.let { oldValue ->
-                    val interaction = PressInteraction.Cancel(oldValue)
+    Modifier.pointerInput(interactionSource) {
+        detectTapAndPress(
+            onPress = {
+                scope.launch {
+                    // Remove any old interactions if we didn't fire stop / cancel properly
+                    pressedInteraction.value?.let { oldValue ->
+                        val interaction = PressInteraction.Cancel(oldValue)
+                        interactionSource?.emit(interaction)
+                        pressedInteraction.value = null
+                    }
+                    val interaction = PressInteraction.Press(it)
                     interactionSource?.emit(interaction)
-                    pressedInteraction.value = null
+                    pressedInteraction.value = interaction
                 }
-                val interaction = PressInteraction.Press(it)
-                interactionSource?.emit(interaction)
-                pressedInteraction.value = interaction
-            }
-        },
-        onStop = {
-            scope.launch {
-                pressedInteraction.value?.let {
-                    val interaction = PressInteraction.Release(it)
-                    interactionSource?.emit(interaction)
-                    pressedInteraction.value = null
+                val success = tryAwaitRelease()
+                scope.launch {
+                    pressedInteraction.value?.let { oldValue ->
+                        val interaction =
+                            if (success) {
+                                PressInteraction.Release(oldValue)
+                            } else {
+                                PressInteraction.Cancel(oldValue)
+                            }
+                        interactionSource?.emit(interaction)
+                        pressedInteraction.value = null
+                    }
                 }
-            }
-        },
-        onCancel = {
-            scope.launch {
-                pressedInteraction.value?.let {
-                    val interaction = PressInteraction.Cancel(it)
-                    interactionSource?.emit(interaction)
-                    pressedInteraction.value = null
-                }
-            }
-        }
-    )
+            },
+            onTap = { onTapState.value.invoke(it) }
+        )
+    }
 } else this
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index 413118a..d3da4b5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.text.selection
 
+import androidx.compose.foundation.text.detectDragGesturesWithObserver
 import androidx.compose.foundation.text.isInTouchMode
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -26,7 +27,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.foundation.legacygestures.dragGestureFilter
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalClipboardManager
 import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.LocalTextToolbar
@@ -71,7 +72,7 @@
  * area with start and end handles.
  */
 @OptIn(ExperimentalComposeUiApi::class)
-@Suppress("ComposableLambdaParameterNaming", "DEPRECATION")
+@Suppress("ComposableLambdaParameterNaming")
 @Composable
 internal fun SelectionContainer(
     /** A [Modifier] for SelectionContainer. */
@@ -100,17 +101,18 @@
             if (isInTouchMode && manager.hasFocus) {
                 manager.selection?.let {
                     listOf(true, false).fastForEach { isStartHandle ->
+                        val observer = remember(isStartHandle) {
+                            manager.handleDragObserver(isStartHandle)
+                        }
                         SelectionHandle(
                             startHandlePosition = manager.startHandlePosition,
                             endHandlePosition = manager.endHandlePosition,
                             isStartHandle = isStartHandle,
                             directions = Pair(it.start.direction, it.end.direction),
                             handlesCrossed = it.handlesCrossed,
-                            modifier = Modifier.dragGestureFilter(
-                                manager.handleDragObserver(
-                                    isStartHandle
-                                )
-                            ),
+                            modifier = Modifier.pointerInput(observer) {
+                                detectDragGesturesWithObserver(observer)
+                            },
                             handle = null
                         )
                     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 53acc0b..cdbf21c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -32,7 +32,7 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.foundation.legacygestures.DragObserver
+import androidx.compose.foundation.text.TextDragObserver
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.input.key.KeyEvent
@@ -40,6 +40,7 @@
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.TextToolbarStatus
@@ -121,13 +122,16 @@
             }
         }
 
+    private var previousPosition: Offset? = null
     /**
      * Layout Coordinates of the selection container.
      */
     var containerLayoutCoordinates: LayoutCoordinates? = null
         set(value) {
             field = value
-            if (hasFocus) {
+            val positionInWindow = value?.positionInWindow()
+            if (hasFocus && previousPosition != positionInWindow) {
+                previousPosition = positionInWindow
                 updateHandleOffsets()
                 updateSelectionToolbarPosition()
             }
@@ -170,9 +174,14 @@
         private set
 
     init {
-        selectionRegistrar.onPositionChangeCallback = {
-            updateHandleOffsets()
-            updateSelectionToolbarPosition()
+        selectionRegistrar.onPositionChangeCallback = { selectableId ->
+            if (
+                selectableId == selection?.start?.selectableId ||
+                selectableId == selection?.end?.selectableId
+            ) {
+                updateHandleOffsets()
+                updateSelectionToolbarPosition()
+            }
         }
 
         selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
@@ -458,9 +467,9 @@
         }
     }
 
-    fun handleDragObserver(isStartHandle: Boolean): DragObserver {
-        return object : DragObserver {
-            override fun onStart(downPosition: Offset) {
+    fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
+        return object : TextDragObserver {
+            override fun onStart(startPoint: Offset) {
                 hideSelectionToolbar()
                 val selection = selection!!
                 val startSelectable =
@@ -501,9 +510,9 @@
                 dragTotalDistance = Offset.Zero
             }
 
-            override fun onDrag(dragDistance: Offset): Offset {
+            override fun onDrag(delta: Offset) {
                 val selection = selection!!
-                dragTotalDistance += dragDistance
+                dragTotalDistance += delta
                 val startSelectable =
                     selectionRegistrar.selectableMap[selection.start.selectableId]
                 val endSelectable =
@@ -540,10 +549,10 @@
                     endPosition = currentEnd,
                     isStartHandle = isStartHandle
                 )
-                return dragDistance
+                return
             }
 
-            override fun onStop(velocity: Offset) {
+            override fun onStop() {
                 showSelectionToolbar()
             }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrar.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrar.kt
index 5d6fdab..25ae682 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrar.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrar.kt
@@ -55,7 +55,7 @@
      * When the Global Position of a subscribed [Selectable] changes, this method
      * is called.
      */
-    fun notifyPositionChange()
+    fun notifyPositionChange(selectableId: Long)
 
     /**
      * Call this method to notify the [SelectionContainer] that the selection has been initiated.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
index 3cda909..f43c89f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
@@ -59,7 +59,7 @@
     /**
      * The callback to be invoked when the position change was triggered.
      */
-    internal var onPositionChangeCallback: (() -> Unit)? = null
+    internal var onPositionChangeCallback: ((Long) -> Unit)? = null
 
     /**
      * The callback to be invoked when the selection is initiated.
@@ -150,11 +150,11 @@
         return selectables
     }
 
-    override fun notifyPositionChange() {
+    override fun notifyPositionChange(selectableId: Long) {
         // Set the variable sorted to be false, when the global position of a registered
         // selectable changes.
         sorted = false
-        onPositionChangeCallback?.invoke()
+        onPositionChangeCallback?.invoke(selectableId)
     }
 
     override fun notifySelectionUpdateStart(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 55616a6..b36438c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -14,26 +14,24 @@
  * limitations under the License.
  */
 
-@file:Suppress("DEPRECATION") // gestures
-
 package androidx.compose.foundation.text.selection
 
 import androidx.compose.foundation.gestures.drag
 import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.foundation.text.InternalFoundationTextApi
+import androidx.compose.foundation.text.TextDragObserver
 import androidx.compose.foundation.text.TextFieldDelegate
 import androidx.compose.foundation.text.TextFieldState
+import androidx.compose.foundation.text.detectDragGesturesWithObserver
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.foundation.legacygestures.DragObserver
-import androidx.compose.foundation.legacygestures.LongPressDragObserver
-import androidx.compose.foundation.legacygestures.dragGestureFilter
-import androidx.compose.foundation.text.InternalFoundationTextApi
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
@@ -43,6 +41,7 @@
 import androidx.compose.ui.input.pointer.changedToDown
 import androidx.compose.ui.input.pointer.consumeAllChanges
 import androidx.compose.ui.input.pointer.consumeDownChange
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.positionChange
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
@@ -146,20 +145,20 @@
     private var oldValue: TextFieldValue = TextFieldValue()
 
     /**
-     * [LongPressDragObserver] for long press and drag to select in TextField.
+     * [TextDragObserver] for long press and drag to select in TextField.
      */
-    internal val touchSelectionObserver = object : LongPressDragObserver {
-        override fun onLongPress(pxPosition: Offset) {
+    internal val touchSelectionObserver = object : TextDragObserver {
+        override fun onStart(startPoint: Offset) {
             state?.let {
                 if (it.draggingHandle) return
             }
 
             // Long Press at the blank area, the cursor should show up at the end of the line.
-            if (state?.layoutResult?.isPositionOnText(pxPosition) != true) {
+            if (state?.layoutResult?.isPositionOnText(startPoint) != true) {
                 state?.layoutResult?.let { layoutResult ->
                     val offset = offsetMapping.transformedToOriginal(
                         layoutResult.getLineEnd(
-                            layoutResult.getLineForVerticalPosition(pxPosition.y)
+                            layoutResult.getLineForVerticalPosition(startPoint.y)
                         )
                     )
                     hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
@@ -178,7 +177,7 @@
             if (value.text.isEmpty()) return
             enterSelectionMode()
             state?.layoutResult?.let { layoutResult ->
-                val offset = layoutResult.getOffsetForPosition(pxPosition)
+                val offset = layoutResult.getOffsetForPosition(startPoint)
                 updateSelection(
                     value = value,
                     transformedStartOffset = offset,
@@ -188,15 +187,15 @@
                 )
                 dragBeginOffsetInText = offset
             }
-            dragBeginPosition = pxPosition
+            dragBeginPosition = startPoint
             dragTotalDistance = Offset.Zero
         }
 
-        override fun onDrag(dragDistance: Offset): Offset {
+        override fun onDrag(delta: Offset) {
             // selection never started, did not consume any drag
-            if (value.text.isEmpty()) return Offset.Zero
+            if (value.text.isEmpty()) return
 
-            dragTotalDistance += dragDistance
+            dragTotalDistance += delta
             state?.layoutResult?.let { layoutResult ->
                 val startOffset = dragBeginOffsetInText ?: layoutResult.getOffsetForPosition(
                     position = dragBeginPosition,
@@ -215,15 +214,15 @@
                 )
             }
             state?.showFloatingToolbar = false
-            return dragDistance
         }
 
-        override fun onStop(velocity: Offset) {
-            super.onStop(velocity)
+        override fun onStop() {
             state?.showFloatingToolbar = true
             if (textToolbar?.status == TextToolbarStatus.Hidden) showSelectionToolbar()
             dragBeginOffsetInText = null
         }
+
+        override fun onCancel() {}
     }
 
     internal interface MouseSelectionObserver {
@@ -267,6 +266,7 @@
                 }
             }
         }
+
         override fun onDrag(dragDistance: Offset) {
             if (value.text.isEmpty()) return
 
@@ -305,11 +305,11 @@
     }
 
     /**
-     * [DragObserver] for dragging the selection handles to change the selection in TextField.
+     * [TextDragObserver] for dragging the selection handles to change the selection in TextField.
      */
-    internal fun handleDragObserver(isStartHandle: Boolean): DragObserver {
-        return object : DragObserver {
-            override fun onStart(downPosition: Offset) {
+    internal fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
+        return object : TextDragObserver {
+            override fun onStart(startPoint: Offset) {
                 // The position of the character where the drag gesture should begin. This is in
                 // the composable coordinates.
                 dragBeginPosition = getAdjustedCoordinates(getHandlePosition(isStartHandle))
@@ -319,8 +319,8 @@
                 state?.showFloatingToolbar = false
             }
 
-            override fun onDrag(dragDistance: Offset): Offset {
-                dragTotalDistance += dragDistance
+            override fun onDrag(delta: Offset) {
+                dragTotalDistance += delta
 
                 state?.layoutResult?.value?.let { layoutResult ->
                     val startOffset = if (isStartHandle)
@@ -342,15 +342,15 @@
                     )
                 }
                 state?.showFloatingToolbar = false
-                return dragDistance
             }
 
-            override fun onStop(velocity: Offset) {
-                super.onStop(velocity)
+            override fun onStop() {
                 state?.draggingHandle = false
                 state?.showFloatingToolbar = true
                 if (textToolbar?.status == TextToolbarStatus.Hidden) showSelectionToolbar()
             }
+
+            override fun onCancel() {}
         }
     }
 
@@ -670,13 +670,18 @@
     directions: Pair<ResolvedTextDirection, ResolvedTextDirection>,
     manager: TextFieldSelectionManager
 ) {
+    val observer = remember(isStartHandle, manager) {
+        manager.handleDragObserver(isStartHandle)
+    }
     SelectionHandle(
         startHandlePosition = manager.getHandlePosition(true),
         endHandlePosition = manager.getHandlePosition(false),
         isStartHandle = isStartHandle,
         directions = directions,
         handlesCrossed = manager.value.selection.reversed,
-        modifier = Modifier.dragGestureFilter(manager.handleDragObserver(isStartHandle)),
+        modifier = Modifier.pointerInput(observer) {
+            detectDragGesturesWithObserver(observer)
+        },
         handle = null
     )
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
index 68d1712..cb83d7b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
@@ -30,6 +30,16 @@
 import kotlin.math.max
 import kotlin.math.min
 
+internal class TextPreparedSelectionState {
+    // it's set at the start of vertical navigation and used as the preferred value to set a new
+    // cursor position.
+    var cachedX: Float? = null
+
+    fun resetCachedX() {
+        cachedX = null
+    }
+}
+
 /**
  * This utility class implements many selection-related operations on text (including basic
  * cursor movements and deletions) and combines them, taking into account how the text was
@@ -47,25 +57,29 @@
     val originalText: AnnotatedString,
     val originalSelection: TextRange,
     val layoutResult: TextLayoutResult?,
-    val offsetMapping: OffsetMapping
+    val offsetMapping: OffsetMapping,
+    val state: TextPreparedSelectionState
 ) {
     var selection = originalSelection
 
     var annotatedString = originalText
-    protected val text
+    internal val text
         get() = annotatedString.text
 
     @Suppress("UNCHECKED_CAST")
-    inline fun <U> U.apply(block: U.() -> Unit): T {
+    protected inline fun <U> U.apply(resetCachedX: Boolean = true, block: U.() -> Unit): T {
+        if (resetCachedX) {
+            state.resetCachedX()
+        }
         block()
         return this as T
     }
 
-    fun setCursor(offset: Int) = apply {
+    protected fun setCursor(offset: Int) {
         setSelection(offset, offset)
     }
 
-    fun setSelection(start: Int, end: Int) = apply {
+    protected fun setSelection(start: Int, end: Int) {
         selection = TextRange(start, end)
     }
 
@@ -175,11 +189,11 @@
         setCursor(getParagraphEnd())
     }
 
-    fun moveCursorUpByLine() = apply {
+    fun moveCursorUpByLine() = apply(false) {
         layoutResult?.jumpByLinesOffset(-1)?.let { setCursor(it) }
     }
 
-    fun moveCursorDownByLine() = apply {
+    fun moveCursorDownByLine() = apply(false) {
         layoutResult?.jumpByLinesOffset(1)?.let { setCursor(it) }
     }
 
@@ -208,11 +222,16 @@
     }
 
     // it selects a text from the original selection start to a current selection end
-    fun selectMovement() = apply {
+    fun selectMovement() = apply(false) {
         selection = TextRange(originalSelection.start, selection.end)
     }
 
-    // delete currently selected text and update [selection] and [annotatedString]
+    /**
+     * delete currently selected text and update [selection] and [annotatedString]
+     *
+     * it supposed to be the last operation, it doesn't relayout text by itself, so any
+     * subsequent calls could give wrong results
+     */
     fun deleteSelected() = apply {
         val maxChars = text.length
         val beforeSelection =
@@ -224,7 +243,7 @@
     }
 
     private fun isLtr(): Boolean {
-        val direction = layoutResult?.getBidiRunDirection(selection.end)
+        val direction = layoutResult?.getParagraphDirection(selection.end)
         return direction != ResolvedTextDirection.Rtl
     }
 
@@ -273,22 +292,26 @@
     private fun TextLayoutResult.jumpByLinesOffset(linesAmount: Int): Int {
         val currentOffset = transformedEndOffset()
 
-        val newLine = getLineForOffset(currentOffset) + linesAmount
+        if (state.cachedX == null) {
+            state.cachedX = getCursorRect(currentOffset).left
+        }
+
+        val targetLine = getLineForOffset(currentOffset) + linesAmount
         when {
-            newLine < 0 -> {
+            targetLine < 0 -> {
                 return 0
             }
-            newLine >= lineCount -> {
+            targetLine >= lineCount -> {
                 return text.length
             }
         }
 
-        val y = getLineBottom(newLine) - 1
-        val x = getCursorRect(currentOffset).left.also {
-            if ((isLtr() && it >= getLineRight(newLine)) ||
-                (!isLtr() && it <= getLineLeft(newLine))
+        val y = getLineBottom(targetLine) - 1
+        val x = state.cachedX!!.also {
+            if ((isLtr() && it >= getLineRight(targetLine)) ||
+                (!isLtr() && it <= getLineLeft(targetLine))
             ) {
-                return getLineEnd(newLine, true)
+                return getLineEnd(targetLine, true)
             }
         }
 
@@ -300,19 +323,19 @@
     }
 
     private fun transformedEndOffset(): Int {
-        return offsetMapping.originalToTransformed(originalSelection.end)
+        return offsetMapping.originalToTransformed(selection.end)
     }
 
     private fun transformedMinOffset(): Int {
-        return offsetMapping.originalToTransformed(originalSelection.min)
+        return offsetMapping.originalToTransformed(selection.min)
     }
 
     private fun transformedMaxOffset(): Int {
-        return offsetMapping.originalToTransformed(originalSelection.max)
+        return offsetMapping.originalToTransformed(selection.max)
     }
 
     private fun charOffset(offset: Int) =
-        offset.coerceAtMost(originalText.length - 1)
+        offset.coerceAtMost(text.length - 1)
 
     private fun getParagraphStart(): Int {
         var index = selection.min
@@ -343,15 +366,31 @@
     }
 }
 
+internal class TextPreparedSelection(
+    originalText: AnnotatedString,
+    originalSelection: TextRange,
+    layoutResult: TextLayoutResult? = null,
+    offsetMapping: OffsetMapping = OffsetMapping.Identity,
+    state: TextPreparedSelectionState = TextPreparedSelectionState()
+) : BaseTextPreparedSelection<TextPreparedSelection>(
+    originalText = originalText,
+    originalSelection = originalSelection,
+    layoutResult = layoutResult,
+    offsetMapping = offsetMapping,
+    state = state
+)
+
 internal class TextFieldPreparedSelection(
     val currentValue: TextFieldValue,
     offsetMapping: OffsetMapping = OffsetMapping.Identity,
-    val layoutResultProxy: TextLayoutResultProxy?
+    val layoutResultProxy: TextLayoutResultProxy?,
+    state: TextPreparedSelectionState = TextPreparedSelectionState()
 ) : BaseTextPreparedSelection<TextFieldPreparedSelection>(
     originalText = currentValue.annotatedString,
     originalSelection = currentValue.selection,
     offsetMapping = offsetMapping,
-    layoutResult = layoutResultProxy?.value
+    layoutResult = layoutResultProxy?.value,
+    state = state
 ) {
     val value
         get() = currentValue.copy(
@@ -367,11 +406,11 @@
         }
     }
 
-    fun moveCursorUpByPage() = apply {
+    fun moveCursorUpByPage() = apply(false) {
         layoutResultProxy?.jumpByPagesOffset(-1)?.let { setCursor(it) }
     }
 
-    fun moveCursorDownByPage() = apply {
+    fun moveCursorDownByPage() = apply(false) {
         layoutResultProxy?.jumpByPagesOffset(1)?.let { setCursor(it) }
     }
 
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
index eab10ec..71ed883 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
@@ -14,36 +14,39 @@
  * limitations under the License.
  */
 
-@file:Suppress("DEPRECATION")
-
 package androidx.compose.foundation
 
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.drag
+import androidx.compose.foundation.gestures.forEachGesture
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.foundation.legacygestures.DragObserver
-import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
-import androidx.compose.foundation.legacygestures.rawDragGestureFilter
 import androidx.compose.foundation.interaction.DragInteraction
 import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.gestures.detectTapAndPress
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.input.pointer.pointerMoveFilter
+import androidx.compose.ui.input.pointer.consumePositionChange
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.platform.LocalDensity
@@ -53,7 +56,6 @@
 import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlin.math.roundToInt
 import kotlin.math.sign
@@ -181,7 +183,6 @@
     interactionSource: MutableInteractionSource,
     isVertical: Boolean
 ) = with(LocalDensity.current) {
-    val scope = rememberCoroutineScope()
     val dragInteraction = remember { mutableStateOf<DragInteraction.Start?>(null) }
     DisposableEffect(interactionSource) {
         onDispose {
@@ -211,48 +212,6 @@
         }
     }
 
-    val dragObserver = object : DragObserver {
-        override fun onStart(downPosition: Offset) {
-            scope.launch {
-                dragInteraction.value?.let { oldInteraction ->
-                    interactionSource.emit(
-                        DragInteraction.Cancel(oldInteraction)
-                    )
-                }
-                val interaction = DragInteraction.Start()
-                interactionSource.emit(interaction)
-                dragInteraction.value = interaction
-            }
-        }
-
-        override fun onStop(velocity: Offset) {
-            scope.launch {
-                dragInteraction.value?.let { interaction ->
-                    interactionSource.emit(
-                        DragInteraction.Stop(interaction)
-                    )
-                    dragInteraction.value = null
-                }
-            }
-        }
-
-        override fun onCancel() {
-            scope.launch {
-                dragInteraction.value?.let { interaction ->
-                    interactionSource.emit(
-                        DragInteraction.Cancel(interaction)
-                    )
-                    dragInteraction.value = null
-                }
-            }
-        }
-
-        override fun onDrag(dragDistance: Offset): Offset {
-            sliderAdapter.position += if (isVertical) dragDistance.y else dragDistance.x
-            return dragDistance
-        }
-    }
-
     val color by animateColorAsState(
         if (isHover) style.hoverColor else style.unhoverColor,
         animationSpec = TweenSpec(durationMillis = style.hoverDurationMillis)
@@ -265,7 +224,9 @@
             Box(
                 Modifier
                     .background(if (isVisible) color else Color.Transparent, style.shape)
-                    .rawDragGestureFilter(dragObserver)
+                    .scrollbarDrag(interactionSource, dragInteraction) { offset ->
+                        sliderAdapter.position += if (isVertical) offset.y else offset.x
+                    }
             )
         },
         modifier
@@ -278,7 +239,32 @@
     )
 }
 
-@Suppress("DEPRECATION") // press gesture filter
+private fun Modifier.scrollbarDrag(
+    interactionSource: MutableInteractionSource,
+    draggedInteraction: MutableState<DragInteraction.Start?>,
+    onDelta: (Offset) -> Unit
+): Modifier = pointerInput(interactionSource, draggedInteraction, onDelta) {
+    forEachGesture {
+        awaitPointerEventScope {
+            val down = awaitFirstDown(requireUnconsumed = false)
+            val interaction = DragInteraction.Start()
+            interactionSource.tryEmit(interaction)
+            draggedInteraction.value = interaction
+            val isSuccess = drag(down.id) { change ->
+                onDelta.invoke(change.positionChange())
+                change.consumePositionChange()
+            }
+            val finishInteraction = if (isSuccess) {
+                DragInteraction.Stop(interaction)
+            } else {
+                DragInteraction.Cancel(interaction)
+            }
+            interactionSource.tryEmit(finishInteraction)
+            draggedInteraction.value = null
+        }
+    }
+}
+
 private fun Modifier.scrollOnPressOutsideSlider(
     isVertical: Boolean,
     sliderAdapter: SliderAdapter,
@@ -309,12 +295,16 @@
             }
         }
     }
-
-    pressIndicatorGestureFilter(
-        onStart = { targetOffset = it },
-        onStop = { targetOffset = null },
-        onCancel = { targetOffset = null }
-    )
+    Modifier.pointerInput(Unit) {
+        detectTapAndPress(
+            onPress = { offset ->
+                targetOffset = offset
+                tryAwaitRelease()
+                targetOffset = null
+            },
+            onTap = {}
+        )
+    }
 }
 
 /**
@@ -546,4 +536,4 @@
  * The time that must elapse before a tap gesture sends onTapDown, if there's
  * any doubt that the gesture is a tap.
  */
-private const val PressTimeoutMillis: Long = 100L
\ No newline at end of file
+private const val PressTimeoutMillis: Long = 100L
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
index 1e62c8e..b5ade04 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
@@ -18,7 +18,6 @@
 
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -32,7 +31,6 @@
 import org.junit.runners.JUnit4
 
 @Suppress("RemoveExplicitTypeArguments")
-@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(JUnit4::class)
 class MutatorMutexTest {
     interface MutateCaller {
@@ -42,12 +40,12 @@
         ): R
     }
 
-    class MutateWithoutReceiverCaller(val mutex: MutatorMutex) : MutateCaller {
+    class MutateWithoutReceiverCaller(private val mutex: MutatorMutex) : MutateCaller {
         override suspend fun <R> mutate(priority: MutatePriority, block: suspend () -> R): R =
             mutex.mutate(priority, block)
     }
 
-    class MutateWithReceiverCaller(val mutex: MutatorMutex) : MutateCaller {
+    class MutateWithReceiverCaller(private val mutex: MutatorMutex) : MutateCaller {
         override suspend fun <R> mutate(priority: MutatePriority, block: suspend () -> R): R {
             val receiver = Any()
             return mutex.mutateWith(receiver, priority) {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
index 8f068f1..058acdd 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
@@ -62,6 +62,22 @@
         )
     }
 
+    private val utilWithShortcut = SuspendingGestureTestUtil {
+        detectTapAndPress(
+            onPress = {
+                pressed = true
+                if (tryAwaitRelease()) {
+                    released = true
+                } else {
+                    canceled = true
+                }
+            },
+            onTap = {
+                tapped = true
+            }
+        )
+    }
+
     private val allGestures = SuspendingGestureTestUtil {
         detectTapGestures(
             onPress = {
@@ -113,6 +129,26 @@
      * Clicking in the region should result in the callback being invoked.
      */
     @Test
+    fun normalTap_withShortcut() = utilWithShortcut.executeInComposition {
+        val down = down(5f, 5f)
+        assertTrue(down.consumed.downChange)
+
+        assertTrue(pressed)
+        assertFalse(tapped)
+        assertFalse(released)
+
+        val up = down.up(50)
+        assertTrue(up.consumed.downChange)
+
+        assertTrue(tapped)
+        assertTrue(released)
+        assertFalse(canceled)
+    }
+
+    /**
+     * Clicking in the region should result in the callback being invoked.
+     */
+    @Test
     fun normalTapWithAllGestures() = allGestures.executeInComposition {
         val down = down(5f, 5f)
         assertTrue(down.consumed.downChange)
@@ -206,6 +242,23 @@
      * the callback not being invoked
      */
     @Test
+    fun tapMiss_withShortcut() = utilWithShortcut.executeInComposition {
+        val up = down(5f, 5f)
+            .moveTo(15f, 15f)
+            .up()
+
+        assertTrue(pressed)
+        assertTrue(canceled)
+        assertFalse(released)
+        assertFalse(tapped)
+        assertFalse(up.consumed.downChange)
+    }
+
+    /**
+     * Pressing in the region, sliding out and then lifting should result in
+     * the callback not being invoked
+     */
+    @Test
     fun longPressMiss() = allGestures.executeInComposition {
         val pointer = down(5f, 5f)
             .moveTo(15f, 15f)
@@ -271,6 +324,24 @@
     }
 
     /**
+     * Pressing in the region, sliding out, then back in, then lifting
+     * should result the gesture being canceled.
+     */
+    @Test
+    fun tapOutAndIn_withShortcut() = utilWithShortcut.executeInComposition {
+        val up = down(5f, 5f)
+            .moveTo(15f, 15f)
+            .moveTo(6f, 6f)
+            .up()
+
+        assertFalse(tapped)
+        assertFalse(up.consumed.downChange)
+        assertTrue(pressed)
+        assertFalse(released)
+        assertTrue(canceled)
+    }
+
+    /**
      * After a first tap, a second tap should also be detected.
      */
     @Test
@@ -296,6 +367,31 @@
     }
 
     /**
+     * After a first tap, a second tap should also be detected.
+     */
+    @Test
+    fun secondTap_withShortcut() = utilWithShortcut.executeInComposition {
+        down(5f, 5f)
+            .up()
+
+        assertTrue(pressed)
+        assertTrue(released)
+        assertFalse(canceled)
+
+        tapped = false
+        pressed = false
+        released = false
+
+        val up2 = down(4f, 4f)
+            .up()
+        assertTrue(tapped)
+        assertTrue(up2.consumed.downChange)
+        assertTrue(pressed)
+        assertTrue(released)
+        assertFalse(canceled)
+    }
+
+    /**
      * Clicking in the region with the up already consumed should result in the callback not
      * being invoked.
      */
@@ -316,6 +412,26 @@
     }
 
     /**
+     * Clicking in the region with the up already consumed should result in the callback not
+     * being invoked.
+     */
+    @Test
+    fun consumedUpTap_withShortcut() = utilWithShortcut.executeInComposition {
+        val down = down(5f, 5f)
+
+        assertFalse(tapped)
+        assertTrue(pressed)
+
+        down.up {
+            consumeDownChange()
+        }
+
+        assertFalse(tapped)
+        assertFalse(released)
+        assertTrue(canceled)
+    }
+
+    /**
      * Clicking in the region with the motion consumed should result in the callback not
      * being invoked.
      */
@@ -334,6 +450,24 @@
     }
 
     /**
+     * Clicking in the region with the motion consumed should result in the callback not
+     * being invoked.
+     */
+    @Test
+    fun consumedMotionTap_withShortcut() = utilWithShortcut.executeInComposition {
+        down(5f, 5f)
+            .moveTo(6f, 2f) {
+                consumePositionChange()
+            }
+            .up(50)
+
+        assertFalse(tapped)
+        assertTrue(pressed)
+        assertFalse(released)
+        assertTrue(canceled)
+    }
+
+    /**
      * Ensure that two-finger taps work.
      */
     @Test
@@ -363,6 +497,35 @@
     }
 
     /**
+     * Ensure that two-finger taps work.
+     */
+    @Test
+    fun twoFingerTap_withShortcut() = utilWithShortcut.executeInComposition {
+        val down = down(1f, 1f)
+        assertTrue(down.consumed.downChange)
+
+        assertTrue(pressed)
+        pressed = false
+
+        val down2 = down(9f, 5f)
+        assertFalse(down2.consumed.downChange)
+
+        assertFalse(pressed)
+
+        val up = down.up()
+        assertFalse(up.consumed.downChange)
+        assertFalse(tapped)
+        assertFalse(released)
+
+        val up2 = down2.up()
+        assertTrue(up2.consumed.downChange)
+
+        assertTrue(tapped)
+        assertTrue(released)
+        assertFalse(canceled)
+    }
+
+    /**
      * A position change consumption on any finger should cause tap to cancel.
      */
     @Test
@@ -389,6 +552,32 @@
     }
 
     /**
+     * A position change consumption on any finger should cause tap to cancel.
+     */
+    @Test
+    fun twoFingerTapCancel_withShortcut() = utilWithShortcut.executeInComposition {
+        val down = down(1f, 1f)
+
+        assertTrue(pressed)
+
+        val down2 = down(9f, 5f)
+
+        val up = down.moveTo(5f, 5f) {
+            consumePositionChange()
+        }.up()
+        assertFalse(up.consumed.downChange)
+
+        assertFalse(tapped)
+        assertTrue(canceled)
+
+        val up2 = down2.up(50)
+        assertFalse(up2.consumed.downChange)
+
+        assertFalse(tapped)
+        assertFalse(released)
+    }
+
+    /**
      * Detect the second tap as long press.
      */
     @Test
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 8fee380..a1583ec 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -14,18 +14,14 @@
  * limitations under the License.
  */
 
-@file:Suppress("DEPRECATION") // LongPressDragObserver
-
 package androidx.compose.foundation.text
 
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.foundation.legacygestures.LongPressDragObserver
-import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.foundation.text.selection.Selectable
 import androidx.compose.foundation.text.selection.Selection
 import androidx.compose.foundation.text.selection.SelectionRegistrarImpl
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.style.ResolvedTextDirection
-import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.doReturn
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.spy
@@ -59,7 +55,7 @@
         )
     )
 
-    private lateinit var gesture: LongPressDragObserver
+    private lateinit var gesture: TextDragObserver
     private lateinit var layoutCoordinates: LayoutCoordinates
     private lateinit var state: TextState
 
@@ -74,17 +70,17 @@
         state = TextState(mock(), selectableId)
         state.layoutCoordinates = layoutCoordinates
 
-        gesture = longPressDragObserver(
-            state = state,
-            selectionRegistrar = selectionRegistrar
-        )
+        val controller = TextController(state).also {
+            it.update(selectionRegistrar)
+        }
+        gesture = controller.longPressDragObserver
     }
 
     @Test
     fun longPressDragObserver_onLongPress_calls_notifySelectionInitiated() {
         val position = Offset(100f, 100f)
 
-        gesture.onLongPress(position)
+        gesture.onStart(position)
 
         verify(selectionRegistrar, times(1)).notifySelectionUpdateStart(
             layoutCoordinates = layoutCoordinates,
@@ -99,17 +95,15 @@
         val beginPosition1 = Offset(30f, 20f)
         val dragDistance2 = Offset(100f, 300f)
         val beginPosition2 = Offset(300f, 200f)
-        gesture.onLongPress(beginPosition1)
-        gesture.onDragStart()
+        gesture.onStart(beginPosition1)
         gesture.onDrag(dragDistance1)
         // Setup. Cancel selection and reselect.
 //        selectionManager.onRelease()
         // Start the new selection
-        gesture.onLongPress(beginPosition2)
+        gesture.onStart(beginPosition2)
         selectionRegistrar.subselections = mapOf(selectableId to fakeSelection)
 
         // Act. Reset selectionManager.dragTotalDistance to zero.
-        gesture.onDragStart()
         gesture.onDrag(dragDistance2)
 
         // Verify.
@@ -125,13 +119,10 @@
     fun longPressDragObserver_onDrag_calls_notifySelectionDrag() {
         val dragDistance = Offset(15f, 10f)
         val beginPosition = Offset(30f, 20f)
-        gesture.onLongPress(beginPosition)
+        gesture.onStart(beginPosition)
         selectionRegistrar.subselections = mapOf(selectableId to fakeSelection)
-        gesture.onDragStart()
 
-        val result = gesture.onDrag(dragDistance)
-
-        assertThat(result).isEqualTo(dragDistance)
+        gesture.onDrag(dragDistance)
         verify(selectionRegistrar, times(1))
             .notifySelectionUpdate(
                 layoutCoordinates = layoutCoordinates,
@@ -142,12 +133,10 @@
 
     @Test
     fun longPressDragObserver_onStop_calls_notifySelectionEnd() {
-        val dragDistance = Offset(15f, 10f)
         val beginPosition = Offset(30f, 20f)
-        gesture.onLongPress(beginPosition)
+        gesture.onStart(beginPosition)
         selectionRegistrar.subselections = mapOf(selectableId to fakeSelection)
-        gesture.onDragStart()
-        gesture.onStop(dragDistance)
+        gesture.onStop()
 
         verify(selectionRegistrar, times(1))
             .notifySelectionUpdateEnd()
@@ -156,9 +145,8 @@
     @Test
     fun longPressDragObserver_onCancel_calls_notifySelectionEnd() {
         val beginPosition = Offset(30f, 20f)
-        gesture.onLongPress(beginPosition)
+        gesture.onStart(beginPosition)
         selectionRegistrar.subselections = mapOf(selectableId to fakeSelection)
-        gesture.onDragStart()
         gesture.onCancel()
 
         verify(selectionRegistrar, times(1))
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
index e89b190..07ae5d7 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
@@ -144,7 +144,7 @@
         val dragDistance = Offset(100f, 100f)
         selectionManager.handleDragObserver(isStartHandle = true).onStart(startOffset)
 
-        val result = selectionManager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
+        selectionManager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
 
         verify(containerLayoutCoordinates, times(1))
             .localPositionOf(
@@ -163,7 +163,6 @@
 
         assertThat(selection).isEqualTo(fakeResultSelection)
         verify(spyLambda, times(1)).invoke(fakeResultSelection)
-        assertThat(result).isEqualTo(dragDistance)
     }
 
     @Test
@@ -172,7 +171,7 @@
         val dragDistance = Offset(100f, 100f)
         selectionManager.handleDragObserver(isStartHandle = false).onStart(startOffset)
 
-        val result = selectionManager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
+        selectionManager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
 
         verify(containerLayoutCoordinates, times(1))
             .localPositionOf(
@@ -191,7 +190,6 @@
 
         assertThat(selection).isEqualTo(fakeResultSelection)
         verify(spyLambda, times(1)).invoke(fakeResultSelection)
-        assertThat(result).isEqualTo(dragDistance)
     }
 
     private fun getAdjustedCoordinates(position: Offset): Offset {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
index d5e37c6..77af370 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
@@ -212,7 +212,7 @@
         assertThat(selectionRegistrar.sorted).isTrue()
 
         // Act.
-        selectionRegistrar.notifyPositionChange()
+        selectionRegistrar.notifyPositionChange(handlerId0)
 
         // Assert.
         assertThat(selectionRegistrar.sorted).isFalse()
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index 4d87a55..63ae16a 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -147,7 +147,7 @@
     fun TextFieldSelectionManager_touchSelectionObserver_onLongPress() {
         whenever(layoutResultProxy.isPositionOnText(dragBeginPosition)).thenReturn(true)
 
-        manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+        manager.touchSelectionObserver.onStart(dragBeginPosition)
 
         assertThat(state.selectionIsOn).isTrue()
         assertThat(state.showFloatingToolbar).isTrue()
@@ -176,7 +176,7 @@
         whenever(layoutResultProxy.getLineEnd(fakeLineNumber)).thenReturn(fakeLineEnd)
 
         // Act
-        manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+        manager.touchSelectionObserver.onStart(dragBeginPosition)
 
         // Assert
         assertThat(state.selectionIsOn).isTrue()
@@ -195,7 +195,7 @@
 
     @Test
     fun TextFieldSelectionManager_touchSelectionObserver_onDrag() {
-        manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+        manager.touchSelectionObserver.onStart(dragBeginPosition)
         manager.touchSelectionObserver.onDrag(dragDistance)
 
         assertThat(value.selection).isEqualTo(TextRange(0, text.length))
@@ -208,10 +208,10 @@
 
     @Test
     fun TextFieldSelectionManager_touchSelectionObserver_onStop() {
-        manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+        manager.touchSelectionObserver.onStart(dragBeginPosition)
         manager.touchSelectionObserver.onDrag(dragDistance)
 
-        manager.touchSelectionObserver.onStop(Offset.Zero)
+        manager.touchSelectionObserver.onStop()
 
         assertThat(state.showFloatingToolbar).isTrue()
     }
@@ -246,9 +246,8 @@
     fun TextFieldSelectionManager_handleDragObserver_onDrag_startHandle() {
         manager.value = TextFieldValue(text = text, selection = TextRange(0, "Hello".length))
 
-        val result = manager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
+        manager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
 
-        assertThat(result).isEqualTo(dragDistance)
         assertThat(state.showFloatingToolbar).isFalse()
         assertThat(value.selection).isEqualTo(TextRange(dragOffset, "Hello".length))
         verify(
@@ -261,9 +260,8 @@
     fun TextFieldSelectionManager_handleDragObserver_onDrag_endHandle() {
         manager.value = TextFieldValue(text = text, selection = TextRange(0, "Hello".length))
 
-        val result = manager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
+        manager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
 
-        assertThat(result).isEqualTo(dragDistance)
         assertThat(state.showFloatingToolbar).isFalse()
         assertThat(value.selection).isEqualTo(TextRange(0, dragOffset))
         verify(
@@ -277,7 +275,7 @@
         manager.handleDragObserver(false).onStart(Offset.Zero)
         manager.handleDragObserver(false).onDrag(Offset.Zero)
 
-        manager.handleDragObserver(false).onStop(Offset.Zero)
+        manager.handleDragObserver(false).onStop()
 
         assertThat(state.draggingHandle).isFalse()
         assertThat(state.showFloatingToolbar).isTrue()
@@ -502,7 +500,7 @@
 
     @Test
     fun isTextChanged_text_changed_return_true() {
-        manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+        manager.touchSelectionObserver.onStart(dragBeginPosition)
         manager.value = TextFieldValue(text + text)
 
         assertThat(manager.isTextChanged()).isTrue()
@@ -510,7 +508,7 @@
 
     @Test
     fun isTextChanged_text_unchange_return_false() {
-        manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+        manager.touchSelectionObserver.onStart(dragBeginPosition)
 
         assertThat(manager.isTextChanged()).isFalse()
     }
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
index 9060916..4ba6f05 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
@@ -66,7 +66,6 @@
 import androidx.test.espresso.assertion.ViewAssertions.matches
 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 import androidx.test.espresso.matcher.ViewMatchers.withText
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -262,7 +261,6 @@
     }
 }
 
-@OptIn(ExperimentalCoroutinesApi::class)
 private object TestingSnippets13 {
     class MyTest() {
 
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
deleted file mode 100644
index 43a0f581..0000000
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.integration.macrobenchmark
-
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-/**
- * Macrobenchmark used for local validation of performance numbers coming from MacrobenchmarkRule.
- */
-@LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(Parameterized::class)
-class ProcessSpeedProfileValidation(
-    private val compilationMode: CompilationMode,
-    private val startupMode: StartupMode
-) {
-    @get:Rule
-    val benchmarkRule = MacrobenchmarkRule()
-
-    @Test
-    fun start() = benchmarkRule.measureRepeated(
-        packageName = PACKAGE_NAME,
-        metrics = listOf(StartupTimingMetric()),
-        compilationMode = compilationMode,
-        iterations = 3,
-        startupMode = startupMode
-    ) {
-        pressHome()
-        startActivityAndWait()
-    }
-
-    companion object {
-        private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
-
-        @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
-        @JvmStatic
-        fun kilProcessParameters(): List<Array<Any>> {
-            val compilationModes = listOf(
-                CompilationMode.None,
-                CompilationMode.SpeedProfile(warmupIterations = 3)
-            )
-            val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
-            return compilationModes.zip(processKillOptions).map {
-                arrayOf(it.first, it.second)
-            }
-        }
-    }
-}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
index 1a35bf5..56696de 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -16,6 +16,9 @@
 
 package androidx.compose.integration.macrobenchmark
 
+import androidx.benchmark.integration.macrobenchmark.createStartupCompilationParams
+import androidx.benchmark.integration.macrobenchmark.measureStartup
+import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 import androidx.test.filters.LargeTest
@@ -26,13 +29,16 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-class SmallListStartupBenchmark(private val startupMode: StartupMode) {
+class SmallListStartupBenchmark(
+    private val startupMode: StartupMode,
+    private val compilationMode: CompilationMode
+) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
     fun startup() = benchmarkRule.measureStartup(
-        profileCompiled = true,
+        compilationMode = compilationMode,
         startupMode = startupMode
     ) {
         action = "androidx.compose.integration.macrobenchmark.target.LAZY_COLUMN_ACTIVITY"
@@ -40,11 +46,8 @@
     }
 
     companion object {
-        @Parameterized.Parameters(name = "mode={0}")
+        @Parameterized.Parameters(name = "startup={0},compilation={1}")
         @JvmStatic
-        fun parameters(): List<Array<Any>> {
-            return listOf(StartupMode.COLD, StartupMode.WARM)
-                .map { arrayOf(it) }
-        }
+        fun parameters() = createStartupCompilationParams()
     }
 }
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
index dfbb035..5f26ee6 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
@@ -14,39 +14,67 @@
  * limitations under the License.
  */
 
-package androidx.compose.integration.macrobenchmark
+package androidx.benchmark.integration.macrobenchmark
 
 import android.content.Intent
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.isSupportedWithVmSettings
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 
-const val TargetPackage = "androidx.compose.integration.macrobenchmark.target"
+const val TARGET_PACKAGE = "androidx.benchmark.integration.compose.target"
 
-/**
- * Simplified interface for standardizing e.g. package,
- * compilation types, and iteration count across project
- */
 fun MacrobenchmarkRule.measureStartup(
-    profileCompiled: Boolean,
+    compilationMode: CompilationMode,
     startupMode: StartupMode,
-    iterations: Int = 5,
+    iterations: Int = 3,
     setupIntent: Intent.() -> Unit = {}
 ) = measureRepeated(
-    packageName = TargetPackage,
+    packageName = TARGET_PACKAGE,
     metrics = listOf(StartupTimingMetric()),
-    compilationMode = if (profileCompiled) {
-        CompilationMode.SpeedProfile(warmupIterations = 3)
-    } else {
-        CompilationMode.None
-    },
+    compilationMode = compilationMode,
     iterations = iterations,
     startupMode = startupMode
 ) {
     pressHome()
     val intent = Intent()
-    intent.setPackage(TargetPackage)
+    intent.setPackage(TARGET_PACKAGE)
     setupIntent(intent)
     startActivityAndWait(intent)
+}
+
+fun createStartupCompilationParams(
+    startupModes: List<StartupMode> = listOf(StartupMode.HOT, StartupMode.WARM, StartupMode.COLD),
+    compilationModes: List<CompilationMode> = listOf(
+        CompilationMode.None,
+        CompilationMode.Interpreted,
+        CompilationMode.SpeedProfile()
+    )
+): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+    for (startupMode in startupModes) {
+        for (compilationMode in compilationModes) {
+            // Skip configs that can't run, so they don't clutter Studio benchmark
+            // output with AssumptionViolatedException dumps
+            if (compilationMode.isSupportedWithVmSettings()) {
+                add(arrayOf(startupMode, compilationMode))
+            }
+        }
+    }
+}
+
+fun createCompilationParams(
+    compilationModes: List<CompilationMode> = listOf(
+        CompilationMode.None,
+        CompilationMode.Interpreted,
+        CompilationMode.SpeedProfile()
+    )
+): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+    for (compilationMode in compilationModes) {
+        // Skip configs that can't run, so they don't clutter Studio benchmark
+        // output with AssumptionViolatedException dumps
+        if (compilationMode.isSupportedWithVmSettings()) {
+            add(arrayOf(compilationMode))
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/FrameTimingMetricValidation.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
similarity index 88%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/FrameTimingMetricValidation.kt
rename to compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
index bf96acd..0e97a25 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/FrameTimingMetricValidation.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
@@ -18,6 +18,7 @@
 
 import android.content.Intent
 import android.graphics.Point
+import androidx.benchmark.integration.macrobenchmark.createCompilationParams
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.FrameTimingMetric
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -34,7 +35,9 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-class FrameTimingMetricValidation(private val compilationMode: CompilationMode) {
+class TrivialListScrollBenchmark(
+    private val compilationMode: CompilationMode
+) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
@@ -78,13 +81,8 @@
 
         private const val COMPOSE_IDLE = "COMPOSE-IDLE"
 
-        @Parameterized.Parameters(name = "compilation_mode={0}")
+        @Parameterized.Parameters(name = "compilation={0}")
         @JvmStatic
-        fun jankParameters(): List<Array<Any>> {
-            return listOf(
-                CompilationMode.None,
-                CompilationMode.SpeedProfile(warmupIterations = 3)
-            ).map { arrayOf(it) }
-        }
+        fun parameters() = createCompilationParams()
     }
 }
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
index 9ebb6ed..3b0e076 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -16,6 +16,9 @@
 
 package androidx.compose.integration.macrobenchmark
 
+import androidx.benchmark.integration.macrobenchmark.createStartupCompilationParams
+import androidx.benchmark.integration.macrobenchmark.measureStartup
+import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 import androidx.test.filters.LargeTest
@@ -26,24 +29,24 @@
 
 @LargeTest
 @RunWith(Parameterized::class)
-class TrivialStartupBenchmark(private val startupMode: StartupMode) {
+class TrivialStartupBenchmark(
+    private val startupMode: StartupMode,
+    private val compilationMode: CompilationMode
+) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
     fun startup() = benchmarkRule.measureStartup(
-        profileCompiled = true,
+        compilationMode = compilationMode,
         startupMode = startupMode
     ) {
         action = "androidx.compose.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY"
     }
 
     companion object {
-        @Parameterized.Parameters(name = "mode={0}")
+        @Parameterized.Parameters(name = "startup={0},compilation={1}")
         @JvmStatic
-        fun parameters(): List<Array<Any>> {
-            return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT)
-                .map { arrayOf(it) }
-        }
+        fun parameters() = createStartupCompilationParams()
     }
 }
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
index 0c50d08..c3bde4f 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
@@ -30,6 +30,7 @@
 
         val Composable = Name(PackageName, "Composable")
         val CompositionLocal = Name(PackageName, "CompositionLocal")
+        val MutableState = Name(PackageName, "MutableState")
         val MutableStateOf = Name(PackageName, "mutableStateOf")
         val MutableStateListOf = Name(PackageName, "mutableStateListOf")
         val MutableStateMapOf = Name(PackageName, "mutableStateMapOf")
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt
index ccb3a74..ddd3859 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt
@@ -19,6 +19,7 @@
 import com.intellij.psi.PsiJavaFile
 import com.intellij.psi.PsiMethod
 import com.intellij.psi.PsiType
+import com.intellij.psi.util.InheritanceUtil
 
 /**
  * Returns whether [this] has [packageName] as its package name.
@@ -33,3 +34,9 @@
  */
 val PsiMethod.returnsUnit
     get() = returnType == PsiType.VOID
+
+/**
+ * @return whether [this] inherits from [name]. Returns `true` if [this] _is_ directly [name].
+ */
+fun PsiType.inheritsFrom(name: Name) =
+    InheritanceUtil.isInheritor(this, name.javaFqn)
\ No newline at end of file
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
index 7e22dda..b583394 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
@@ -28,7 +28,6 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.impl.compiled.ClsMethodImpl
-import com.intellij.psi.util.InheritanceUtil
 import kotlinx.metadata.KmClassifier
 import org.jetbrains.kotlin.psi.KtForExpression
 import org.jetbrains.kotlin.psi.KtNamedFunction
@@ -54,7 +53,7 @@
             // Type of the variable we are iterating on, i.e the type of `b` in `for (a in b)`
             val iteratedValueType = node.iteratedValue.getExpressionType()
             // We are iterating on a List
-            if (InheritanceUtil.isInheritor(iteratedValueType, JavaList.javaFqn)) {
+            if (iteratedValueType?.inheritsFrom(JavaList) == true) {
                 // Find the `in` keyword to use as location
                 val inKeyword = (node.sourcePsi as? KtForExpression)?.inKeyword
                 val location = if (inKeyword == null) {
@@ -75,9 +74,7 @@
             val receiverType = node.receiverType
 
             // We are calling a method on a `List` type
-            if (receiverType != null &&
-                InheritanceUtil.isInheritor(node.receiverType, JavaList.javaFqn)
-            ) {
+            if (receiverType?.inheritsFrom(JavaList) == true) {
                 when (val method = node.resolveToUElement()?.sourcePsi) {
                     // Parsing a class file
                     is ClsMethodImpl -> {
diff --git a/compose/material/material/api/1.0.0-beta04.txt b/compose/material/material/api/1.0.0-beta04.txt
index c630e0c..7cedeb6 100644
--- a/compose/material/material/api/1.0.0-beta04.txt
+++ b/compose/material/material/api/1.0.0-beta04.txt
@@ -47,38 +47,6 @@
   public final class BackdropScaffoldKt {
   }
 
-  public final class BottomDrawerState {
-    ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
-    method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final T! getCurrentValue();
-    method public final float getDirection();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
-    method public final T! getTargetValue();
-    method public final boolean isAnimationRunning();
-    method public boolean isClosed();
-    method public boolean isExpanded();
-    method public boolean isOpen();
-    method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final float performDrag(float delta);
-    method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    property public final boolean isClosed;
-    property public final boolean isExpanded;
-    property public final boolean isOpen;
-    field public static final androidx.compose.material.BottomDrawerState.Companion Companion;
-  }
-
-  public static final class BottomDrawerState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
-  }
-
-  public enum BottomDrawerValue {
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
-  }
-
   public final class BottomNavigationDefaults {
     method public float getElevation-D9Ej5fM();
     property public final float Elevation;
@@ -243,24 +211,19 @@
 
   public final class DrawerKt {
     method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
   }
 
   @androidx.compose.runtime.Stable public final class DrawerState {
     ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final T! getCurrentValue();
-    method public final float getDirection();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
-    method public final T! getTargetValue();
-    method public final boolean isAnimationRunning();
+    method public androidx.compose.material.DrawerValue getCurrentValue();
+    method public boolean isAnimationRunning();
     method public boolean isClosed();
     method public boolean isOpen();
     method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final float performDrag(float delta);
-    method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final androidx.compose.material.DrawerValue currentValue;
+    property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
     field public static final androidx.compose.material.DrawerState.Companion Companion;
diff --git a/compose/material/material/api/current.ignore b/compose/material/material/api/current.ignore
new file mode 100644
index 0000000..f5556ca
--- /dev/null
+++ b/compose/material/material/api/current.ignore
@@ -0,0 +1,25 @@
+// Baseline format: 1.0
+ChangedType: androidx.compose.material.DrawerState#getCurrentValue():
+    Method androidx.compose.material.DrawerState.getCurrentValue has changed return type from T to androidx.compose.material.DrawerValue
+
+
+RemovedClass: androidx.compose.material.BottomDrawerState:
+    Removed class androidx.compose.material.BottomDrawerState
+RemovedClass: androidx.compose.material.BottomDrawerValue:
+    Removed class androidx.compose.material.BottomDrawerValue
+
+
+RemovedMethod: androidx.compose.material.DrawerKt#rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue, kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean>):
+    Removed method androidx.compose.material.DrawerKt.rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue,kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean>)
+RemovedMethod: androidx.compose.material.DrawerState#getDirection():
+    Removed method androidx.compose.material.DrawerState.getDirection()
+RemovedMethod: androidx.compose.material.DrawerState#getOffset():
+    Removed method androidx.compose.material.DrawerState.getOffset()
+RemovedMethod: androidx.compose.material.DrawerState#getOverflow():
+    Removed method androidx.compose.material.DrawerState.getOverflow()
+RemovedMethod: androidx.compose.material.DrawerState#getTargetValue():
+    Removed method androidx.compose.material.DrawerState.getTargetValue()
+RemovedMethod: androidx.compose.material.DrawerState#performDrag(float):
+    Removed method androidx.compose.material.DrawerState.performDrag(float)
+RemovedMethod: androidx.compose.material.DrawerState#performFling(float, kotlin.coroutines.Continuation<? super kotlin.Unit>):
+    Removed method androidx.compose.material.DrawerState.performFling(float,kotlin.coroutines.Continuation<? super kotlin.Unit>)
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index c630e0c..7cedeb6 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -47,38 +47,6 @@
   public final class BackdropScaffoldKt {
   }
 
-  public final class BottomDrawerState {
-    ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
-    method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final T! getCurrentValue();
-    method public final float getDirection();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
-    method public final T! getTargetValue();
-    method public final boolean isAnimationRunning();
-    method public boolean isClosed();
-    method public boolean isExpanded();
-    method public boolean isOpen();
-    method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final float performDrag(float delta);
-    method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    property public final boolean isClosed;
-    property public final boolean isExpanded;
-    property public final boolean isOpen;
-    field public static final androidx.compose.material.BottomDrawerState.Companion Companion;
-  }
-
-  public static final class BottomDrawerState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
-  }
-
-  public enum BottomDrawerValue {
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
-  }
-
   public final class BottomNavigationDefaults {
     method public float getElevation-D9Ej5fM();
     property public final float Elevation;
@@ -243,24 +211,19 @@
 
   public final class DrawerKt {
     method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
   }
 
   @androidx.compose.runtime.Stable public final class DrawerState {
     ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final T! getCurrentValue();
-    method public final float getDirection();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
-    method public final T! getTargetValue();
-    method public final boolean isAnimationRunning();
+    method public androidx.compose.material.DrawerValue getCurrentValue();
+    method public boolean isAnimationRunning();
     method public boolean isClosed();
     method public boolean isOpen();
     method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final float performDrag(float delta);
-    method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final androidx.compose.material.DrawerValue currentValue;
+    property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
     field public static final androidx.compose.material.DrawerState.Companion Companion;
diff --git a/compose/material/material/api/public_plus_experimental_1.0.0-beta04.txt b/compose/material/material/api/public_plus_experimental_1.0.0-beta04.txt
index 2795abb..ab08c2c 100644
--- a/compose/material/material/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/material/material/api/public_plus_experimental_1.0.0-beta04.txt
@@ -71,7 +71,7 @@
     enum_constant public static final androidx.compose.material.BackdropValue Revealed;
   }
 
-  public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
+  @androidx.compose.material.ExperimentalMaterialApi public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
     ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
@@ -89,7 +89,7 @@
     method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
   }
 
-  public enum BottomDrawerValue {
+  @androidx.compose.material.ExperimentalMaterialApi public enum BottomDrawerValue {
     enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
     enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
     enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
@@ -307,18 +307,28 @@
   public final class DrawerKt {
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomDrawer--6CoO6E(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomDrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
   }
 
-  @androidx.compose.runtime.Stable public final class DrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.DrawerValue> {
+  @androidx.compose.runtime.Stable public final class DrawerState {
     ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
+    method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? animateTo(androidx.compose.material.DrawerValue targetValue, androidx.compose.animation.core.AnimationSpec<java.lang.Float> anim, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public androidx.compose.material.DrawerValue getCurrentValue();
+    method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.runtime.State<java.lang.Float> getOffset();
+    method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.material.DrawerValue getTargetValue();
+    method public boolean isAnimationRunning();
     method public boolean isClosed();
     method public boolean isOpen();
     method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? snapTo(androidx.compose.material.DrawerValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final androidx.compose.material.DrawerValue currentValue;
+    property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
+    property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.runtime.State<java.lang.Float> offset;
+    property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
     field public static final androidx.compose.material.DrawerState.Companion Companion;
   }
 
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 2795abb..ab08c2c 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -71,7 +71,7 @@
     enum_constant public static final androidx.compose.material.BackdropValue Revealed;
   }
 
-  public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
+  @androidx.compose.material.ExperimentalMaterialApi public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
     ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
@@ -89,7 +89,7 @@
     method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
   }
 
-  public enum BottomDrawerValue {
+  @androidx.compose.material.ExperimentalMaterialApi public enum BottomDrawerValue {
     enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
     enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
     enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
@@ -307,18 +307,28 @@
   public final class DrawerKt {
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomDrawer--6CoO6E(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomDrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
   }
 
-  @androidx.compose.runtime.Stable public final class DrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.DrawerValue> {
+  @androidx.compose.runtime.Stable public final class DrawerState {
     ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
+    method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? animateTo(androidx.compose.material.DrawerValue targetValue, androidx.compose.animation.core.AnimationSpec<java.lang.Float> anim, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public androidx.compose.material.DrawerValue getCurrentValue();
+    method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.runtime.State<java.lang.Float> getOffset();
+    method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.material.DrawerValue getTargetValue();
+    method public boolean isAnimationRunning();
     method public boolean isClosed();
     method public boolean isOpen();
     method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? snapTo(androidx.compose.material.DrawerValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final androidx.compose.material.DrawerValue currentValue;
+    property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
+    property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.runtime.State<java.lang.Float> offset;
+    property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
     field public static final androidx.compose.material.DrawerState.Companion Companion;
   }
 
diff --git a/compose/material/material/api/restricted_1.0.0-beta04.txt b/compose/material/material/api/restricted_1.0.0-beta04.txt
index c630e0c..7cedeb6 100644
--- a/compose/material/material/api/restricted_1.0.0-beta04.txt
+++ b/compose/material/material/api/restricted_1.0.0-beta04.txt
@@ -47,38 +47,6 @@
   public final class BackdropScaffoldKt {
   }
 
-  public final class BottomDrawerState {
-    ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
-    method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final T! getCurrentValue();
-    method public final float getDirection();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
-    method public final T! getTargetValue();
-    method public final boolean isAnimationRunning();
-    method public boolean isClosed();
-    method public boolean isExpanded();
-    method public boolean isOpen();
-    method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final float performDrag(float delta);
-    method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    property public final boolean isClosed;
-    property public final boolean isExpanded;
-    property public final boolean isOpen;
-    field public static final androidx.compose.material.BottomDrawerState.Companion Companion;
-  }
-
-  public static final class BottomDrawerState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
-  }
-
-  public enum BottomDrawerValue {
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
-  }
-
   public final class BottomNavigationDefaults {
     method public float getElevation-D9Ej5fM();
     property public final float Elevation;
@@ -243,24 +211,19 @@
 
   public final class DrawerKt {
     method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
   }
 
   @androidx.compose.runtime.Stable public final class DrawerState {
     ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final T! getCurrentValue();
-    method public final float getDirection();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
-    method public final T! getTargetValue();
-    method public final boolean isAnimationRunning();
+    method public androidx.compose.material.DrawerValue getCurrentValue();
+    method public boolean isAnimationRunning();
     method public boolean isClosed();
     method public boolean isOpen();
     method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final float performDrag(float delta);
-    method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final androidx.compose.material.DrawerValue currentValue;
+    property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
     field public static final androidx.compose.material.DrawerState.Companion Companion;
diff --git a/compose/material/material/api/restricted_current.ignore b/compose/material/material/api/restricted_current.ignore
new file mode 100644
index 0000000..f5556ca
--- /dev/null
+++ b/compose/material/material/api/restricted_current.ignore
@@ -0,0 +1,25 @@
+// Baseline format: 1.0
+ChangedType: androidx.compose.material.DrawerState#getCurrentValue():
+    Method androidx.compose.material.DrawerState.getCurrentValue has changed return type from T to androidx.compose.material.DrawerValue
+
+
+RemovedClass: androidx.compose.material.BottomDrawerState:
+    Removed class androidx.compose.material.BottomDrawerState
+RemovedClass: androidx.compose.material.BottomDrawerValue:
+    Removed class androidx.compose.material.BottomDrawerValue
+
+
+RemovedMethod: androidx.compose.material.DrawerKt#rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue, kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean>):
+    Removed method androidx.compose.material.DrawerKt.rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue,kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean>)
+RemovedMethod: androidx.compose.material.DrawerState#getDirection():
+    Removed method androidx.compose.material.DrawerState.getDirection()
+RemovedMethod: androidx.compose.material.DrawerState#getOffset():
+    Removed method androidx.compose.material.DrawerState.getOffset()
+RemovedMethod: androidx.compose.material.DrawerState#getOverflow():
+    Removed method androidx.compose.material.DrawerState.getOverflow()
+RemovedMethod: androidx.compose.material.DrawerState#getTargetValue():
+    Removed method androidx.compose.material.DrawerState.getTargetValue()
+RemovedMethod: androidx.compose.material.DrawerState#performDrag(float):
+    Removed method androidx.compose.material.DrawerState.performDrag(float)
+RemovedMethod: androidx.compose.material.DrawerState#performFling(float, kotlin.coroutines.Continuation<? super kotlin.Unit>):
+    Removed method androidx.compose.material.DrawerState.performFling(float,kotlin.coroutines.Continuation<? super kotlin.Unit>)
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index c630e0c..7cedeb6 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -47,38 +47,6 @@
   public final class BackdropScaffoldKt {
   }
 
-  public final class BottomDrawerState {
-    ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
-    method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final T! getCurrentValue();
-    method public final float getDirection();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
-    method public final T! getTargetValue();
-    method public final boolean isAnimationRunning();
-    method public boolean isClosed();
-    method public boolean isExpanded();
-    method public boolean isOpen();
-    method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final float performDrag(float delta);
-    method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    property public final boolean isClosed;
-    property public final boolean isExpanded;
-    property public final boolean isOpen;
-    field public static final androidx.compose.material.BottomDrawerState.Companion Companion;
-  }
-
-  public static final class BottomDrawerState.Companion {
-    method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
-  }
-
-  public enum BottomDrawerValue {
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
-    enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
-  }
-
   public final class BottomNavigationDefaults {
     method public float getElevation-D9Ej5fM();
     property public final float Elevation;
@@ -243,24 +211,19 @@
 
   public final class DrawerKt {
     method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
   }
 
   @androidx.compose.runtime.Stable public final class DrawerState {
     ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final T! getCurrentValue();
-    method public final float getDirection();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
-    method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
-    method public final T! getTargetValue();
-    method public final boolean isAnimationRunning();
+    method public androidx.compose.material.DrawerValue getCurrentValue();
+    method public boolean isAnimationRunning();
     method public boolean isClosed();
     method public boolean isOpen();
     method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
-    method public final float performDrag(float delta);
-    method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    property public final androidx.compose.material.DrawerValue currentValue;
+    property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
     field public static final androidx.compose.material.DrawerState.Companion Companion;
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt
index d9d9983..c474e24 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt
@@ -58,7 +58,6 @@
         // Kotlin 1.4) each property with the same name will be considered as a possible candidate
         // for resolution, regardless of the access modifier, so by using unique names we reduce
         // the size from ~6000 to 1, and speed up compilation time for these icons.
-        @OptIn(ExperimentalStdlibApi::class)
         val backingPropertyName = "_" + iconName.decapitalize(Locale.ROOT)
         val backingProperty = backingProperty(name = backingPropertyName)
         return FileSpec.builder(
diff --git a/compose/material/material/integration-tests/material-catalog/build.gradle b/compose/material/material/integration-tests/material-catalog/build.gradle
index 3cf2268..3b1ed12 100644
--- a/compose/material/material/integration-tests/material-catalog/build.gradle
+++ b/compose/material/material/integration-tests/material-catalog/build.gradle
@@ -28,20 +28,17 @@
     kotlinPlugin project(":compose:compiler:compiler")
     implementation(KOTLIN_STDLIB)
 
-    implementation project(":compose:foundation:foundation")
-    implementation project(":compose:foundation:foundation-layout")
     implementation project(":compose:integration-tests:demos:common")
+    implementation project(":compose:ui:ui")
     implementation project(":compose:material:material")
     implementation project(":compose:material:material:material-samples")
-    implementation project(":compose:runtime:runtime")
-    implementation project(":compose:ui:ui")
-    implementation project(":compose:ui:ui-text")
     implementation project(":activity:activity-compose")
+    implementation project(":navigation:navigation-compose")
 }
 
 androidx {
-    name = "Compose Material Catalog"
+    name = "Compose Material catalog"
     publish = Publish.NONE
     inceptionYear = "2021"
-    description = "This is a project for a Material catalog."
+    description = "This is a project for a Compose Material catalog."
 }
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/AndroidManifest.xml b/compose/material/material/integration-tests/material-catalog/src/main/AndroidManifest.xml
index 40c6046..d6ee456 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/AndroidManifest.xml
+++ b/compose/material/material/integration-tests/material-catalog/src/main/AndroidManifest.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   ~ Copyright (C) 2021 The Android Open Source Project
   ~
@@ -20,9 +21,8 @@
 
     <application>
         <activity android:name=".CatalogActivity"
-            android:theme="@android:style/Theme.NoTitleBar"
-            android:configChanges="orientation|screenSize"
-            android:label="Catalog">
+            android:theme="@style/Theme.Catalog"
+            android:label="@string/app_name">
         </activity>
     </application>
 </manifest>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogActivity.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogActivity.kt
index 55c1ac1..580b0f5 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogActivity.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogActivity.kt
@@ -19,38 +19,14 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
+import androidx.core.view.WindowCompat
 
 class CatalogActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+        WindowCompat.setDecorFitsSystemWindows(window, false)
         setContent {
             CatalogApp()
         }
     }
 }
-
-@Composable
-fun CatalogApp() {
-    val colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
-    MaterialTheme(colors = colors) {
-        Surface(modifier = Modifier.fillMaxSize()) {
-            Box {
-                Text(
-                    text = "Nothing to see here!",
-                    modifier = Modifier.align(Alignment.Center)
-                )
-            }
-        }
-    }
-}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogApp.kt
similarity index 69%
copy from compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
copy to compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogApp.kt
index 5ece3c2..503ec64 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogApp.kt
@@ -16,6 +16,15 @@
 
 package androidx.compose.material.catalog
 
-import androidx.compose.integration.demos.common.ActivityDemo
+import androidx.compose.material.catalog.insets.ProvideWindowInsets
+import androidx.compose.material.catalog.ui.theme.CatalogTheme
+import androidx.compose.runtime.Composable
 
-val MaterialCatalog = ActivityDemo("Material Catalog", CatalogActivity::class)
+@Composable
+fun CatalogApp() {
+    ProvideWindowInsets {
+        CatalogTheme {
+            NavGraph()
+        }
+    }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogDemo.kt
similarity index 91%
rename from compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
rename to compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogDemo.kt
index 5ece3c2..febf878 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogDemo.kt
@@ -18,4 +18,4 @@
 
 import androidx.compose.integration.demos.common.ActivityDemo
 
-val MaterialCatalog = ActivityDemo("Material Catalog", CatalogActivity::class)
+val MaterialCatalog = ActivityDemo("Material catalog", CatalogActivity::class)
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/NavGraph.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/NavGraph.kt
new file mode 100644
index 0000000..62d0076
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/NavGraph.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog
+
+import androidx.compose.material.catalog.model.Components
+import androidx.compose.material.catalog.ui.component.Component
+import androidx.compose.material.catalog.ui.example.Example
+import androidx.compose.material.catalog.ui.home.Home
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navArgument
+import androidx.navigation.compose.navigate
+import androidx.navigation.compose.rememberNavController
+
+@Composable
+fun NavGraph() {
+    val navController = rememberNavController()
+    NavHost(
+        navController = navController,
+        startDestination = HOME_ROUTE
+    ) {
+        composable(HOME_ROUTE) {
+            Home(
+                components = Components,
+                onComponentClick = { component ->
+                    val componentId = component.id
+                    val route = "$COMPONENT_ROUTE/$componentId"
+                    navController.navigate(route)
+                }
+            )
+        }
+        composable(
+            route = "$COMPONENT_ROUTE/" +
+                "{$COMPONENT_ID_ARG_NAME}",
+            arguments = listOf(
+                navArgument(COMPONENT_ID_ARG_NAME) { type = NavType.IntType }
+            )
+        ) { navBackStackEntry ->
+            val arguments = requireNotNull(navBackStackEntry.arguments) { "No arguments" }
+            val componentId = arguments.getInt(COMPONENT_ID_ARG_NAME)
+            val component = Components.first { component -> component.id == componentId }
+            Component(
+                component = component,
+                onExampleClick = { example ->
+                    val exampleIndex = component.examples.indexOf(example)
+                    val route = "$EXAMPLE_ROUTE/$componentId/$exampleIndex"
+                    navController.navigate(route)
+                },
+                onBackClick = { navController.popBackStack() }
+            )
+        }
+        composable(
+            route = "$EXAMPLE_ROUTE/" +
+                "{$COMPONENT_ID_ARG_NAME}/" +
+                "{$EXAMPLE_INDEX_ARG_NAME}",
+            arguments = listOf(
+                navArgument(COMPONENT_ID_ARG_NAME) { type = NavType.IntType },
+                navArgument(EXAMPLE_INDEX_ARG_NAME) { type = NavType.IntType }
+            )
+        ) { navBackStackEntry ->
+            val arguments = requireNotNull(navBackStackEntry.arguments) { "No arguments" }
+            val componentId = arguments.getInt(COMPONENT_ID_ARG_NAME)
+            val exampleIndex = arguments.getInt(EXAMPLE_INDEX_ARG_NAME)
+            val component = Components.first { component -> component.id == componentId }
+            val example = component.examples[exampleIndex]
+            Example(
+                example = example,
+                onBackClick = { navController.popBackStack() }
+            )
+        }
+    }
+}
+
+private const val HOME_ROUTE = "home"
+private const val COMPONENT_ROUTE = "component"
+private const val EXAMPLE_ROUTE = "example"
+private const val COMPONENT_ID_ARG_NAME = "componentId"
+private const val EXAMPLE_INDEX_ARG_NAME = "exampleIndex"
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Insets.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Insets.kt
new file mode 100644
index 0000000..cd8110f
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Insets.kt
@@ -0,0 +1,644 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * TODO: Move to depending on Accompanist with prebuilts when we hit a stable version
+ * https://github.com/google/accompanist/blob/main/insets/src/main/java/com/google/accompanist
+ * /insets/Insets.kt
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "unused", "PropertyName")
+
+@file:JvmName("ComposeInsets")
+@file:JvmMultifileClass
+
+package androidx.compose.material.catalog.insets
+
+import android.annotation.SuppressLint
+import android.view.View
+import android.view.WindowInsetsAnimation
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
+
+/**
+ * Main holder of our inset values.
+ */
+@Stable
+class WindowInsets {
+    /**
+     * Inset values which match [WindowInsetsCompat.Type.systemBars]
+     */
+    val systemBars: InsetsType = InsetsType()
+
+    /**
+     * Inset values which match [WindowInsetsCompat.Type.systemGestures]
+     */
+    val systemGestures: InsetsType = InsetsType()
+
+    /**
+     * Inset values which match [WindowInsetsCompat.Type.navigationBars]
+     */
+    val navigationBars: InsetsType = InsetsType()
+
+    /**
+     * Inset values which match [WindowInsetsCompat.Type.statusBars]
+     */
+    val statusBars: InsetsType = InsetsType()
+
+    /**
+     * Inset values which match [WindowInsetsCompat.Type.ime]
+     */
+    val ime: InsetsType = InsetsType()
+}
+
+/**
+ * Represents the values for a type of insets, and stores information about the layout insets,
+ * animating insets, and visibility of the insets.
+ *
+ * [InsetsType] instances are commonly stored in a [WindowInsets] instance.
+ */
+@Stable
+@Suppress("MemberVisibilityCanBePrivate")
+class InsetsType : Insets {
+    private var ongoingAnimationsCount by mutableStateOf(0)
+    internal val _layoutInsets = MutableInsets()
+    internal val _animatedInsets = MutableInsets()
+
+    /**
+     * The layout insets for this [InsetsType]. These are the insets which are defined from the
+     * current window layout.
+     *
+     * You should not normally need to use this directly, and instead use [left], [top],
+     * [right], and [bottom] to return the correct value for the current state.
+     */
+    val layoutInsets: Insets
+        get() = _layoutInsets
+
+    /**
+     * The animated insets for this [InsetsType]. These are the insets which are updated from
+     * any on-going animations. If there are no animations in progress, the returned [Insets] will
+     * be empty.
+     *
+     * You should not normally need to use this directly, and instead use [left], [top],
+     * [right], and [bottom] to return the correct value for the current state.
+     */
+    val animatedInsets: Insets
+        get() = _animatedInsets
+
+    /**
+     * The left dimension of the insets in pixels.
+     */
+    override val left: Int
+        get() = (if (animationInProgress) animatedInsets else layoutInsets).left
+
+    /**
+     * The top dimension of the insets in pixels.
+     */
+    override val top: Int
+        get() = (if (animationInProgress) animatedInsets else layoutInsets).top
+
+    /**
+     * The right dimension of the insets in pixels.
+     */
+    override val right: Int
+        get() = (if (animationInProgress) animatedInsets else layoutInsets).right
+
+    /**
+     * The bottom dimension of the insets in pixels.
+     */
+    override val bottom: Int
+        get() = (if (animationInProgress) animatedInsets else layoutInsets).bottom
+
+    /**
+     * Whether the insets are currently visible.
+     */
+    var isVisible by mutableStateOf(true)
+        internal set
+
+    /**
+     * Whether this insets type is being animated at this moment.
+     */
+    val animationInProgress: Boolean
+        get() = ongoingAnimationsCount > 0
+
+    /**
+     * The progress of any ongoing animations, in the range of 0 to 1.
+     * If there is no animation in progress, this will return 0.
+     */
+    @get:FloatRange(from = 0.0, to = 1.0)
+    var animationFraction by mutableStateOf(0f)
+        internal set
+
+    internal fun onAnimationStart() {
+        ongoingAnimationsCount++
+    }
+
+    internal fun onAnimationEnd() {
+        ongoingAnimationsCount--
+
+        if (ongoingAnimationsCount == 0) {
+            // If there are no on-going animations, clear out the animated insets
+            _animatedInsets.reset()
+            animationFraction = 0f
+        }
+    }
+}
+
+@Stable
+interface Insets {
+    /**
+     * The left dimension of these insets in pixels.
+     */
+    @get:IntRange(from = 0)
+    val left: Int
+
+    /**
+     * The top dimension of these insets in pixels.
+     */
+    @get:IntRange(from = 0)
+    val top: Int
+
+    /**
+     * The right dimension of these insets in pixels.
+     */
+    @get:IntRange(from = 0)
+    val right: Int
+
+    /**
+     * The bottom dimension of these insets in pixels.
+     */
+    @get:IntRange(from = 0)
+    val bottom: Int
+
+    fun copy(
+        left: Int = this.left,
+        top: Int = this.top,
+        right: Int = this.right,
+        bottom: Int = this.bottom,
+    ): Insets = MutableInsets(left, top, right, bottom)
+
+    operator fun minus(other: Insets): Insets = copy(
+        left = this.left - other.left,
+        top = this.top - other.top,
+        right = this.right - other.right,
+        bottom = this.bottom - other.bottom,
+    )
+
+    operator fun plus(other: Insets): Insets = copy(
+        left = this.left + other.left,
+        top = this.top + other.top,
+        right = this.right + other.right,
+        bottom = this.bottom + other.bottom,
+    )
+}
+
+internal class MutableInsets(
+    left: Int = 0,
+    top: Int = 0,
+    right: Int = 0,
+    bottom: Int = 0,
+) : Insets {
+    override var left by mutableStateOf(left)
+        internal set
+
+    override var top by mutableStateOf(top)
+        internal set
+
+    override var right by mutableStateOf(right)
+        internal set
+
+    override var bottom by mutableStateOf(bottom)
+        internal set
+
+    fun reset() {
+        left = 0
+        top = 0
+        right = 0
+        bottom = 0
+    }
+}
+
+/**
+ * Composition local containing the current [WindowInsets].
+ */
+val LocalWindowInsets = staticCompositionLocalOf { WindowInsets() }
+
+/**
+ * This class sets up the necessary listeners on the given [view] to be able to observe
+ * [WindowInsetsCompat] instances dispatched by the system.
+ *
+ * This class is useful for when you prefer to handle the ownership of the [WindowInsets]
+ * yourself. One example of this is if you find yourself using [ProvideWindowInsets] in fragments.
+ *
+ * It is convenient to use [ProvideWindowInsets] in fragments, but that can result in a
+ * delay in the initial inset update, which results in a visual flicker.
+ * See [this issue](https://github.com/google/accompanist/issues/155) for more information.
+ *
+ * The alternative is for fragments to manage the [WindowInsets] themselves, like so:
+ *
+ * ```
+ * override fun onCreateView(
+ *     inflater: LayoutInflater,
+ *     container: ViewGroup?,
+ *     savedInstanceState: Bundle?
+ * ): View = ComposeView(requireContext()).apply {
+ *     layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ *
+ *     // Create an ViewWindowInsetObserver using this view
+ *     val observer = ViewWindowInsetObserver(this)
+ *
+ *     // Call start() to start listening now.
+ *     // The WindowInsets instance is returned to us.
+ *     val windowInsets = observer.start()
+ *
+ *     setContent {
+ *         // Instead of calling ProvideWindowInsets, we use CompositionLocalProvider to provide
+ *         // the WindowInsets instance from above to LocalWindowInsets
+ *         CompositionLocalProvider(LocalWindowInsets provides windowInsets) {
+ *             /* Content */
+ *         }
+ *     }
+ * }
+ * ```
+ *
+ * @param view The view to observe [WindowInsetsCompat]s from.
+ */
+class ViewWindowInsetObserver(private val view: View) {
+    private val attachListener = object : View.OnAttachStateChangeListener {
+        override fun onViewAttachedToWindow(v: View) = v.requestApplyInsets()
+        override fun onViewDetachedFromWindow(v: View) = Unit
+    }
+
+    /**
+     * Whether this [ViewWindowInsetObserver] is currently observing.
+     */
+    @Suppress("MemberVisibilityCanBePrivate")
+    var isObserving: Boolean = false
+        private set
+
+    /**
+     * Start observing window insets from [view]. Make sure to call [stop] if required.
+     *
+     * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are
+     * dispatched to the host view. Defaults to `true`.
+     */
+    fun start(
+        consumeWindowInsets: Boolean = true
+    ): WindowInsets {
+        return WindowInsets().apply {
+            observeInto(
+                windowInsets = this,
+                consumeWindowInsets = consumeWindowInsets,
+                windowInsetsAnimationsEnabled = false
+            )
+        }
+    }
+
+    /**
+     * Start observing window insets from [view]. Make sure to call [stop] if required.
+     *
+     * @param windowInsetsAnimationsEnabled Whether to listen for [WindowInsetsAnimation]s, such as
+     * IME animations.
+     * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are
+     * dispatched to the host view. Defaults to `true`.
+     */
+    @ExperimentalAnimatedInsets
+    fun start(
+        windowInsetsAnimationsEnabled: Boolean,
+        consumeWindowInsets: Boolean = true,
+    ): WindowInsets {
+        return WindowInsets().apply {
+            observeInto(
+                windowInsets = this,
+                consumeWindowInsets = consumeWindowInsets,
+                windowInsetsAnimationsEnabled = windowInsetsAnimationsEnabled
+            )
+        }
+    }
+
+    internal fun observeInto(
+        windowInsets: WindowInsets,
+        consumeWindowInsets: Boolean,
+        windowInsetsAnimationsEnabled: Boolean,
+    ) {
+        require(!isObserving) {
+            "start() called, but this ViewWindowInsetObserver is already observing"
+        }
+
+        ViewCompat.setOnApplyWindowInsetsListener(view) { _, wic ->
+            // Go through each inset type and update its layoutInsets from the
+            // WindowInsetsCompat values
+            windowInsets.statusBars.run {
+                _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.statusBars()))
+                isVisible = wic.isVisible(WindowInsetsCompat.Type.statusBars())
+            }
+            windowInsets.navigationBars.run {
+                _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.navigationBars()))
+                isVisible = wic.isVisible(WindowInsetsCompat.Type.navigationBars())
+            }
+            windowInsets.systemBars.run {
+                _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.systemBars()))
+                isVisible = wic.isVisible(WindowInsetsCompat.Type.systemBars())
+            }
+            windowInsets.systemGestures.run {
+                _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.systemGestures()))
+                isVisible = wic.isVisible(WindowInsetsCompat.Type.systemGestures())
+            }
+            windowInsets.ime.run {
+                _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.ime()))
+                isVisible = wic.isVisible(WindowInsetsCompat.Type.ime())
+            }
+
+            if (consumeWindowInsets) WindowInsetsCompat.CONSUMED else wic
+        }
+
+        // Add an OnAttachStateChangeListener to request an inset pass each time we're attached
+        // to the window
+        val attachListener = object : View.OnAttachStateChangeListener {
+            override fun onViewAttachedToWindow(v: View) = v.requestApplyInsets()
+            override fun onViewDetachedFromWindow(v: View) = Unit
+        }
+        view.addOnAttachStateChangeListener(attachListener)
+
+        if (windowInsetsAnimationsEnabled) {
+            ViewCompat.setWindowInsetsAnimationCallback(
+                view,
+                InnerWindowInsetsAnimationCallback(windowInsets)
+            )
+        } else {
+            ViewCompat.setWindowInsetsAnimationCallback(view, null)
+        }
+
+        if (view.isAttachedToWindow) {
+            // If the view is already attached, we can request an inset pass now
+            view.requestApplyInsets()
+        }
+
+        isObserving = true
+    }
+
+    /**
+     * Removes any listeners from the [view] so that we no longer observe inset changes.
+     *
+     * This is only required to be called from hosts which have a shorter lifetime than the [view].
+     * For example, if you're using [ViewWindowInsetObserver] from a `@Composable` function,
+     * you should call [stop] from an `onDispose` block, like so:
+     *
+     * ```
+     * DisposableEffect(view) {
+     *     val observer = ViewWindowInsetObserver(view)
+     *     // ...
+     *     onDispose {
+     *         observer.stop()
+     *     }
+     * }
+     * ```
+     *
+     * Whereas if you're using this class from a fragment (or similar), it is not required to
+     * call this function since it will live as least as longer as the view.
+     */
+    fun stop() {
+        require(isObserving) {
+            "stop() called, but this ViewWindowInsetObserver is not currently observing"
+        }
+        view.removeOnAttachStateChangeListener(attachListener)
+        ViewCompat.setOnApplyWindowInsetsListener(view, null)
+        isObserving = false
+    }
+}
+
+/**
+ * Applies any [WindowInsetsCompat] values to [LocalWindowInsets], which are then available
+ * within [content].
+ *
+ * If you're using this in fragments, you may wish to take a look at
+ * [ViewWindowInsetObserver] for a more optimal solution.
+ *
+ * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are dispatched to
+ * the host view. Defaults to `true`.
+ */
+@SuppressLint("UnnecessaryLambdaCreation")
+@Composable
+fun ProvideWindowInsets(
+    consumeWindowInsets: Boolean = true,
+    content: @Composable () -> Unit,
+) {
+    val view = LocalView.current
+    val windowInsets = LocalWindowInsets.current
+
+    DisposableEffect(view) {
+        val observer = ViewWindowInsetObserver(view)
+        observer.observeInto(
+            windowInsets = windowInsets,
+            consumeWindowInsets = consumeWindowInsets,
+            windowInsetsAnimationsEnabled = false
+        )
+        onDispose {
+            observer.stop()
+        }
+    }
+
+    CompositionLocalProvider(LocalWindowInsets provides windowInsets) {
+        content()
+    }
+}
+
+/**
+ * Applies any [WindowInsetsCompat] values to [LocalWindowInsets], which are then available
+ * within [content].
+ *
+ * If you're using this in fragments, you may wish to take a look at
+ * [ViewWindowInsetObserver] for a more optimal solution.
+ *
+ * @param windowInsetsAnimationsEnabled Whether to listen for [WindowInsetsAnimation]s, such as
+ * IME animations.
+ * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are dispatched to
+ * the host view. Defaults to `true`.
+ */
+@SuppressLint("UnnecessaryLambdaCreation")
+@ExperimentalAnimatedInsets
+@Composable
+fun ProvideWindowInsets(
+    windowInsetsAnimationsEnabled: Boolean,
+    consumeWindowInsets: Boolean = true,
+    content: @Composable () -> Unit
+) {
+    val view = LocalView.current
+    val windowInsets = remember { WindowInsets() }
+
+    DisposableEffect(view) {
+        val observer = ViewWindowInsetObserver(view)
+        observer.observeInto(
+            windowInsets = windowInsets,
+            consumeWindowInsets = consumeWindowInsets,
+            windowInsetsAnimationsEnabled = windowInsetsAnimationsEnabled
+        )
+        onDispose {
+            observer.stop()
+        }
+    }
+
+    CompositionLocalProvider(LocalWindowInsets provides windowInsets) {
+        content()
+    }
+}
+
+private class InnerWindowInsetsAnimationCallback(
+    private val windowInsets: WindowInsets,
+) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
+    override fun onPrepare(animation: WindowInsetsAnimationCompat) {
+        // Go through each type and flag that an animation has started
+        if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
+            windowInsets.ime.onAnimationStart()
+        }
+        if (animation.typeMask and WindowInsetsCompat.Type.statusBars() != 0) {
+            windowInsets.statusBars.onAnimationStart()
+        }
+        if (animation.typeMask and WindowInsetsCompat.Type.navigationBars() != 0) {
+            windowInsets.navigationBars.onAnimationStart()
+        }
+        if (animation.typeMask and WindowInsetsCompat.Type.systemBars() != 0) {
+            windowInsets.systemBars.onAnimationStart()
+        }
+        if (animation.typeMask and WindowInsetsCompat.Type.systemGestures() != 0) {
+            windowInsets.systemGestures.onAnimationStart()
+        }
+    }
+
+    override fun onProgress(
+        platformInsets: WindowInsetsCompat,
+        runningAnimations: List<WindowInsetsAnimationCompat>
+    ): WindowInsetsCompat {
+        // Update each inset type with the given parameters
+        windowInsets.ime.updateAnimation(
+            platformInsets = platformInsets,
+            runningAnimations = runningAnimations,
+            type = WindowInsetsCompat.Type.ime()
+        )
+        windowInsets.statusBars.updateAnimation(
+            platformInsets = platformInsets,
+            runningAnimations = runningAnimations,
+            type = WindowInsetsCompat.Type.statusBars()
+        )
+        windowInsets.navigationBars.updateAnimation(
+            platformInsets = platformInsets,
+            runningAnimations = runningAnimations,
+            type = WindowInsetsCompat.Type.navigationBars()
+        )
+        windowInsets.systemBars.updateAnimation(
+            platformInsets = platformInsets,
+            runningAnimations = runningAnimations,
+            type = WindowInsetsCompat.Type.systemBars()
+        )
+        windowInsets.systemBars.updateAnimation(
+            platformInsets = platformInsets,
+            runningAnimations = runningAnimations,
+            type = WindowInsetsCompat.Type.systemGestures()
+        )
+        return platformInsets
+    }
+
+    private inline fun InsetsType.updateAnimation(
+        platformInsets: WindowInsetsCompat,
+        runningAnimations: List<WindowInsetsAnimationCompat>,
+        type: Int,
+    ) {
+        // If there are animations of the given type...
+        if (runningAnimations.any { it.typeMask or type != 0 }) {
+            // Update our animated inset values
+            _animatedInsets.updateFrom(platformInsets.getInsets(type))
+            // And update the animation fraction. We use the maximum animation progress of any
+            // ongoing animations for this type.
+            animationFraction = runningAnimations.maxOf { it.fraction }
+        }
+    }
+
+    override fun onEnd(animation: WindowInsetsAnimationCompat) {
+        // Go through each type and flag that an animation has ended
+        if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
+            windowInsets.ime.onAnimationEnd()
+        }
+        if (animation.typeMask and WindowInsetsCompat.Type.statusBars() != 0) {
+            windowInsets.statusBars.onAnimationEnd()
+        }
+        if (animation.typeMask and WindowInsetsCompat.Type.navigationBars() != 0) {
+            windowInsets.navigationBars.onAnimationEnd()
+        }
+        if (animation.typeMask and WindowInsetsCompat.Type.systemBars() != 0) {
+            windowInsets.systemBars.onAnimationEnd()
+        }
+        if (animation.typeMask and WindowInsetsCompat.Type.systemGestures() != 0) {
+            windowInsets.systemGestures.onAnimationEnd()
+        }
+    }
+}
+
+/**
+ * Updates our mutable state backed [InsetsType] from an Android system insets.
+ */
+private fun MutableInsets.updateFrom(insets: androidx.core.graphics.Insets) {
+    left = insets.left
+    top = insets.top
+    right = insets.right
+    bottom = insets.bottom
+}
+
+/**
+ * Ensures that each dimension is not less than corresponding dimension in the
+ * specified [minimumValue].
+ *
+ * @return this if every dimension is greater than or equal to the corresponding
+ * dimension value in [minimumValue], otherwise a copy of this with each dimension coerced with the
+ * corresponding dimension value in [minimumValue].
+ */
+fun InsetsType.coerceEachDimensionAtLeast(minimumValue: InsetsType): Insets {
+    // Fast path, no need to copy if: this >= minimumValue
+    if (left >= minimumValue.left && top >= minimumValue.top &&
+        right >= minimumValue.right && bottom >= minimumValue.bottom
+    ) {
+        return this
+    }
+    return MutableInsets(
+        left = left.coerceAtLeast(minimumValue.left),
+        top = top.coerceAtLeast(minimumValue.top),
+        right = right.coerceAtLeast(minimumValue.right),
+        bottom = bottom.coerceAtLeast(minimumValue.bottom),
+    )
+}
+
+enum class HorizontalSide { Left, Right }
+enum class VerticalSide { Top, Bottom }
+
+@RequiresOptIn(
+    message = "Animated Insets support is experimental. The API may be changed in the " +
+        "future."
+)
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+annotation class ExperimentalAnimatedInsets
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Padding.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Padding.kt
new file mode 100644
index 0000000..c3fcfe6
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Padding.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * TODO: Move to depending on Accompanist with prebuilts when we hit a stable version
+ * https://github.com/google/accompanist/blob/main/insets/src/main/java/com/google/accompanist
+ * /insets/Padding.kt
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "unused")
+
+@file:JvmName("ComposeInsets")
+@file:JvmMultifileClass
+
+package androidx.compose.material.catalog.insets
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
+
+/**
+ * Selectively apply additional space which matches the width/height of any system bars present
+ * on the respective edges of the screen.
+ *
+ * @param enabled Whether to apply padding using the system bars dimensions on the respective edges.
+ * Defaults to `true`.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.systemBarsPadding(
+    enabled: Boolean = true
+): Modifier = composed {
+    InsetsPaddingModifier(
+        insetsType = LocalWindowInsets.current.systemBars,
+        applyLeft = enabled,
+        applyTop = enabled,
+        applyRight = enabled,
+        applyBottom = enabled
+    )
+}
+
+/**
+ * Apply additional space which matches the height of the status bars height along the top edge
+ * of the content.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.statusBarsPadding(): Modifier = composed {
+    InsetsPaddingModifier(
+        insetsType = LocalWindowInsets.current.statusBars,
+        applyTop = true
+    )
+}
+
+/**
+ * Apply additional space which matches the height of the navigation bars height
+ * along the [bottom] edge of the content, and additional space which matches the width of
+ * the navigation bars on the respective [left] and [right] edges.
+ *
+ * @param bottom Whether to apply padding to the bottom edge, which matches the navigation bars
+ * height (if present) at the bottom edge of the screen. Defaults to `true`.
+ * @param left Whether to apply padding to the left edge, which matches the navigation bars width
+ * (if present) on the left edge of the screen. Defaults to `true`.
+ * @param right Whether to apply padding to the right edge, which matches the navigation bars width
+ * (if present) on the right edge of the screen. Defaults to `true`.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.navigationBarsPadding(
+    bottom: Boolean = true,
+    left: Boolean = true,
+    right: Boolean = true
+): Modifier = composed {
+    InsetsPaddingModifier(
+        insetsType = LocalWindowInsets.current.navigationBars,
+        applyLeft = left,
+        applyRight = right,
+        applyBottom = bottom
+    )
+}
+
+/**
+ * Apply additional space which matches the height of the [WindowInsets.ime] (on-screen keyboard)
+ * height along the bottom edge of the content.
+ *
+ * This method has no special handling for the [WindowInsets.navigationBars], which usually
+ * intersect the [WindowInsets.ime]. Most apps will usually want to use the
+ * [Modifier.navigationBarsWithImePadding] modifier.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.imePadding(): Modifier = composed {
+    InsetsPaddingModifier(
+        insetsType = LocalWindowInsets.current.ime,
+        applyLeft = true,
+        applyRight = true,
+        applyBottom = true,
+    )
+}
+
+/**
+ * Apply additional space which matches the height of the [WindowInsets.ime] (on-screen keyboard)
+ * height and [WindowInsets.navigationBars]. This is what apps should use to handle any insets
+ * at the bottom of the screen.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.navigationBarsWithImePadding(): Modifier = composed {
+    InsetsPaddingModifier(
+        insetsType = LocalWindowInsets.current.ime,
+        minimumInsetsType = LocalWindowInsets.current.navigationBars,
+        applyLeft = true,
+        applyRight = true,
+        applyBottom = true,
+    )
+}
+
+private data class InsetsPaddingModifier(
+    private val insetsType: InsetsType,
+    private val minimumInsetsType: InsetsType? = null,
+    private val applyLeft: Boolean = false,
+    private val applyTop: Boolean = false,
+    private val applyRight: Boolean = false,
+    private val applyBottom: Boolean = false,
+) : LayoutModifier {
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val transformedInsets = if (minimumInsetsType != null) {
+            // If we have a minimum insets, coerce each dimensions
+            insetsType.coerceEachDimensionAtLeast(minimumInsetsType)
+        } else insetsType
+
+        val left = if (applyLeft) transformedInsets.left else 0
+        val top = if (applyTop) transformedInsets.top else 0
+        val right = if (applyRight) transformedInsets.right else 0
+        val bottom = if (applyBottom) transformedInsets.bottom else 0
+        val horizontal = left + right
+        val vertical = top + bottom
+
+        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
+
+        val width = (placeable.width + horizontal)
+            .coerceIn(constraints.minWidth, constraints.maxWidth)
+        val height = (placeable.height + vertical)
+            .coerceIn(constraints.minHeight, constraints.maxHeight)
+        return layout(width, height) {
+            placeable.place(left, top)
+        }
+    }
+}
+
+/**
+ * Returns the current insets converted into a [PaddingValues].
+ *
+ * @param start Whether to apply the inset on the start dimension.
+ * @param top Whether to apply the inset on the top dimension.
+ * @param end Whether to apply the inset on the end dimension.
+ * @param bottom Whether to apply the inset on the bottom dimension.
+ * @param additionalHorizontal Value to add to the start and end dimensions.
+ * @param additionalVertical Value to add to the top and bottom dimensions.
+ */
+@Composable
+inline fun InsetsType.toPaddingValues(
+    start: Boolean = true,
+    top: Boolean = true,
+    end: Boolean = true,
+    bottom: Boolean = true,
+    additionalHorizontal: Dp = 0.dp,
+    additionalVertical: Dp = 0.dp,
+) = toPaddingValues(
+    start = start,
+    top = top,
+    end = end,
+    bottom = bottom,
+    additionalStart = additionalHorizontal,
+    additionalTop = additionalVertical,
+    additionalEnd = additionalHorizontal,
+    additionalBottom = additionalVertical
+)
+
+/**
+ * Returns the current insets converted into a [PaddingValues].
+ *
+ * @param start Whether to apply the inset on the start dimension.
+ * @param top Whether to apply the inset on the top dimension.
+ * @param end Whether to apply the inset on the end dimension.
+ * @param bottom Whether to apply the inset on the bottom dimension.
+ * @param additionalStart Value to add to the start dimension.
+ * @param additionalTop Value to add to the top dimension.
+ * @param additionalEnd Value to add to the end dimension.
+ * @param additionalBottom Value to add to the bottom dimension.
+ */
+@Composable
+fun InsetsType.toPaddingValues(
+    start: Boolean = true,
+    top: Boolean = true,
+    end: Boolean = true,
+    bottom: Boolean = true,
+    additionalStart: Dp = 0.dp,
+    additionalTop: Dp = 0.dp,
+    additionalEnd: Dp = 0.dp,
+    additionalBottom: Dp = 0.dp,
+): PaddingValues = with(LocalDensity.current) {
+    val layoutDirection = LocalLayoutDirection.current
+    PaddingValues(
+        start = additionalStart + when {
+            start && layoutDirection == LayoutDirection.Ltr -> [email protected]()
+            start && layoutDirection == LayoutDirection.Rtl -> [email protected]()
+            else -> 0.dp
+        },
+        top = additionalTop + when {
+            top -> [email protected]()
+            else -> 0.dp
+        },
+        end = additionalEnd + when {
+            end && layoutDirection == LayoutDirection.Ltr -> [email protected]()
+            end && layoutDirection == LayoutDirection.Rtl -> [email protected]()
+            else -> 0.dp
+        },
+        bottom = additionalBottom + when {
+            bottom -> [email protected]()
+            else -> 0.dp
+        }
+    )
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Size.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Size.kt
new file mode 100644
index 0000000..115014d
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Size.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * TODO: Move to depending on Accompanist with prebuilts when we hit a stable version
+ * https://github.com/google/accompanist/blob/main/insets/src/main/java/com/google/accompanist
+ * /insets/Size.kt
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "unused")
+
+@file:JvmName("ComposeInsets")
+@file:JvmMultifileClass
+
+package androidx.compose.material.catalog.insets
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.height
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Declare the height of the content to match the height of the status bars exactly.
+ *
+ * This is very handy when used with `Spacer` to push content below the status bars:
+ * ```
+ * Column {
+ *     Spacer(Modifier.statusBarHeight())
+ *
+ *     // Content to be drawn below status bars (y-axis)
+ * }
+ * ```
+ *
+ * It's also useful when used to draw a scrim which matches the status bars:
+ * ```
+ * Spacer(
+ *     Modifier.statusBarHeight()
+ *         .fillMaxWidth()
+ *         .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f)
+ * )
+ * ```
+ *
+ * Internally this matches the behavior of the [Modifier.height] modifier.
+ *
+ * @param additional Any additional height to add to the status bars size.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.statusBarsHeight(
+    additional: Dp = 0.dp
+): Modifier = composed {
+    InsetsSizeModifier(
+        insetsType = LocalWindowInsets.current.statusBars,
+        heightSide = VerticalSide.Top,
+        additionalHeight = additional
+    )
+}
+
+/**
+ * Declare the preferred height of the content to match the height of the navigation bars when
+ * present at the bottom of the screen.
+ *
+ * This is very handy when used with `Spacer` to push content below the navigation bars:
+ * ```
+ * Column {
+ *     // Content to be drawn above status bars (y-axis)
+ *     Spacer(Modifier.navigationBarHeight())
+ * }
+ * ```
+ *
+ * It's also useful when used to draw a scrim which matches the navigation bars:
+ * ```
+ * Spacer(
+ *     Modifier.navigationBarHeight()
+ *         .fillMaxWidth()
+ *         .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f)
+ * )
+ * ```
+ *
+ * Internally this matches the behavior of the [Modifier.height] modifier.
+ *
+ * @param additional Any additional height to add to the status bars size.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.navigationBarsHeight(
+    additional: Dp = 0.dp
+): Modifier = composed {
+    InsetsSizeModifier(
+        insetsType = LocalWindowInsets.current.navigationBars,
+        heightSide = VerticalSide.Bottom,
+        additionalHeight = additional
+    )
+}
+
+/**
+ * Declare the preferred width of the content to match the width of the navigation bars,
+ * on the given [side].
+ *
+ * This is very handy when used with `Spacer` to push content inside from any vertical
+ * navigation bars (typically when the device is in landscape):
+ * ```
+ * Row {
+ *     Spacer(Modifier.navigationBarWidth(HorizontalSide.Left))
+ *
+ *     // Content to be inside the navigation bars (x-axis)
+ *
+ *     Spacer(Modifier.navigationBarWidth(HorizontalSide.Right))
+ * }
+ * ```
+ *
+ * It's also useful when used to draw a scrim which matches the navigation bars:
+ * ```
+ * Spacer(
+ *     Modifier.navigationBarWidth(HorizontalSide.Left)
+ *         .fillMaxHeight()
+ *         .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f)
+ * )
+ * ```
+ *
+ * Internally this matches the behavior of the [Modifier.height] modifier.
+ *
+ * @param side The navigation bar side to use as the source for the width.
+ * @param additional Any additional width to add to the status bars size.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.navigationBarsWidth(
+    side: HorizontalSide,
+    additional: Dp = 0.dp
+): Modifier = composed {
+    InsetsSizeModifier(
+        insetsType = LocalWindowInsets.current.navigationBars,
+        widthSide = side,
+        additionalWidth = additional
+    )
+}
+
+/**
+ * [Modifier] class which powers the modifiers above. This is the lower level modifier which
+ * supports the functionality through a number of parameters.
+ *
+ * We may make this public at some point. If you need this, please let us know via the
+ * issue tracker.
+ */
+private data class InsetsSizeModifier(
+    private val insetsType: InsetsType,
+    private val widthSide: HorizontalSide? = null,
+    private val additionalWidth: Dp = 0.dp,
+    private val heightSide: VerticalSide? = null,
+    private val additionalHeight: Dp = 0.dp
+) : LayoutModifier {
+    private val Density.targetConstraints: Constraints
+        get() {
+            val additionalWidthPx = additionalWidth.roundToPx()
+            val additionalHeightPx = additionalHeight.roundToPx()
+            return Constraints(
+                minWidth = additionalWidthPx + when (widthSide) {
+                    HorizontalSide.Left -> insetsType.left
+                    HorizontalSide.Right -> insetsType.right
+                    null -> 0
+                },
+                minHeight = additionalHeightPx + when (heightSide) {
+                    VerticalSide.Top -> insetsType.top
+                    VerticalSide.Bottom -> insetsType.bottom
+                    null -> 0
+                },
+                maxWidth = when (widthSide) {
+                    HorizontalSide.Left -> insetsType.left + additionalWidthPx
+                    HorizontalSide.Right -> insetsType.right + additionalWidthPx
+                    null -> Constraints.Infinity
+                },
+                maxHeight = when (heightSide) {
+                    VerticalSide.Top -> insetsType.top + additionalHeightPx
+                    VerticalSide.Bottom -> insetsType.bottom + additionalHeightPx
+                    null -> Constraints.Infinity
+                }
+            )
+        }
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val wrappedConstraints = targetConstraints.let { targetConstraints ->
+            val resolvedMinWidth = if (widthSide != null) {
+                targetConstraints.minWidth
+            } else {
+                constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
+            }
+            val resolvedMaxWidth = if (widthSide != null) {
+                targetConstraints.maxWidth
+            } else {
+                constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
+            }
+            val resolvedMinHeight = if (heightSide != null) {
+                targetConstraints.minHeight
+            } else {
+                constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
+            }
+            val resolvedMaxHeight = if (heightSide != null) {
+                targetConstraints.maxHeight
+            } else {
+                constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
+            }
+            Constraints(
+                resolvedMinWidth,
+                resolvedMaxWidth,
+                resolvedMinHeight,
+                resolvedMaxHeight
+            )
+        }
+        val placeable = measurable.measure(wrappedConstraints)
+        return layout(placeable.width, placeable.height) {
+            placeable.place(0, 0)
+        }
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ) = measurable.minIntrinsicWidth(height).let {
+        val constraints = targetConstraints
+        it.coerceIn(constraints.minWidth, constraints.maxWidth)
+    }
+
+    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ) = measurable.maxIntrinsicWidth(height).let {
+        val constraints = targetConstraints
+        it.coerceIn(constraints.minWidth, constraints.maxWidth)
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ) = measurable.minIntrinsicHeight(width).let {
+        val constraints = targetConstraints
+        it.coerceIn(constraints.minHeight, constraints.maxHeight)
+    }
+
+    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ) = measurable.maxIntrinsicHeight(width).let {
+        val constraints = targetConstraints
+        it.coerceIn(constraints.minHeight, constraints.maxHeight)
+    }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Components.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Components.kt
new file mode 100644
index 0000000..5e17a3a
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Components.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.model
+
+import androidx.annotation.DrawableRes
+import androidx.compose.material.catalog.R
+
+data class Component(
+    val id: Int,
+    val name: String,
+    val description: String,
+    @DrawableRes
+    val icon: Int = R.drawable.ic_component,
+    val examples: List<Example>
+)
+
+private val AppBarsBottom = Component(
+    id = 1,
+    name = "App bars: bottom",
+    description = "A bottom app bar displays navigation and key actions at the bottom of mobile " +
+        "screens.",
+    examples = AppBarsBottomExamples
+)
+
+private val AppBarsTop = Component(
+    id = 2,
+    name = "App bars: top",
+    description = "The top app bar displays information and actions relating to the current " +
+        "screen.",
+    examples = AppBarsTopExamples
+)
+
+private val Backdrop = Component(
+    id = 3,
+    name = "Backdrop",
+    description = "A backdrop appears behind all other surfaces in an app, displaying contextual " +
+        "and actionable content.",
+    examples = BackdropExamples
+)
+
+private val BottomNavigation = Component(
+    id = 4,
+    name = "Bottom navigation",
+    description = "Bottom navigation bars allow movement between primary destinations in an app.",
+    examples = BottomNavigationExamples
+)
+
+private val Buttons = Component(
+    id = 5,
+    name = "Buttons",
+    description = "Buttons allow users to take actions, and make choices, with a single tap.",
+    examples = ButtonsExamples
+)
+
+private val ButtonsFloatingActionButton = Component(
+    id = 6,
+    name = "Buttons: floating action button",
+    description = "A floating action button (FAB) represents the primary action of a screen.",
+    examples = ButtonsFloatingActionButtonExamples
+)
+
+private val Cards = Component(
+    id = 7,
+    name = "Cards",
+    description = "Cards contain content and actions about a single subject.",
+    examples = CardsExamples
+)
+
+private val Checkboxes = Component(
+    id = 8,
+    name = "Checkboxes",
+    description = "Checkboxes allow the user to select one or more items from a set or turn an " +
+        "option on or off.",
+    examples = CheckboxesExamples
+)
+
+private val Dialogs = Component(
+    id = 9,
+    name = "Dialogs",
+    description = "Dialogs inform users about a task and can contain critical information, " +
+        "require decisions, or involve multiple tasks.",
+    examples = DialogsExamples
+)
+
+private val Dividers = Component(
+    id = 10,
+    name = "Dividers",
+    description = "A divider is a thin line that groups content in lists and layouts.",
+    examples = DividersExamples
+)
+
+private val Lists = Component(
+    id = 11,
+    name = "Lists",
+    description = "Lists are continuous, vertical indexes of text or images.",
+    examples = ListsExamples
+)
+
+private val Menus = Component(
+    id = 12,
+    name = "Menus",
+    description = "Menus display a list of choices on temporary surfaces.",
+    examples = MenusExamples
+)
+
+private val NavigationDrawer = Component(
+    id = 13,
+    name = "Navigation drawer",
+    description = "Navigation drawers provide access to destinations in your app.",
+    examples = NavigationDrawerExamples
+)
+
+private val ProgressIndicators = Component(
+    id = 14,
+    name = "Progress indicators",
+    description = "Progress indicators express an unspecified wait time or display the length of " +
+        "a process.",
+    examples = ProgressIndicatorsExamples
+)
+
+private val RadioButtons = Component(
+    id = 15,
+    name = "Radio buttons",
+    description = "Radio buttons allow the user to select one option from a set.",
+    examples = RadioButtonsExamples
+)
+
+private val SheetsBottom = Component(
+    id = 16,
+    name = "Sheets: bottom",
+    description = "Bottom sheets are surfaces containing supplementary content that are anchored " +
+        "to the bottom of the screen.",
+    examples = SheetsBottomExamples
+)
+
+private val Sliders = Component(
+    id = 17,
+    name = "Sliders",
+    description = "Sliders allow users to make selections from a range of values.",
+    examples = SlidersExamples
+)
+
+private val Snackbars = Component(
+    id = 18,
+    name = "Snackbars",
+    description = "Snackbars provide brief messages about app processes at the bottom of the " +
+        "screen.",
+    examples = SnackbarsExamples
+)
+
+private val Switches = Component(
+    id = 19,
+    name = "Switches",
+    description = "Switches toggle the state of a single setting on or off.",
+    examples = SwitchesExamples
+)
+
+private val Tabs = Component(
+    id = 20,
+    name = "Tabs",
+    description = "Tabs organize content across different screens, data sets, and other " +
+        "interactions.",
+    examples = TabsExamples
+)
+
+private val TextFields = Component(
+    id = 21,
+    name = "Text fields",
+    description = "Text fields let users enter and edit text.",
+    examples = TextFieldsExamples
+)
+
+val Components = listOf(
+    AppBarsBottom,
+    AppBarsTop,
+    Backdrop,
+    BottomNavigation,
+    Buttons,
+    ButtonsFloatingActionButton,
+    Cards,
+    Checkboxes,
+    Dialogs,
+    Dividers,
+    Lists,
+    Menus,
+    NavigationDrawer,
+    ProgressIndicators,
+    RadioButtons,
+    SheetsBottom,
+    Sliders,
+    Snackbars,
+    Switches,
+    Tabs,
+    TextFields
+)
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Examples.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Examples.kt
new file mode 100644
index 0000000..8a192f8
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Examples.kt
@@ -0,0 +1,524 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("COMPOSABLE_FUNCTION_REFERENCE")
+
+package androidx.compose.material.catalog.model
+
+import androidx.compose.material.samples.AlertDialogSample
+import androidx.compose.material.samples.BackdropScaffoldSample
+import androidx.compose.material.samples.BottomDrawerSample
+import androidx.compose.material.samples.BottomNavigationSample
+import androidx.compose.material.samples.BottomNavigationWithOnlySelectedLabelsSample
+import androidx.compose.material.samples.BottomSheetScaffoldSample
+import androidx.compose.material.samples.ButtonSample
+import androidx.compose.material.samples.ButtonWithIconSample
+import androidx.compose.material.samples.CardSample
+import androidx.compose.material.samples.CheckboxSample
+import androidx.compose.material.samples.CircularProgressIndicatorSample
+import androidx.compose.material.samples.ClickableListItems
+import androidx.compose.material.samples.CustomAlertDialogSample
+import androidx.compose.material.samples.FancyIndicatorContainerTabs
+import androidx.compose.material.samples.FancyIndicatorTabs
+import androidx.compose.material.samples.FancyTabs
+import androidx.compose.material.samples.FluidExtendedFab
+import androidx.compose.material.samples.IconTabs
+import androidx.compose.material.samples.LeadingIconTabs
+import androidx.compose.material.samples.LinearProgressIndicatorSample
+import androidx.compose.material.samples.MenuSample
+import androidx.compose.material.samples.ModalBottomSheetSample
+import androidx.compose.material.samples.ModalDrawerSample
+import androidx.compose.material.samples.OneLineListItems
+import androidx.compose.material.samples.OneLineRtlLtrListItems
+import androidx.compose.material.samples.OutlinedButtonSample
+import androidx.compose.material.samples.OutlinedTextFieldSample
+import androidx.compose.material.samples.PasswordTextField
+import androidx.compose.material.samples.RadioButtonSample
+import androidx.compose.material.samples.RadioGroupSample
+import androidx.compose.material.samples.ScaffoldWithCoroutinesSnackbar
+import androidx.compose.material.samples.ScaffoldWithCustomSnackbar
+import androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
+import androidx.compose.material.samples.ScrollingFancyIndicatorContainerTabs
+import androidx.compose.material.samples.ScrollingTextTabs
+import androidx.compose.material.samples.SimpleBottomAppBar
+import androidx.compose.material.samples.SimpleExtendedFabNoIcon
+import androidx.compose.material.samples.SimpleExtendedFabWithIcon
+import androidx.compose.material.samples.SimpleFab
+import androidx.compose.material.samples.SimpleOutlinedTextFieldSample
+import androidx.compose.material.samples.SimpleTextFieldSample
+import androidx.compose.material.samples.SimpleTopAppBar
+import androidx.compose.material.samples.SliderSample
+import androidx.compose.material.samples.StepsSliderSample
+import androidx.compose.material.samples.SwitchSample
+import androidx.compose.material.samples.TextAndIconTabs
+import androidx.compose.material.samples.TextButtonSample
+import androidx.compose.material.samples.TextFieldSample
+import androidx.compose.material.samples.TextFieldWithErrorState
+import androidx.compose.material.samples.TextFieldWithHelperMessage
+import androidx.compose.material.samples.TextFieldWithHideKeyboardOnImeAction
+import androidx.compose.material.samples.TextFieldWithIcons
+import androidx.compose.material.samples.TextFieldWithPlaceholder
+import androidx.compose.material.samples.TextTabs
+import androidx.compose.material.samples.ThreeLineListItems
+import androidx.compose.material.samples.ThreeLineRtlLtrListItems
+import androidx.compose.material.samples.TriStateCheckboxSample
+import androidx.compose.material.samples.TwoLineListItems
+import androidx.compose.material.samples.TwoLineRtlLtrListItems
+import androidx.compose.runtime.Composable
+
+data class Example(
+    val name: String,
+    val description: String,
+    val content: @Composable () -> Unit
+)
+
+private const val AppBarsBottomExampleDescription = "App bars: bottom examples"
+val AppBarsBottomExamples = listOf(
+    Example(
+        name = ::SimpleBottomAppBar.name,
+        description = AppBarsBottomExampleDescription
+    ) {
+        SimpleBottomAppBar()
+    }
+)
+
+private const val AppBarsTopExampleDescription = "App bars: top examples"
+val AppBarsTopExamples = listOf(
+    Example(
+        name = ::SimpleTopAppBar.name,
+        description = AppBarsTopExampleDescription
+    ) {
+        SimpleTopAppBar()
+    }
+)
+
+private const val BackdropExampleDescription = "Backdrop examples"
+val BackdropExamples = listOf(
+    Example(
+        name = ::BackdropScaffoldSample.name,
+        description = BackdropExampleDescription
+    ) {
+        BackdropScaffoldSample()
+    }
+)
+
+private const val BottomNavigationExampleDescription = "Bottom navigation examples"
+val BottomNavigationExamples = listOf(
+    Example(
+        name = ::BottomNavigationSample.name,
+        description = BottomNavigationExampleDescription
+    ) {
+        BottomNavigationSample()
+    },
+    Example(
+        name = ::BottomNavigationWithOnlySelectedLabelsSample.name,
+        description = BottomNavigationExampleDescription
+    ) {
+        BottomNavigationWithOnlySelectedLabelsSample()
+    }
+)
+
+private const val ButtonsExampleDescription = "Buttons examples"
+val ButtonsExamples = listOf(
+    Example(
+        name = ::ButtonSample.name,
+        description = ButtonsExampleDescription
+    ) {
+        ButtonSample()
+    },
+    Example(
+        name = ::OutlinedButtonSample.name,
+        description = ButtonsExampleDescription
+    ) {
+        OutlinedButtonSample()
+    },
+    Example(
+        name = ::TextButtonSample.name,
+        description = ButtonsExampleDescription
+    ) {
+        TextButtonSample()
+    },
+    Example(
+        name = ::ButtonWithIconSample.name,
+        description = ButtonsExampleDescription
+    ) {
+        ButtonWithIconSample()
+    }
+)
+
+private const val ButtonsFloatingActionButtonExampleDescription = "Buttons: floating action " +
+    "button examples"
+val ButtonsFloatingActionButtonExamples = listOf(
+    Example(
+        name = ::SimpleFab.name,
+        description = ButtonsFloatingActionButtonExampleDescription
+    ) {
+        SimpleFab()
+    },
+    Example(
+        name = ::SimpleExtendedFabNoIcon.name,
+        description = ButtonsFloatingActionButtonExampleDescription
+    ) {
+        SimpleExtendedFabNoIcon()
+    },
+    Example(
+        name = ::SimpleExtendedFabWithIcon.name,
+        description = ButtonsFloatingActionButtonExampleDescription
+    ) {
+        SimpleExtendedFabWithIcon()
+    },
+    Example(
+        name = ::FluidExtendedFab.name,
+        description = ButtonsFloatingActionButtonExampleDescription
+    ) {
+        FluidExtendedFab()
+    },
+)
+
+private const val CardsExampleDescription = "Cards examples"
+val CardsExamples = listOf(
+    Example(
+        name = ::CardSample.name,
+        description = CardsExampleDescription
+    ) {
+        CardSample()
+    }
+)
+
+private const val CheckboxesExampleDescription = "Checkboxes examples"
+val CheckboxesExamples = listOf(
+    Example(
+        name = ::CheckboxSample.name,
+        description = CheckboxesExampleDescription
+    ) {
+        CheckboxSample()
+    },
+    Example(
+        name = ::TriStateCheckboxSample.name,
+        description = CheckboxesExampleDescription
+    ) {
+        TriStateCheckboxSample()
+    }
+)
+
+private const val DialogsExampleDescription = "Dialogs examples"
+val DialogsExamples = listOf(
+    Example(
+        name = ::AlertDialogSample.name,
+        description = DialogsExampleDescription
+    ) {
+        AlertDialogSample()
+    },
+    Example(
+        name = ::CustomAlertDialogSample.name,
+        description = DialogsExampleDescription
+    ) {
+        CustomAlertDialogSample()
+    }
+)
+
+// No divider samples
+val DividersExamples = emptyList<Example>()
+
+private const val ListsExampleDescription = "Lists examples"
+val ListsExamples = listOf(
+    Example(
+        name = ::ClickableListItems.name,
+        description = ListsExampleDescription
+    ) {
+        ClickableListItems()
+    },
+    Example(
+        name = ::OneLineListItems.name,
+        description = ListsExampleDescription
+    ) {
+        OneLineListItems()
+    },
+    Example(
+        name = ::TwoLineListItems.name,
+        description = ListsExampleDescription
+    ) {
+        TwoLineListItems()
+    },
+    Example(
+        name = ::ThreeLineListItems.name,
+        description = ListsExampleDescription
+    ) {
+        ThreeLineListItems()
+    },
+    Example(
+        name = ::OneLineRtlLtrListItems.name,
+        description = ListsExampleDescription
+    ) {
+        OneLineRtlLtrListItems()
+    },
+    Example(
+        name = ::TwoLineRtlLtrListItems.name,
+        description = ListsExampleDescription
+    ) {
+        TwoLineRtlLtrListItems()
+    },
+    Example(
+        name = ::ThreeLineRtlLtrListItems.name,
+        description = ListsExampleDescription
+    ) {
+        ThreeLineRtlLtrListItems()
+    }
+)
+
+private const val MenusExampleDescription = "Menus examples"
+val MenusExamples = listOf(
+    Example(
+        name = ::MenuSample.name,
+        description = MenusExampleDescription
+    ) {
+        MenuSample()
+    }
+)
+
+private const val NavigationDrawerExampleDescription = "Navigation drawer examples"
+val NavigationDrawerExamples = listOf(
+    Example(
+        name = ::ModalDrawerSample.name,
+        description = NavigationDrawerExampleDescription
+    ) {
+        ModalDrawerSample()
+    },
+    Example(
+        name = ::BottomDrawerSample.name,
+        description = NavigationDrawerExampleDescription
+    ) {
+        BottomDrawerSample()
+    }
+)
+
+private const val ProgressIndicatorsExampleDescription = "Progress indicators examples"
+val ProgressIndicatorsExamples = listOf(
+    Example(
+        name = ::LinearProgressIndicatorSample.name,
+        description = ProgressIndicatorsExampleDescription
+    ) {
+        LinearProgressIndicatorSample()
+    },
+    Example(
+        name = ::CircularProgressIndicatorSample.name,
+        description = ProgressIndicatorsExampleDescription
+    ) {
+        CircularProgressIndicatorSample()
+    }
+)
+
+private const val RadioButtonsExampleDescription = "Radio buttons examples"
+val RadioButtonsExamples = listOf(
+    Example(
+        name = ::RadioButtonSample.name,
+        description = RadioButtonsExampleDescription
+    ) {
+        RadioButtonSample()
+    },
+    Example(
+        name = ::RadioGroupSample.name,
+        description = RadioButtonsExampleDescription
+    ) {
+        RadioGroupSample()
+    },
+)
+
+private const val SheetsBottomExampleDescription = "Sheets: bottom examples"
+val SheetsBottomExamples = listOf(
+    Example(
+        name = ::BottomSheetScaffoldSample.name,
+        description = SheetsBottomExampleDescription
+    ) {
+        BottomSheetScaffoldSample()
+    },
+    Example(
+        name = ::ModalBottomSheetSample.name,
+        description = SheetsBottomExampleDescription
+    ) {
+        ModalBottomSheetSample()
+    }
+)
+
+private const val SlidersExampleDescription = "Sliders examples"
+val SlidersExamples = listOf(
+    Example(
+        name = ::SliderSample.name,
+        description = SlidersExampleDescription
+    ) {
+        SliderSample()
+    },
+    Example(
+        name = ::StepsSliderSample.name,
+        description = SlidersExampleDescription
+    ) {
+        StepsSliderSample()
+    }
+)
+
+private const val SnackbarsExampleDescription = "Snackbars examples"
+val SnackbarsExamples = listOf(
+    Example(
+        name = ::ScaffoldWithSimpleSnackbar.name,
+        description = SnackbarsExampleDescription
+    ) {
+        ScaffoldWithSimpleSnackbar()
+    },
+    Example(
+        name = ::ScaffoldWithCustomSnackbar.name,
+        description = SnackbarsExampleDescription
+    ) {
+        ScaffoldWithCustomSnackbar()
+    },
+    Example(
+        name = ::ScaffoldWithCoroutinesSnackbar.name,
+        description = SnackbarsExampleDescription
+    ) {
+        ScaffoldWithCoroutinesSnackbar()
+    }
+)
+
+private const val SwitchesExampleDescription = "Switches examples"
+val SwitchesExamples = listOf(
+    Example(
+        name = ::SwitchSample.name,
+        description = SwitchesExampleDescription
+    ) {
+        SwitchSample()
+    }
+)
+
+private const val TabsExampleDescription = "Tabs examples"
+val TabsExamples = listOf(
+    Example(
+        name = ::TextTabs.name,
+        description = TabsExampleDescription
+    ) {
+        TextTabs()
+    },
+    Example(
+        name = ::IconTabs.name,
+        description = TabsExampleDescription
+    ) {
+        IconTabs()
+    },
+    Example(
+        name = ::TextAndIconTabs.name,
+        description = TabsExampleDescription
+    ) {
+        TextAndIconTabs()
+    },
+    Example(
+        name = ::LeadingIconTabs.name,
+        description = TabsExampleDescription
+    ) {
+        LeadingIconTabs()
+    },
+    Example(
+        name = ::ScrollingTextTabs.name,
+        description = TabsExampleDescription
+    ) {
+        ScrollingTextTabs()
+    },
+    Example(
+        name = ::FancyTabs.name,
+        description = TabsExampleDescription
+    ) {
+        FancyTabs()
+    },
+    Example(
+        name = ::FancyIndicatorTabs.name,
+        description = TabsExampleDescription
+    ) {
+        FancyIndicatorTabs()
+    },
+    Example(
+        name = ::FancyIndicatorContainerTabs.name,
+        description = TabsExampleDescription
+    ) {
+        FancyIndicatorContainerTabs()
+    },
+    Example(
+        name = ::ScrollingFancyIndicatorContainerTabs.name,
+        description = TabsExampleDescription
+    ) {
+        ScrollingFancyIndicatorContainerTabs()
+    }
+)
+
+private const val TextFieldsExampleDescription = "Text fields examples"
+val TextFieldsExamples = listOf(
+    Example(
+        name = ::SimpleTextFieldSample.name,
+        description = TextFieldsExampleDescription
+    ) {
+        SimpleTextFieldSample()
+    },
+    Example(
+        name = ::TextFieldSample.name,
+        description = TextFieldsExampleDescription
+    ) {
+        TextFieldSample()
+    },
+    Example(
+        name = ::SimpleOutlinedTextFieldSample.name,
+        description = TextFieldsExampleDescription
+    ) {
+        SimpleOutlinedTextFieldSample()
+    },
+    Example(
+        name = ::OutlinedTextFieldSample.name,
+        description = TextFieldsExampleDescription
+    ) {
+        OutlinedTextFieldSample()
+    },
+    Example(
+        name = ::TextFieldWithIcons.name,
+        description = TextFieldsExampleDescription
+    ) {
+        TextFieldWithIcons()
+    },
+    Example(
+        name = ::TextFieldWithPlaceholder.name,
+        description = TextFieldsExampleDescription
+    ) {
+        TextFieldWithPlaceholder()
+    },
+    Example(
+        name = ::TextFieldWithErrorState.name,
+        description = TextFieldsExampleDescription
+    ) {
+        TextFieldWithErrorState()
+    },
+    Example(
+        name = ::TextFieldWithHelperMessage.name,
+        description = TextFieldsExampleDescription
+    ) {
+        TextFieldWithHelperMessage()
+    },
+    Example(
+        name = ::PasswordTextField.name,
+        description = TextFieldsExampleDescription
+    ) {
+        PasswordTextField()
+    },
+    Example(
+        name = ::TextFieldWithHideKeyboardOnImeAction.name,
+        description = TextFieldsExampleDescription
+    ) {
+        TextFieldWithHideKeyboardOnImeAction()
+    }
+)
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/Border.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/Border.kt
new file mode 100644
index 0000000..9d0fc08
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/Border.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.common
+
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Draws a stroke border on the inner edges of grid items i.e. bottom as well as end (if not the
+ * last item in a row).
+ *
+ * @param itemIndex The zero-based index of the grid item.
+ * @param cellsCount The number of cells (columns for vertical, rows for horizontal) in the grid.
+ * @param color The color of the border.
+ * @param width The width of the border.
+ */
+fun Modifier.gridItemBorder(
+    itemIndex: Int,
+    cellsCount: Int,
+    color: Color,
+    width: Dp = BorderWidth
+) = drawBehind {
+    val end = itemIndex.inc().rem(cellsCount) == 0
+    drawLine(
+        color = color,
+        strokeWidth = width.toPx(),
+        cap = StrokeCap.Square,
+        start = Offset(0f, size.height),
+        end = Offset(size.width, size.height)
+    )
+    if (!end) drawLine(
+        color = color,
+        strokeWidth = width.toPx(),
+        cap = StrokeCap.Square,
+        start = Offset(size.width, size.height),
+        end = Offset(size.width, 0f)
+    )
+}
+
+/**
+ * Composite of local content color at 12% alpha over background color, used by borders.
+ */
+@Composable
+fun compositeBorderColor(): Color = LocalContentColor.current.copy(alpha = BorderAlpha)
+    .compositeOver(MaterialTheme.colors.background)
+
+val BorderWidth = 1.dp
+private const val BorderAlpha = 0.12f
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogScaffold.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogScaffold.kt
new file mode 100644
index 0000000..0cb6307
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogScaffold.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.common
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material.Scaffold
+import androidx.compose.material.catalog.util.DOCS_URL
+import androidx.compose.material.catalog.util.GUIDELINES_URL
+import androidx.compose.material.catalog.util.ISSUE_URL
+import androidx.compose.material.catalog.util.SOURCE_URL
+import androidx.compose.material.catalog.util.openUrl
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+fun CatalogScaffold(
+    topBarTitle: String,
+    showBackNavigationIcon: Boolean = false,
+    onBackClick: () -> Unit = {},
+    content: @Composable (PaddingValues) -> Unit
+) {
+    val context = LocalContext.current
+    Scaffold(
+        topBar = {
+            CatalogTopAppBar(
+                title = topBarTitle,
+                showBackNavigationIcon = showBackNavigationIcon,
+                onBackClick = onBackClick,
+                onGuidelinesClick = { context.openUrl(GUIDELINES_URL) },
+                onDocsClick = { context.openUrl(DOCS_URL) },
+                onSourceClick = { context.openUrl(SOURCE_URL) },
+                onIssueClick = { context.openUrl(ISSUE_URL) }
+            )
+        },
+        content = content
+    )
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogTopAppBar.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogTopAppBar.kt
new file mode 100644
index 0000000..5b3dba8
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogTopAppBar.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.common
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material.AppBarDefaults
+import androidx.compose.material.Divider
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.catalog.R
+import androidx.compose.material.catalog.insets.navigationBarsPadding
+import androidx.compose.material.catalog.insets.statusBarsPadding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.primarySurface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun CatalogTopAppBar(
+    title: String,
+    showBackNavigationIcon: Boolean = false,
+    onBackClick: () -> Unit = {},
+    onThemeClick: () -> Unit = {},
+    onGuidelinesClick: () -> Unit = {},
+    onDocsClick: () -> Unit = {},
+    onSourceClick: () -> Unit = {},
+    onIssueClick: () -> Unit = {},
+) {
+    var moreMenuExpanded by remember { mutableStateOf(false) }
+    // Wrapping in a Surface to handle window insets
+    // https://issuetracker.google.com/issues/183161866
+    Surface(
+        color = MaterialTheme.colors.primarySurface,
+        elevation = AppBarDefaults.TopAppBarElevation
+    ) {
+        TopAppBar(
+            title = {
+                Text(
+                    text = title,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis
+                )
+            },
+            actions = {
+                Box {
+                    Row {
+                        IconButton(onClick = onThemeClick) {
+                            Icon(
+                                painter = painterResource(id = R.drawable.ic_palette_24dp),
+                                contentDescription = null
+                            )
+                        }
+                        IconButton(onClick = { moreMenuExpanded = true }) {
+                            Icon(
+                                imageVector = Icons.Default.MoreVert,
+                                contentDescription = null
+                            )
+                        }
+                    }
+                    MoreMenu(
+                        expanded = moreMenuExpanded,
+                        onDismissRequest = { moreMenuExpanded = false },
+                        onGuidelinesClick = {
+                            onGuidelinesClick()
+                            moreMenuExpanded = false
+                        },
+                        onDocsClick = {
+                            onDocsClick()
+                            moreMenuExpanded = false
+                        },
+                        onSourceClick = {
+                            onSourceClick()
+                            moreMenuExpanded = false
+                        },
+                        onIssueClick = {
+                            onIssueClick()
+                            moreMenuExpanded = false
+                        }
+                    )
+                }
+            },
+            navigationIcon = if (showBackNavigationIcon) {
+                {
+                    IconButton(onClick = onBackClick) {
+                        Icon(
+                            imageVector = Icons.Default.ArrowBack,
+                            contentDescription = null
+                        )
+                    }
+                }
+            } else {
+                null
+            },
+            backgroundColor = Color.Transparent,
+            elevation = 0.dp,
+            modifier = Modifier
+                .statusBarsPadding()
+                .navigationBarsPadding(bottom = false)
+        )
+    }
+}
+
+@Composable
+private fun MoreMenu(
+    expanded: Boolean,
+    onDismissRequest: () -> Unit,
+    onGuidelinesClick: () -> Unit,
+    onDocsClick: () -> Unit,
+    onSourceClick: () -> Unit,
+    onIssueClick: () -> Unit
+) {
+    DropdownMenu(
+        expanded = expanded,
+        onDismissRequest = onDismissRequest
+    ) {
+        DropdownMenuItem(onClick = onGuidelinesClick) {
+            Text(stringResource(id = R.string.view_design_guidelines))
+        }
+        DropdownMenuItem(onClick = onDocsClick) {
+            Text(stringResource(id = R.string.view_developer_docs))
+        }
+        DropdownMenuItem(onClick = onSourceClick) {
+            Text(stringResource(id = R.string.view_source_code))
+        }
+        Divider()
+        DropdownMenuItem(onClick = onIssueClick) {
+            Text(stringResource(id = R.string.report_an_issue))
+        }
+    }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/Component.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/Component.kt
new file mode 100644
index 0000000..86a453b
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/Component.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.component
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.catalog.R
+import androidx.compose.material.catalog.insets.LocalWindowInsets
+import androidx.compose.material.catalog.insets.toPaddingValues
+import androidx.compose.material.catalog.model.Component
+import androidx.compose.material.catalog.model.Example
+import androidx.compose.material.catalog.ui.common.CatalogScaffold
+import androidx.compose.material.catalog.ui.example.ExampleItem
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Component(
+    component: Component,
+    onExampleClick: (example: Example) -> Unit,
+    onBackClick: () -> Unit
+) {
+    CatalogScaffold(
+        topBarTitle = component.name,
+        showBackNavigationIcon = true,
+        onBackClick = onBackClick
+    ) { paddingValues ->
+        LazyColumn(
+            modifier = Modifier
+                .padding(paddingValues)
+                .padding(horizontal = ComponentPadding),
+            contentPadding = LocalWindowInsets.current.navigationBars.toPaddingValues()
+        ) {
+            item {
+                Icon(
+                    painter = painterResource(id = component.icon),
+                    contentDescription = null,
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(ComponentIconHeight)
+                        .padding(vertical = ComponentIconVerticalPadding),
+                    // TODO: Remove tint when using component tile images
+                    tint = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
+                )
+            }
+            item {
+                Text(
+                    text = stringResource(id = R.string.description),
+                    style = MaterialTheme.typography.body1
+                )
+                Spacer(modifier = Modifier.height(ComponentPadding))
+                Text(
+                    text = component.description,
+                    style = MaterialTheme.typography.body2
+                )
+                Spacer(modifier = Modifier.height(ComponentDescriptionPadding))
+            }
+            item {
+                Text(
+                    text = stringResource(id = R.string.examples),
+                    style = MaterialTheme.typography.body1
+                )
+                Spacer(modifier = Modifier.height(ComponentPadding))
+            }
+            if (component.examples.isNotEmpty()) {
+                items(component.examples) { example ->
+                    ExampleItem(
+                        example = example,
+                        onClick = onExampleClick
+                    )
+                    Spacer(modifier = Modifier.height(ComponentPadding))
+                }
+            } else {
+                item {
+                    Text(
+                        text = stringResource(id = R.string.no_examples),
+                        style = MaterialTheme.typography.body2
+                    )
+                    Spacer(modifier = Modifier.height(ComponentPadding))
+                }
+            }
+        }
+    }
+}
+
+private val ComponentIconHeight = 192.dp
+private val ComponentIconVerticalPadding = 42.dp
+private val ComponentPadding = 16.dp
+private val ComponentDescriptionPadding = 32.dp
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/ComponentItem.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/ComponentItem.kt
new file mode 100644
index 0000000..8d55544
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/ComponentItem.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.catalog.model.Component
+import androidx.compose.material.catalog.ui.common.compositeBorderColor
+import androidx.compose.material.catalog.ui.common.gridItemBorder
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ComponentItem(
+    component: Component,
+    onClick: (component: Component) -> Unit,
+    index: Int,
+    cellsCount: Int,
+) {
+    Box(
+        modifier = Modifier
+            .height(ComponentItemHeight)
+            .clickable { onClick(component) }
+            .gridItemBorder(
+                itemIndex = index,
+                cellsCount = cellsCount,
+                color = compositeBorderColor()
+            )
+            .padding(ComponentItemPadding)
+    ) {
+        Icon(
+            painter = painterResource(id = component.icon),
+            contentDescription = null,
+            modifier = Modifier.align(Alignment.Center),
+            // TODO: Remove tint when using component tile images
+            tint = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
+        )
+        Text(
+            text = component.name,
+            modifier = Modifier.align(Alignment.BottomStart),
+            style = MaterialTheme.typography.caption
+        )
+    }
+}
+
+private val ComponentItemHeight = 164.dp
+private val ComponentItemPadding = 16.dp
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/Example.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/Example.kt
new file mode 100644
index 0000000..245bc7e
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/Example.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.example
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.catalog.insets.navigationBarsPadding
+import androidx.compose.material.catalog.model.Example
+import androidx.compose.material.catalog.ui.common.CatalogScaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@Composable
+fun Example(
+    example: Example,
+    onBackClick: () -> Unit
+) {
+    CatalogScaffold(
+        topBarTitle = example.name,
+        showBackNavigationIcon = true,
+        onBackClick = onBackClick
+    ) { paddingValues ->
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(paddingValues)
+                .navigationBarsPadding(),
+            contentAlignment = Alignment.Center
+        ) {
+            example.content()
+        }
+    }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/ExampleItem.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/ExampleItem.kt
new file mode 100644
index 0000000..874afa1
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/ExampleItem.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.example
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Card
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentAlpha
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.catalog.model.Example
+import androidx.compose.material.catalog.ui.common.BorderWidth
+import androidx.compose.material.catalog.ui.common.compositeBorderColor
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ExampleItem(
+    example: Example,
+    onClick: (example: Example) -> Unit
+) {
+    Card(
+        elevation = 0.dp,
+        border = BorderStroke(
+            width = BorderWidth,
+            color = compositeBorderColor()
+        ),
+        modifier = Modifier.fillMaxWidth()
+    ) {
+        Row(
+            modifier = Modifier
+                .clickable { onClick(example) }
+                .padding(ExampleItemPadding)
+        ) {
+            Column(modifier = Modifier.weight(1f, fill = true)) {
+                Text(
+                    text = example.name,
+                    style = MaterialTheme.typography.subtitle2
+                )
+                Spacer(modifier = Modifier.height(ExampleItemTextPadding))
+                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+                    Text(
+                        text = example.description,
+                        style = MaterialTheme.typography.caption
+                    )
+                }
+            }
+            Spacer(modifier = Modifier.width(ExampleItemPadding))
+            Icon(
+                imageVector = Icons.Default.KeyboardArrowRight,
+                contentDescription = null,
+                modifier = Modifier.align(Alignment.CenterVertically)
+            )
+        }
+    }
+}
+
+private val ExampleItemPadding = 16.dp
+private val ExampleItemTextPadding = 8.dp
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/home/Home.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/home/Home.kt
new file mode 100644
index 0000000..ad30d01
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/home/Home.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.home
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.GridCells
+import androidx.compose.foundation.lazy.LazyVerticalGrid
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.catalog.R
+import androidx.compose.material.catalog.insets.LocalWindowInsets
+import androidx.compose.material.catalog.insets.toPaddingValues
+import androidx.compose.material.catalog.model.Component
+import androidx.compose.material.catalog.ui.common.CatalogScaffold
+import androidx.compose.material.catalog.ui.component.ComponentItem
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+
+@Composable
+@OptIn(ExperimentalFoundationApi::class)
+fun Home(
+    components: List<Component>,
+    onComponentClick: (component: Component) -> Unit
+) {
+    CatalogScaffold(
+        topBarTitle = stringResource(id = R.string.material_components)
+    ) { paddingValues ->
+        BoxWithConstraints(modifier = Modifier.padding(paddingValues)) {
+            val cellsCount = maxOf((maxWidth / HomeCellMinSize).toInt(), 1)
+            LazyVerticalGrid(
+                // LazyGridScope doesn't expose nColumns from LazyVerticalGrid
+                // https://issuetracker.google.com/issues/183187002
+                cells = GridCells.Fixed(count = cellsCount),
+                content = {
+                    itemsIndexed(components) { index, component ->
+                        ComponentItem(
+                            component = component,
+                            onClick = onComponentClick,
+                            index = index,
+                            cellsCount = cellsCount
+                        )
+                    }
+                },
+                contentPadding = LocalWindowInsets.current.navigationBars.toPaddingValues()
+            )
+        }
+    }
+}
+
+private val HomeCellMinSize = 180.dp
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/theme/Theme.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/theme/Theme.kt
new file mode 100644
index 0000000..5432380
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/theme/Theme.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+
+@Composable
+fun CatalogTheme(content: @Composable () -> Unit) {
+    MaterialTheme(
+        colors = if (isSystemInDarkTheme()) {
+            darkColors()
+        } else {
+            lightColors()
+        },
+        content = content
+    )
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/util/Url.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/util/Url.kt
new file mode 100644
index 0000000..6bd63ad
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/util/Url.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material.catalog.util
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+
+fun Context.openUrl(url: String) {
+    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+    startActivity(intent)
+}
+
+const val GUIDELINES_URL = "https://material.io/components"
+const val DOCS_URL = "https://developer.android.com/jetpack/androidx/releases/compose-material"
+const val SOURCE_URL = "https://cs.android.com/androidx/platform/frameworks/support/+/" +
+    "androidx-main:compose/material/"
+const val ISSUE_URL = "https://issuetracker.google.com/issues/new?component=742043"
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_component.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_component.xml
new file mode 100644
index 0000000..84894df
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_component.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="52dp"
+    android:height="53dp"
+    android:viewportWidth="52"
+    android:viewportHeight="53">
+  <!-- Long path to avoid Icon stroke line join bevel bug -->
+  <!-- https://issuetracker.google.com/issues/182794035 -->
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M32,0L32.0002,16.0599C32.4949,16.0202 32.9951,16 33.5,16C43.7173,16
+      52,24.2827 52,34.5C52,44.7173 43.7173,53 33.5,53C23.2827,53 15,44.7173 15,34.5C15,33.6522
+      15.057,32.8178 15.1675,32.0002L0,32L0,0L32,0ZM33.5,18.5C32.9944,18.5 32.4944,18.5234
+      32.0008,18.5693L32,32L17.6941,32.0004C17.5663,32.8149 17.5,33.6497 17.5,34.5C17.5,43.3366
+      24.6634,50.5 33.5,50.5C42.3366,50.5 49.5,43.3366 49.5,34.5C49.5,25.6634 42.3366,18.5
+      33.5,18.5ZM29.5002,2.5L2.5,2.5L2.5,29.5L15.6835,29.5003C17.5115,22.9726 22.8306,17.9035
+      29.5002,16.4335L29.5002,2.5ZM18.2968,29.5L29.5,29.5L29.5007,19.0039C24.2157,20.3639
+      19.9874,24.3562 18.2968,29.5Z" />
+</vector>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_palette_24dp.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_palette_24dp.xml
new file mode 100644
index 0000000..b9e640b
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_palette_24dp.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M12,22C6.49,22 2,17.51 2,12S6.49,2 12,2s10,4.04 10,9c0,3.31 -2.69,6
+      -6,6h-1.77c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.12 0.05,0.23 0.13,0.33 0.41,0.47 0.64,1.06
+      0.64,1.67 0,1.38 -1.12,2.5 -2.5,2.5zM12,4c-4.41,0 -8,3.59 -8,8s3.59,8 8,8c0.28,0 0.5,-0.22
+      0.5,-0.5 0,-0.16 -0.08,-0.28 -0.14,-0.35 -0.41,-0.46 -0.63,-1.05 -0.63,-1.65 0,-1.38
+      1.12,-2.5 2.5,-2.5L16,15c2.21,0 4,-1.79 4,-4 0,-3.86 -3.59,-7 -8,-7z" />
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M6.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M9.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M14.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M17.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+</vector>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/values-night/themes.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..62234c3
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/values-night/themes.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+    <style name="Theme.Catalog" parent="@android:style/Theme.Material.NoActionBar">
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
+    </style>
+
+</resources>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/values/strings.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6db56d476
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/values/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+
+    <string name="app_name">Compose Material catalog</string>
+
+    <string name="material_components">Material Components</string>
+
+    <string name="description">Description</string>
+    <string name="examples">Examples</string>
+    <string name="no_examples">No examples</string>
+
+    <string name="view_design_guidelines">View design guidelines</string>
+    <string name="view_developer_docs">View developer docs</string>
+    <string name="view_source_code">View source code</string>
+    <string name="report_an_issue">Report an issue</string>
+
+</resources>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/values/themes.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/values/themes.xml
new file mode 100644
index 0000000..4661c15
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/values/themes.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+    <style name="Theme.Catalog" parent="@android:style/Theme.Material.Light.NoActionBar">
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
+    </style>
+
+</resources>
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
index 8b6a2df4..dbb3ff1 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
@@ -24,8 +24,8 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.shape.CircleShape
@@ -57,12 +57,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.BroadcastChannel
 import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.asFlow
 import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.launch
 import kotlin.math.abs
 import kotlin.math.roundToInt
@@ -260,16 +257,15 @@
 }
 
 @Sampled
-@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
 @Composable
 fun ScaffoldWithCoroutinesSnackbar() {
     // decouple snackbar host state from scaffold state for demo purposes
     // this state, channel and flow is for demo purposes to demonstrate business logic layer
     val snackbarHostState = remember { SnackbarHostState() }
     // we allow only one snackbar to be in the queue here, hence conflated
-    val channel = remember { BroadcastChannel<Int>(Channel.Factory.CONFLATED) }
+    val channel = remember { Channel<Int>(Channel.Factory.CONFLATED) }
     LaunchedEffect(channel) {
-        channel.asFlow().collect { index ->
+        channel.receiveAsFlow().collect { index ->
             val result = snackbarHostState.showSnackbar(
                 message = "Snackbar # $index",
                 actionLabel = "Action on $index"
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
index 51f0eb4..021cac7 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
@@ -197,7 +197,7 @@
         keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
         keyboardActions = KeyboardActions(
             onDone = {
-                keyboardController?.hideSoftwareKeyboard()
+                keyboardController?.hide()
                 // do something here
             }
         )
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index 5d9c108..2dcf21b 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material
 
 import android.os.SystemClock.sleep
+import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
@@ -277,6 +278,97 @@
 
     @Test
     @LargeTest
+    fun modalDrawer_animateTo(): Unit = runBlocking(AutoTestFrameClock()) {
+        lateinit var drawerState: DrawerState
+        rule.setMaterialContent {
+            drawerState = rememberDrawerState(DrawerValue.Closed)
+            ModalDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag("drawer"))
+                },
+                content = {}
+            )
+        }
+
+        val width = rule.rootWidth()
+
+        // Drawer should start in closed state
+        rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
+
+        // When the drawer state is set to Opened
+        drawerState.animateTo(DrawerValue.Open, TweenSpec())
+        // Then the drawer should be opened
+        rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(0.dp)
+
+        // When the drawer state is set to Closed
+        drawerState.animateTo(DrawerValue.Closed, TweenSpec())
+        // Then the drawer should be closed
+        rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
+    }
+
+    @Test
+    @LargeTest
+    fun modalDrawer_snapTo(): Unit = runBlocking(AutoTestFrameClock()) {
+        lateinit var drawerState: DrawerState
+        rule.setMaterialContent {
+            drawerState = rememberDrawerState(DrawerValue.Closed)
+            ModalDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag("drawer"))
+                },
+                content = {}
+            )
+        }
+
+        val width = rule.rootWidth()
+
+        // Drawer should start in closed state
+        rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
+
+        // When the drawer state is set to Opened
+        drawerState.snapTo(DrawerValue.Open)
+        // Then the drawer should be opened
+        rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(0.dp)
+
+        // When the drawer state is set to Closed
+        drawerState.snapTo(DrawerValue.Closed)
+        // Then the drawer should be closed
+        rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
+    }
+
+    @Test
+    @LargeTest
+    fun modalDrawer_currentValue(): Unit = runBlocking(AutoTestFrameClock()) {
+        lateinit var drawerState: DrawerState
+        rule.setMaterialContent {
+            drawerState = rememberDrawerState(DrawerValue.Closed)
+            ModalDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag("drawer"))
+                },
+                content = {}
+            )
+        }
+
+        // Drawer should start in closed state
+        assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+
+        // When the drawer state is set to Opened
+        drawerState.snapTo(DrawerValue.Open)
+        // Then the drawer should be opened
+        assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+
+        // When the drawer state is set to Closed
+        drawerState.snapTo(DrawerValue.Closed)
+        // Then the drawer should be closed
+        assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+    }
+
+    @Test
+    @LargeTest
     fun modalDrawer_bodyContent_clickable(): Unit = runBlocking(AutoTestFrameClock()) {
         var drawerClicks = 0
         var bodyClicks = 0
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
index 7160529..668bf10 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
@@ -15,8 +15,14 @@
  */
 package androidx.compose.material
 
+import android.os.Build
+import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.testutils.assertPixelColor
+import androidx.compose.testutils.assertPixels
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.ProgressBarRangeInfo
 import androidx.compose.ui.test.ExperimentalTestApi
@@ -25,11 +31,15 @@
 import androidx.compose.ui.test.assertRangeInfoEquals
 import androidx.compose.ui.test.assertValueEquals
 import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -173,4 +183,63 @@
         contentToTest
             .assertIsSquareWithSize(40.dp)
     }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun determinateLinearProgressIndicator_sizeModifier() {
+        val expectedWidth = 100.dp
+        val expectedHeight = 10.dp
+        val expectedSize = with(rule.density) {
+            IntSize(expectedWidth.roundToPx(), expectedHeight.roundToPx())
+        }
+        val tag = "progress_indicator"
+        rule.setContent {
+            LinearProgressIndicator(
+                modifier = Modifier.testTag(tag).size(expectedWidth, expectedHeight),
+                progress = 1f,
+                color = Color.Blue
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .captureToImage()
+            .assertPixels(expectedSize = expectedSize) {
+                Color.Blue
+            }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun indeterminateLinearProgressIndicator_sizeModifier() {
+        val expectedWidth = 100.dp
+        val expectedHeight = 10.dp
+        val expectedSize = with(rule.density) {
+            IntSize(expectedWidth.roundToPx(), expectedHeight.roundToPx())
+        }
+        rule.mainClock.autoAdvance = false
+        val tag = "progress_indicator"
+        rule.setContent {
+
+            LinearProgressIndicator(
+                modifier = Modifier.testTag(tag).size(expectedWidth, expectedHeight),
+                color = Color.Blue
+            )
+        }
+
+        rule.mainClock.advanceTimeBy(100)
+
+        rule.onNodeWithTag(tag)
+            .captureToImage()
+            .toPixelMap()
+            .let {
+                assertEquals(expectedSize.width, it.width)
+                assertEquals(expectedSize.height, it.height)
+                // Assert on the first pixel column, to make sure that the progress indicator draws
+                // to the expect height.
+                // We can't assert width as the width dynamically changes during the animation
+                for (i in 0 until it.height) {
+                    it.assertPixelColor(Color.Blue, 0, i)
+                }
+            }
+    }
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index 969ccea..8e21331 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -324,7 +324,7 @@
         rule.runOnIdle { assertThat(hostView.isSoftwareKeyboardShown).isTrue() }
 
         // Hide keyboard.
-        rule.runOnIdle { softwareKeyboardController?.hideSoftwareKeyboard() }
+        rule.runOnIdle { softwareKeyboardController?.hide() }
 
         // Clicking on the text field shows the keyboard.
         rule.onNodeWithTag(TextfieldTag).performClick()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
index b766467..abce8b2 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
@@ -22,8 +22,8 @@
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
@@ -168,6 +168,9 @@
  * See [BottomAppBar anatomy](https://material.io/components/app-bars-bottom/#anatomy) for the
  * recommended content depending on the [FloatingActionButton] position.
  *
+ * Note that when you pass a non-null [cutoutShape] this makes the AppBar shape concave. The shadows
+ * for such shapes will not be drawn on Android versions less than 10.
+ *
  * @sample androidx.compose.material.samples.SimpleBottomAppBar
  *
  * @param modifier The [Modifier] to be applied to this BottomAppBar
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index e623c4d..7009d90 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.material
 
+import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.Canvas
@@ -33,6 +34,7 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.State
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.Saver
@@ -78,6 +80,7 @@
 /**
  * Possible values of [BottomDrawerState].
  */
+@ExperimentalMaterialApi
 enum class BottomDrawerValue {
     /**
      * The state of the bottom drawer when it is closed.
@@ -101,17 +104,20 @@
  * @param initialValue The initial value of the state.
  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
  */
-@Suppress("NotCloseable", "HiddenSuperclass")
+@Suppress("NotCloseable")
 @OptIn(ExperimentalMaterialApi::class)
 @Stable
 class DrawerState(
     initialValue: DrawerValue,
     confirmStateChange: (DrawerValue) -> Boolean = { true }
-) : SwipeableState<DrawerValue>(
-    initialValue = initialValue,
-    animationSpec = AnimationSpec,
-    confirmStateChange = confirmStateChange
 ) {
+
+    internal val swipeableState = SwipeableState(
+        initialValue = initialValue,
+        animationSpec = AnimationSpec,
+        confirmStateChange = confirmStateChange
+    )
+
     /**
      * Whether the drawer is open.
      */
@@ -125,13 +131,33 @@
         get() = currentValue == DrawerValue.Closed
 
     /**
+     * The current value of the state.
+     *
+     * If no swipe or animation is in progress, this corresponds to the start the drawer
+     * currently in. If a swipe or an animation is in progress, this corresponds the state drawer
+     * was in before the swipe or animation started.
+     */
+    val currentValue: DrawerValue
+        get() {
+            return swipeableState.currentValue
+        }
+
+    /**
+     * Whether the state is currently animating.
+     */
+    val isAnimationRunning: Boolean
+        get() {
+            return swipeableState.isAnimationRunning
+        }
+
+    /**
      * Open the drawer with animation and suspend until it if fully opened or animation has been
      * cancelled. This method will throw [CancellationException] if the animation is
      * interrupted
      *
      * @return the reason the open animation ended
      */
-    suspend fun open() = animateTo(DrawerValue.Open)
+    suspend fun open() = animateTo(DrawerValue.Open, AnimationSpec)
 
     /**
      * Close the drawer with animation and suspend until it if fully closed or animation has been
@@ -140,7 +166,48 @@
      *
      * @return the reason the close animation ended
      */
-    suspend fun close() = animateTo(DrawerValue.Closed)
+    suspend fun close() = animateTo(DrawerValue.Closed, AnimationSpec)
+
+    /**
+     * Set the state of the drawer with specific animation
+     *
+     * @param targetValue The new value to animate to.
+     * @param anim The animation that will be used to animate to the new value.
+     */
+    @ExperimentalMaterialApi
+    suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec<Float>) {
+        swipeableState.animateTo(targetValue, anim)
+    }
+
+    /**
+     * Set the state without any animation and suspend until it's set
+     *
+     * @param targetValue The new target value
+     */
+    @ExperimentalMaterialApi
+    suspend fun snapTo(targetValue: DrawerValue) {
+        swipeableState.snapTo(targetValue)
+    }
+
+    /**
+     * The target value of the drawer state.
+     *
+     * If a swipe is in progress, this is the value that the Drawer would animate to if the
+     * swipe finishes. If an animation is running, this is the target value of that animation.
+     * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
+     */
+    @ExperimentalMaterialApi
+    @get:ExperimentalMaterialApi
+    val targetValue: DrawerValue
+        get() = swipeableState.targetValue
+
+    /**
+     * The current position (in pixels) of the drawer sheet.
+     */
+    @ExperimentalMaterialApi
+    @get:ExperimentalMaterialApi
+    val offset: State<Float>
+        get() = swipeableState.offset
 
     companion object {
         /**
@@ -160,8 +227,8 @@
  * @param initialValue The initial value of the state.
  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
  */
-@Suppress("NotCloseable", "HiddenSuperclass")
-@OptIn(ExperimentalMaterialApi::class)
+@Suppress("NotCloseable")
+@ExperimentalMaterialApi
 class BottomDrawerState(
     initialValue: BottomDrawerValue,
     confirmStateChange: (BottomDrawerValue) -> Boolean = { true }
@@ -260,6 +327,7 @@
  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
  */
 @Composable
+@ExperimentalMaterialApi
 fun rememberBottomDrawerState(
     initialValue: BottomDrawerValue,
     confirmStateChange: (BottomDrawerValue) -> Boolean = { true }
@@ -330,7 +398,7 @@
         }
         Box(
             Modifier.swipeable(
-                state = drawerState,
+                state = drawerState.swipeableState,
                 anchors = anchors,
                 thresholds = { _, _ -> FractionalThreshold(0.5f) },
                 orientation = Orientation.Horizontal,
@@ -346,7 +414,9 @@
             Scrim(
                 open = drawerState.isOpen,
                 onClose = { scope.launch { drawerState.close() } },
-                fraction = { calculateFraction(minValue, maxValue, drawerState.offset.value) },
+                fraction = {
+                    calculateFraction(minValue, maxValue, drawerState.offset.value)
+                },
                 color = scrimColor
             )
             Surface(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
index 58fe636..954b607 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
@@ -78,7 +78,7 @@
             .size(LinearIndicatorWidth, LinearIndicatorHeight)
             .focusable()
     ) {
-        val strokeWidth = ProgressIndicatorDefaults.StrokeWidth.toPx()
+        val strokeWidth = size.height
         drawLinearIndicatorBackground(backgroundColor, strokeWidth)
         drawLinearIndicator(0f, progress, color, strokeWidth)
     }
@@ -152,7 +152,7 @@
             .size(LinearIndicatorWidth, LinearIndicatorHeight)
             .focusable()
     ) {
-        val strokeWidth = ProgressIndicatorDefaults.StrokeWidth.toPx()
+        val strokeWidth = size.height
         drawLinearIndicatorBackground(backgroundColor, strokeWidth)
         if (firstLineHead - firstLineTail > 0) {
             drawLinearIndicator(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 2819fda..b1d5f24 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -19,8 +19,8 @@
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.staticCompositionLocalOf
@@ -240,16 +240,16 @@
 
             val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
 
-            val fabPlaceables = subcompose(ScaffoldLayoutContent.Fab, fab).fastMap {
-                it.measure(looseConstraints)
-            }
+            val fabPlaceables =
+                subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable ->
+                    measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
+                }
 
-            val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
-            val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
-
-            // FAB distance from the left of the layout, taking into account LTR / RTL
-            val fabLeftOffset = if (fabWidth != 0 && fabHeight != 0) {
-                if (fabPosition == FabPosition.End) {
+            val fabPlacement = if (fabPlaceables.isNotEmpty()) {
+                val fabWidth = fabPlaceables.fastMaxBy { it.width }!!.width
+                val fabHeight = fabPlaceables.fastMaxBy { it.height }!!.height
+                // FAB distance from the left of the layout, taking into account LTR / RTL
+                val fabLeftOffset = if (fabPosition == FabPosition.End) {
                     if (layoutDirection == LayoutDirection.Ltr) {
                         layoutWidth - FabSpacing.roundToPx() - fabWidth
                     } else {
@@ -258,11 +258,7 @@
                 } else {
                     (layoutWidth - fabWidth) / 2
                 }
-            } else {
-                0
-            }
 
-            val fabPlacement = if (fabWidth != 0 && fabHeight != 0) {
                 FabPlacement(
                     isDocked = isFabDocked,
                     left = fabLeftOffset,
@@ -281,30 +277,23 @@
             }.fastMap { it.measure(looseConstraints) }
 
             val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height ?: 0
-
-            val fabOffsetFromBottom = if (fabWidth != 0 && fabHeight != 0) {
+            val fabOffsetFromBottom = fabPlacement?.let {
                 if (bottomBarHeight == 0) {
-                    fabHeight + FabSpacing.roundToPx()
+                    it.height + FabSpacing.roundToPx()
                 } else {
                     if (isFabDocked) {
                         // Total height is the bottom bar height + half the FAB height
-                        bottomBarHeight + (fabHeight / 2)
+                        bottomBarHeight + (it.height / 2)
                     } else {
                         // Total height is the bottom bar height + the FAB height + the padding
                         // between the FAB and bottom bar
-                        bottomBarHeight + fabHeight + FabSpacing.roundToPx()
+                        bottomBarHeight + it.height + FabSpacing.roundToPx()
                     }
                 }
-            } else {
-                0
             }
 
             val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
-                snackbarHeight + if (fabOffsetFromBottom != 0) {
-                    fabOffsetFromBottom
-                } else {
-                    bottomBarHeight
-                }
+                snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
             } else {
                 0
             }
@@ -332,8 +321,10 @@
                 it.place(0, layoutHeight - bottomBarHeight)
             }
             // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
-            fabPlaceables.fastForEach {
-                it.place(fabLeftOffset, layoutHeight - fabOffsetFromBottom)
+            fabPlacement?.let { placement ->
+                fabPlaceables.fastForEach {
+                    it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
+                }
             }
         }
     }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
index 46eb88b..cd23a8e 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
@@ -43,7 +43,8 @@
  * 1) Clipping: Surface clips its children to the shape specified by [shape]
  *
  * 2) Elevation: Surface draws a shadow to represent depth, where [elevation] represents the
- * depth of this surface.
+ * depth of this surface. If the passed [shape] is concave the shadow will not be drawn on Android
+ * versions less than 10.
  *
  * 3) Borders: If [shape] has a border, then it will also be drawn.
  *
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
index 42acda2..9bee9d4 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
@@ -53,7 +53,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.lerp
 import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.filter
@@ -544,7 +543,6 @@
  * in order to animate to the next state, even if the positional [thresholds] have not been reached.
  */
 @ExperimentalMaterialApi
-@OptIn(ExperimentalCoroutinesApi::class)
 fun <T> Modifier.swipeable(
     state: SwipeableState<T>,
     anchors: Map<Float, T>,
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/CompositionLocalNamingDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/CompositionLocalNamingDetector.kt
index 74d6f2c..ef2fca4 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/CompositionLocalNamingDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/CompositionLocalNamingDetector.kt
@@ -19,6 +19,7 @@
 package androidx.compose.runtime.lint
 
 import androidx.compose.lint.Names
+import androidx.compose.lint.inheritsFrom
 import com.android.tools.lint.client.api.UElementHandler
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Detector
@@ -28,7 +29,6 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.util.InheritanceUtil
 import org.jetbrains.kotlin.psi.KtParameter
 import org.jetbrains.kotlin.psi.KtProperty
 import org.jetbrains.uast.UElement
@@ -54,7 +54,7 @@
             if ((node.sourcePsi as? KtProperty)?.isLocal == true) return
 
             val type = node.type
-            if (!InheritanceUtil.isInheritor(type, Names.Runtime.CompositionLocal.javaFqn)) return
+            if (!type.inheritsFrom(Names.Runtime.CompositionLocal)) return
 
             val name = node.name
             if (name!!.startsWith("Local", ignoreCase = true)) return
diff --git a/compose/runtime/runtime-saveable-lint/build.gradle b/compose/runtime/runtime-saveable-lint/build.gradle
new file mode 100644
index 0000000..d2cc46e
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.BundleInsideHelper
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("kotlin")
+}
+
+BundleInsideHelper.forInsideLintJar(project)
+
+dependencies {
+    // compileOnly because we use lintChecks and it doesn't allow other types of deps
+    // this ugly hack exists because of b/63873667
+    if (rootProject.hasProperty("android.injected.invoked.from.ide")) {
+        compileOnly(LINT_API_LATEST)
+    } else {
+        compileOnly(LINT_API_MIN)
+    }
+    compileOnly(KOTLIN_STDLIB)
+    bundleInside(project(":compose:lint:common"))
+
+    testImplementation(KOTLIN_STDLIB)
+    testImplementation(LINT_CORE)
+    testImplementation(LINT_TESTS)
+}
+
+androidx {
+    name = "Compose Runtime Saveable Lint Checks"
+    type = LibraryType.LINT
+    mavenGroup = LibraryGroups.Compose.RUNTIME
+    inceptionYear = "2021"
+    description = "Compose Runtime Saveable Lint Checks"
+}
diff --git a/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetector.kt b/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetector.kt
new file mode 100644
index 0000000..146c33a
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetector.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.saveable.lint
+
+import androidx.compose.lint.Name
+import androidx.compose.lint.Names
+import androidx.compose.lint.Package
+import androidx.compose.lint.inheritsFrom
+import androidx.compose.lint.isInPackageName
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import java.util.EnumSet
+
+/**
+ * [Detector] that checks `rememberSaveable` calls to make sure that a `Saver` is not passed to
+ * the vararg argument.
+ */
+class RememberSaveableDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames(): List<String> = listOf(RememberSaveable.shortName)
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        if (!method.isInPackageName(RuntimeSaveablePackageName)) return
+
+        val argumentMapping = context.evaluator.computeArgumentMapping(node, method)
+        // Filter the arguments provided to those that correspond to the varargs parameter.
+        val varargArguments = argumentMapping.toList().filter { (_, parameter) ->
+            // TODO: https://youtrack.jetbrains.com/issue/KT-45700 parameter.isVarArgs
+            //  returns false because only varargs parameters at the end of a function are
+            //  considered varargs in PSI, since Java callers can not call them as a vararg
+            //  parameter otherwise. This is true for both KtLightParameterImpl, and
+            //  ClsParameterImpl, so if we wanted to actually find if the parameter was
+            //  vararg for Kotlin callers we would instead need to look through the metadata on
+            //  the class file, or the source KtParameter.
+            //  Instead since they are just treated as an array type in PSI, just find the
+            //  corresponding array parameter.
+            parameter.type is PsiArrayType
+        }.map {
+            // We don't need the parameter anymore, so just return the argument
+            it.first
+        }
+
+        // Ignore if there are no vararg arguments provided, and ignore if there are multiple (we
+        // assume if that multiple are provided then the developer knows what they are doing)
+        if (varargArguments.size != 1) return
+
+        val argument = varargArguments.first()
+        val argumentType = argument.getExpressionType()
+
+        // Ignore if the expression isn't a `Saver`
+        if (argumentType?.inheritsFrom(Saver) != true) return
+
+        // If the type is a MutableState, there is a second overload with a differently
+        // named parameter we should use instead
+        val isMutableStateSaver = node.getExpressionType()
+            ?.inheritsFrom(Names.Runtime.MutableState) == true
+
+        // TODO: might be safer to try and find the other overload through PSI, and get
+        //  the parameter name directly.
+        val parameterName = if (isMutableStateSaver) {
+            "stateSaver"
+        } else {
+            "saver"
+        }
+
+        val argumentText = argument.sourcePsi?.text
+
+        context.report(
+            RememberSaveableSaverParameter,
+            node,
+            context.getLocation(argument),
+            "Passing `Saver` instance to vararg `inputs`",
+            argumentText?.let {
+                val replacement = "$parameterName = $argumentText"
+                LintFix.create()
+                    .replace()
+                    .name("Change to `$replacement`")
+                    .text(argumentText)
+                    .with(replacement)
+                    .autoFix()
+                    .build()
+            }
+        )
+    }
+
+    companion object {
+        val RememberSaveableSaverParameter = Issue.create(
+            "RememberSaveableSaverParameter",
+            "`Saver` objects should be passed to the saver parameter, not the vararg " +
+                "`inputs` parameter",
+            "The first parameter to `rememberSaveable` is a vararg parameter for inputs that when" +
+                " changed will cause the state to reset. Passing a `Saver` object to this " +
+                "parameter is an error, as the intention is to pass the `Saver` object to the " +
+                "saver parameter. Since the saver parameter is not the first parameter, it must " +
+                "be explicitly named.",
+            Category.CORRECTNESS, 3, Severity.ERROR,
+            Implementation(
+                RememberSaveableDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+            )
+        )
+    }
+}
+
+private val RuntimeSaveablePackageName = Package("androidx.compose.runtime.saveable")
+private val RememberSaveable = Name(RuntimeSaveablePackageName, "rememberSaveable")
+private val Saver = Name(RuntimeSaveablePackageName, "Saver")
diff --git a/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RuntimeSaveableIssueRegistry.kt b/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RuntimeSaveableIssueRegistry.kt
new file mode 100644
index 0000000..89dda1a
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RuntimeSaveableIssueRegistry.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.saveable.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.detector.api.CURRENT_API
+
+/**
+ * [IssueRegistry] containing runtime-saveable specific lint issues.
+ */
+class RuntimeSaveableIssueRegistry : IssueRegistry() {
+    // Tests are run with this version. We ensure that with ApiLintVersionsTest
+    override val api = 8
+    override val minApi = CURRENT_API
+    override val issues get() = listOf(
+        RememberSaveableDetector.RememberSaveableSaverParameter
+    )
+}
diff --git a/compose/runtime/runtime-saveable-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/compose/runtime/runtime-saveable-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
new file mode 100644
index 0000000..ff96940
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
@@ -0,0 +1 @@
+androidx.compose.runtime.saveable.lint.RuntimeSaveableIssueRegistry
diff --git a/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/ApiLintVersionsTest.kt b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/ApiLintVersionsTest.kt
new file mode 100644
index 0000000..9bc4684
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/ApiLintVersionsTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.saveable.lint
+
+import com.android.tools.lint.client.api.LintClient
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ApiLintVersionsTest {
+
+    @Test
+    fun versionsCheck() {
+        LintClient.clientName = LintClient.CLIENT_UNIT_TESTS
+
+        val registry = RuntimeSaveableIssueRegistry()
+        // we hardcode version registry.api to the version that is used to run tests
+        assertThat(registry.api).isEqualTo(CURRENT_API)
+        // Intentionally fails in IDE, because we use different API version in
+        // studio and command line
+        assertThat(registry.minApi).isEqualTo(3)
+    }
+}
diff --git a/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
new file mode 100644
index 0000000..2e36deb
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.saveable.lint
+
+import androidx.compose.lint.Stubs
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+// TODO: add tests for methods defined in class files when we update Lint to support bytecode()
+//  test files
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+/**
+ * Test for [RememberSaveableDetector].
+ */
+class RememberSaveableDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = RememberSaveableDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(RememberSaveableDetector.RememberSaveableSaverParameter)
+
+    private val rememberSaveableStub: TestFile = kotlin(
+        """
+        package androidx.compose.runtime.saveable
+
+        import androidx.compose.runtime.*
+
+        @Composable
+        fun <T : Any> rememberSaveable(
+            vararg inputs: Any?,
+            saver: Saver<T, out Any> = autoSaver(),
+            key: String? = null,
+            init: () -> T
+        ): T = init()
+
+        @Composable
+        fun <T> rememberSaveable(
+            vararg inputs: Any?,
+            stateSaver: Saver<T, out Any>,
+            key: String? = null,
+            init: () -> MutableState<T>
+        ): MutableState<T> = rememberSaveable(
+            *inputs,
+            saver = mutableStateSaver(stateSaver),
+            key = key,
+            init = init
+        )
+
+        interface Saver<Original, Saveable : Any>
+
+        @Suppress("UNCHECKED_CAST")
+        private fun <T> autoSaver(): Saver<T, Any> =
+            (Any() as Saver<T, Any>)
+
+        @Suppress("UNCHECKED_CAST")
+        private fun <T> mutableStateSaver(inner: Saver<T, out Any>) =
+            Any() as Saver<MutableState<T>, MutableState<Any?>>
+    """
+    )
+
+    @Test
+    fun saverPassedToVarargs() {
+        lint().files(
+            kotlin(
+                """
+                package test
+
+                import androidx.compose.runtime.*
+                import androidx.compose.runtime.saveable.*
+
+                class Foo
+                object FooSaver : Saver<Any, Any>
+                class FooSaver2 : Saver<Any, Any>
+                val fooSaver3 = object : Saver<Any, Any> {}
+                val fooSaver4 = FooSaver2()
+
+                @Composable
+                fun Test() {
+                    val foo = rememberSaveable(FooSaver) { Foo() }
+                    val mutableStateFoo = rememberSaveable(FooSaver) { mutableStateOf(Foo()) }
+                    val foo2 = rememberSaveable(FooSaver2()) { Foo() }
+                    val mutableStateFoo2 = rememberSaveable(FooSaver2()) { mutableStateOf(Foo()) }
+                    val foo3 = rememberSaveable(fooSaver3) { Foo() }
+                    val mutableStateFoo3 = rememberSaveable(fooSaver3) { mutableStateOf(Foo()) }
+                    val foo4 = rememberSaveable(fooSaver4) { Foo() }
+                    val mutableStateFoo4 = rememberSaveable(fooSaver4) { mutableStateOf(Foo()) }
+                }
+            """
+            ),
+            rememberSaveableStub,
+            kotlin(Stubs.Composable),
+            kotlin(Stubs.MutableState)
+        )
+            .run()
+            .expect(
+                """
+src/test/Foo.kt:15: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+                    val foo = rememberSaveable(FooSaver) { Foo() }
+                                               ~~~~~~~~
+src/test/Foo.kt:16: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+                    val mutableStateFoo = rememberSaveable(FooSaver) { mutableStateOf(Foo()) }
+                                                           ~~~~~~~~
+src/test/Foo.kt:17: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+                    val foo2 = rememberSaveable(FooSaver2()) { Foo() }
+                                                ~~~~~~~~~~~
+src/test/Foo.kt:18: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+                    val mutableStateFoo2 = rememberSaveable(FooSaver2()) { mutableStateOf(Foo()) }
+                                                            ~~~~~~~~~~~
+src/test/Foo.kt:19: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+                    val foo3 = rememberSaveable(fooSaver3) { Foo() }
+                                                ~~~~~~~~~
+src/test/Foo.kt:20: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+                    val mutableStateFoo3 = rememberSaveable(fooSaver3) { mutableStateOf(Foo()) }
+                                                            ~~~~~~~~~
+src/test/Foo.kt:21: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+                    val foo4 = rememberSaveable(fooSaver4) { Foo() }
+                                                ~~~~~~~~~
+src/test/Foo.kt:22: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+                    val mutableStateFoo4 = rememberSaveable(fooSaver4) { mutableStateOf(Foo()) }
+                                                            ~~~~~~~~~
+8 errors, 0 warnings
+            """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/Foo.kt line 15: Change to `saver = FooSaver`:
+@@ -15 +15
+-                     val foo = rememberSaveable(FooSaver) { Foo() }
++                     val foo = rememberSaveable(saver = FooSaver) { Foo() }
+Fix for src/test/Foo.kt line 16: Change to `stateSaver = FooSaver`:
+@@ -16 +16
+-                     val mutableStateFoo = rememberSaveable(FooSaver) { mutableStateOf(Foo()) }
++                     val mutableStateFoo = rememberSaveable(stateSaver = FooSaver) { mutableStateOf(Foo()) }
+Fix for src/test/Foo.kt line 17: Change to `saver = FooSaver2()`:
+@@ -17 +17
+-                     val foo2 = rememberSaveable(FooSaver2()) { Foo() }
++                     val foo2 = rememberSaveable(saver = FooSaver2()) { Foo() }
+Fix for src/test/Foo.kt line 18: Change to `stateSaver = FooSaver2()`:
+@@ -18 +18
+-                     val mutableStateFoo2 = rememberSaveable(FooSaver2()) { mutableStateOf(Foo()) }
++                     val mutableStateFoo2 = rememberSaveable(stateSaver = FooSaver2()) { mutableStateOf(Foo()) }
+Fix for src/test/Foo.kt line 19: Change to `saver = fooSaver3`:
+@@ -19 +19
+-                     val foo3 = rememberSaveable(fooSaver3) { Foo() }
++                     val foo3 = rememberSaveable(saver = fooSaver3) { Foo() }
+Fix for src/test/Foo.kt line 20: Change to `stateSaver = fooSaver3`:
+@@ -20 +20
+-                     val mutableStateFoo3 = rememberSaveable(fooSaver3) { mutableStateOf(Foo()) }
++                     val mutableStateFoo3 = rememberSaveable(stateSaver = fooSaver3) { mutableStateOf(Foo()) }
+Fix for src/test/Foo.kt line 21: Change to `saver = fooSaver4`:
+@@ -21 +21
+-                     val foo4 = rememberSaveable(fooSaver4) { Foo() }
++                     val foo4 = rememberSaveable(saver = fooSaver4) { Foo() }
+Fix for src/test/Foo.kt line 22: Change to `stateSaver = fooSaver4`:
+@@ -22 +22
+-                     val mutableStateFoo4 = rememberSaveable(fooSaver4) { mutableStateOf(Foo()) }
++                     val mutableStateFoo4 = rememberSaveable(stateSaver = fooSaver4) { mutableStateOf(Foo()) }
+            """
+            )
+    }
+
+    @Test
+    fun noErrors() {
+        lint().files(
+            kotlin(
+                """
+                package test
+
+                import androidx.compose.runtime.*
+                import androidx.compose.runtime.saveable.*
+
+                class Foo
+                object FooSaver : Saver<Any, Any>
+                class FooSaver2 : Saver<Any, Any>
+                val fooSaver3 = object : Saver<Any, Any> {}
+                val fooSaver4 = FooSaver2()
+
+                @Composable
+                fun Test() {
+                    val foo = rememberSaveable(saver = FooSaver) { Foo() }
+                    val mutableStateFoo = rememberSaveable(stateSaver = FooSaver) {
+                        mutableStateOf(Foo())
+                    }
+                    val foo2 = rememberSaveable(saver = FooSaver2()) { Foo() }
+                    val mutableStateFoo2 = rememberSaveable(stateSaver = FooSaver2()) {
+                        mutableStateOf(Foo())
+                    }
+                    val foo3 = rememberSaveable(saver = fooSaver3) { Foo() }
+                    val mutableStateFoo3 = rememberSaveable(stateSaver = fooSaver3) {
+                        mutableStateOf(Foo())
+                    }
+                    val foo4 = rememberSaveable(saver = fooSaver4) { Foo() }
+                    val mutableStateFoo4 = rememberSaveable(stateSaver = fooSaver4) {
+                        mutableStateOf(Foo())
+                    }
+
+                    val fooVarargs = rememberSaveable(Any(), FooSaver, Any()) { Foo() }
+                    val mutableStateFooVarargs = rememberSaveable(Any(), FooSaver, Any()) {
+                        mutableStateOf(Foo())
+                    }
+                }
+            """
+            ),
+            rememberSaveableStub,
+            kotlin(Stubs.Composable),
+            kotlin(Stubs.MutableState)
+        )
+            .run()
+            .expectClean()
+    }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index d062d34..2f53274 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -60,6 +60,8 @@
         // DexMaker has it"s own MockMaker
         androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy)
         // DexMaker has it"s own MockMaker
+
+        lintPublish(project(":compose:runtime:runtime-saveable-lint"))
     }
 }
 
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt b/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
index 2e204db..033b693 100644
--- a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
+++ b/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
@@ -382,6 +382,34 @@
 
         assertThat(actualKey).isNotEmpty()
     }
+
+    @Test
+    fun restoreCorrectValueAfterInputChanges() {
+        var counter = 0
+        var composedValue: Int? = null
+        var input by mutableStateOf(0)
+        restorationTester.setContent {
+            composedValue = rememberSaveable(input) {
+                counter++
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(composedValue).isEqualTo(0)
+            input = 1 // this will reset the state
+        }
+
+        rule.runOnIdle {
+            assertThat(composedValue).isEqualTo(1)
+            composedValue = null // clear to make sure the restoration worked
+        }
+
+        restorationTester.emulateSavedInstanceStateRestore()
+
+        rule.runOnIdle {
+            assertThat(composedValue).isEqualTo(1)
+        }
+    }
 }
 
 @Composable
diff --git a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
index e1cb14e..e1e5146 100644
--- a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
+++ b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
@@ -95,7 +95,7 @@
 
     // re-register if the registry or key has been changed
     if (registry != null) {
-        DisposableEffect(registry, finalKey) {
+        DisposableEffect(registry, finalKey, value) {
             val valueProvider = {
                 with(saverHolder.value) { SaverScope { registry.canBeSaved(it) }.save(value) }
             }
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
index 579ca44..61ff963 100644
--- a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
+++ b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
@@ -16,16 +16,13 @@
 
 package androidx.compose.runtime
 
+import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import androidx.compose.ui.test.junit4.createComposeRule
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.BroadcastChannel
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asFlow
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
@@ -34,29 +31,22 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-@ExperimentalCoroutinesApi
-@FlowPreview
 class FlowAdapterTest {
 
-    private class FlowChannel<T> {
-        val channel = BroadcastChannel<T>(1)
-        val flow = channel.asFlow()
-    }
-
     @get:Rule
     val rule = createComposeRule()
 
     @Test
     fun weReceiveSubmittedValue() {
-        val stream = FlowChannel<String>()
+        val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
 
         var realValue: String? = null
         rule.setContent {
-            realValue = stream.flow.collectAsState(initial = null).value
+            realValue = stream.collectAsState(initial = null).value
         }
 
         rule.runOnIdle {
-            stream.channel.offer("value")
+            stream.tryEmit("value")
         }
 
         rule.runOnIdle {
@@ -66,20 +56,20 @@
 
     @Test
     fun weReceiveSecondValue() {
-        val stream = FlowChannel<String>()
+        val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
 
         var realValue: String? = null
         rule.setContent {
-            realValue = stream.flow.collectAsState(initial = null).value
+            realValue = stream.collectAsState(initial = null).value
         }
 
         rule.runOnIdle {
-            stream.channel.offer("value")
+            stream.tryEmit("value")
         }
 
         rule.runOnIdle {
             assertThat(realValue).isEqualTo("value")
-            stream.channel.offer("value2")
+            stream.tryEmit("value2")
         }
 
         rule.runOnIdle {
@@ -89,19 +79,19 @@
 
     @Test
     fun noUpdatesAfterDispose() {
-        val stream = FlowChannel<String>()
+        val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
         var emit by mutableStateOf(true)
         var realValue: String? = "to-be-updated"
         rule.setContent {
             if (emit) {
-                realValue = stream.flow.collectAsState(initial = null).value
+                realValue = stream.collectAsState(initial = null).value
             }
         }
 
         rule.runOnIdle { emit = false }
 
         rule.runOnIdle {
-            stream.channel.offer("value")
+            stream.tryEmit("value")
         }
 
         rule.runOnIdle {
@@ -111,10 +101,10 @@
 
     @Test
     fun testCollectionWithInitialValue() {
-        val stream = FlowChannel<String>()
+        val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
         var realValue = "to-be-updated"
         rule.setContent {
-            realValue = stream.flow.collectAsState("value").value
+            realValue = stream.collectAsState("value").value
         }
 
         assertThat(realValue).isEqualTo("value")
@@ -122,14 +112,14 @@
 
     @Test
     fun testOverridingInitialValue() {
-        val stream = FlowChannel<String>()
+        val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
         var realValue = "to-be-updated"
         rule.setContent {
-            realValue = stream.flow.collectAsState("value").value
+            realValue = stream.collectAsState("value").value
         }
 
         rule.runOnIdle {
-            stream.channel.offer("value2")
+            stream.tryEmit("value2")
         }
 
         rule.runOnIdle {
@@ -139,12 +129,12 @@
 
     @Test
     fun theCurrentValueIsNotLostWhenWeUpdatedInitial() {
-        val stream = FlowChannel<String>()
+        val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
         var initial by mutableStateOf("initial1")
 
         var realValue: String? = null
         rule.setContent {
-            realValue = stream.flow.collectAsState(initial).value
+            realValue = stream.collectAsState(initial).value
         }
 
         rule.runOnIdle {
@@ -158,13 +148,13 @@
 
     @Test
     fun replacingStreams() {
-        val stream1 = FlowChannel<String>()
-        val stream2 = FlowChannel<String>()
+        val stream1 = MutableSharedFlow<String>(extraBufferCapacity = 1)
+        val stream2 = MutableSharedFlow<String>(extraBufferCapacity = 1)
         var stream by mutableStateOf(stream1)
 
         var realValue: String? = null
         rule.setContent {
-            realValue = stream.flow.collectAsState(initial = null).value
+            realValue = stream.collectAsState(initial = null).value
         }
 
         rule.runOnIdle {
@@ -172,11 +162,11 @@
         }
 
         rule.runOnIdle {
-            stream2.channel.offer("stream2")
+            stream2.tryEmit("stream2")
         }
 
         rule.runOnIdle {
-            stream1.channel.offer("stream1")
+            stream1.tryEmit("stream1")
         }
 
         rule.runOnIdle {
@@ -186,17 +176,17 @@
 
     @Test
     fun theCurrentValueIsNotLostWhenWeReplacedStreams() {
-        val stream1 = FlowChannel<String>()
-        val stream2 = FlowChannel<String>()
+        val stream1 = MutableSharedFlow<String>(extraBufferCapacity = 1)
+        val stream2 = MutableSharedFlow<String>(extraBufferCapacity = 1)
         var stream by mutableStateOf(stream1)
 
         var realValue: String? = null
         rule.setContent {
-            realValue = stream.flow.collectAsState(initial = null).value
+            realValue = stream.collectAsState(initial = null).value
         }
 
         rule.runOnIdle {
-            stream1.channel.offer("value")
+            stream1.tryEmit("value")
         }
 
         rule.runOnIdle {
@@ -211,15 +201,15 @@
     @Ignore("b/177256608")
     @Test
     fun observingOnCustomContext() {
-        val stream = FlowChannel<String>()
+        val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
 
         var realValue: String? = null
         rule.setContent {
-            realValue = stream.flow.collectAsState(null, Dispatchers.Default).value
+            realValue = stream.collectAsState(null, Dispatchers.Default).value
         }
 
         rule.runOnUiThread {
-            stream.channel.offer("value")
+            stream.tryEmit("value")
         }
 
         rule.waitUntil { realValue != null }
@@ -228,16 +218,16 @@
 
     @Test
     fun theCurrentValueIsNotLostWhenWeReplacedContext() {
-        val stream = FlowChannel<String>()
+        val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
         var context by mutableStateOf<CoroutineContext>(Dispatchers.Main)
 
         var realValue: String? = null
         rule.setContent {
-            realValue = stream.flow.collectAsState(null, context).value
+            realValue = stream.collectAsState(null, context).value
         }
 
         rule.runOnIdle {
-            stream.channel.offer("value")
+            stream.tryEmit("value")
         }
 
         rule.runOnIdle {
diff --git a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/FlowAdapterSamples.kt b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/FlowAdapterSamples.kt
index 8a20b7d..733a59a 100644
--- a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/FlowAdapterSamples.kt
+++ b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/FlowAdapterSamples.kt
@@ -21,7 +21,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
@@ -34,7 +33,6 @@
 
 @Sampled
 @Composable
-@ExperimentalCoroutinesApi
 fun StateFlowSample(stateFlow: StateFlow<String>) {
     val value: String by stateFlow.collectAsState()
     Text("Value is $value")
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BitwiseOperators.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BitwiseOperators.kt
index 024c2b5..6739924 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BitwiseOperators.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BitwiseOperators.kt
@@ -17,8 +17,12 @@
 @file:Suppress("NOTHING_TO_INLINE")
 package androidx.compose.runtime
 
+// NOTE: rotateRight, marked @ExperimentalStdlibApi is also marked inline-only,
+// which makes this usage stable.
 @OptIn(ExperimentalStdlibApi::class)
 internal inline infix fun Int.ror(other: Int) = this.rotateRight(other)
 
+// NOTE: rotateLeft, marked @ExperimentalStdlibApi is also marked inline-only,
+// which makes this usage stable.
 @OptIn(ExperimentalStdlibApi::class)
 internal inline infix fun Int.rol(other: Int) = this.rotateLeft(other)
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index d970e3c..558a505 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -1331,6 +1331,11 @@
             sideEffects += effect
         }
 
+        val hasEffects: Boolean
+            get() = sideEffects.isNotEmpty() ||
+                forgetting.isNotEmpty() ||
+                remembering.isNotEmpty()
+
         fun dispatchRememberObservers() {
             // Send forgets
             if (forgetting.isNotEmpty()) {
@@ -1361,11 +1366,13 @@
 
         fun dispatchAbandons() {
             if (abandoning.isNotEmpty()) {
-                val iterator = abandoning.iterator()
-                while (iterator.hasNext()) {
-                    val instance = iterator.next()
-                    iterator.remove()
-                    instance.onAbandoned()
+                trace("Compose:dispatchAbandons") {
+                    val iterator = abandoning.iterator()
+                    while (iterator.hasNext()) {
+                        val instance = iterator.next()
+                        iterator.remove()
+                        instance.onAbandoned()
+                    }
                 }
             }
         }
@@ -1410,8 +1417,12 @@
                 // Side effects run after lifecycle observers so that any remembered objects
                 // that implement RememberObserver receive onRemembered before a side effect
                 // that captured it and operates on it can run.
-                manager.dispatchRememberObservers()
-                manager.dispatchSideEffects()
+                if (manager.hasEffects) {
+                    trace("Compose:dispatchEffects") {
+                        manager.dispatchRememberObservers()
+                        manager.dispatchSideEffects()
+                    }
+                }
 
                 if (pendingInvalidScopes) {
                     pendingInvalidScopes = false
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 3da5ece..14629c8 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -413,10 +413,10 @@
             // each time, but because we've installed the broadcastFrameClock as the scope
             // clock above for user code to locate.
             parentFrameClock.withFrameNanos { frameTime ->
-                trace("recomposeFrame") {
-                    // Dispatch MonotonicFrameClock frames first; this may produce new
-                    // composer invalidations that we must handle during the same frame.
-                    if (broadcastFrameClock.hasAwaiters) {
+                // Dispatch MonotonicFrameClock frames first; this may produce new
+                // composer invalidations that we must handle during the same frame.
+                if (broadcastFrameClock.hasAwaiters) {
+                    trace("Recomposer:animation") {
                         // Propagate the frame time to anyone who is awaiting from the
                         // recomposer clock.
                         broadcastFrameClock.sendFrame(frameTime)
@@ -424,7 +424,9 @@
                         // Ensure any global changes are observed
                         Snapshot.sendApplyNotifications()
                     }
+                }
 
+                trace("Recomposer:recompose") {
                     // Drain any composer invalidations from snapshot changes and record
                     // composers to work on
                     synchronized(stateLock) {
@@ -531,10 +533,10 @@
             // each time, but because we've installed the broadcastFrameClock as the scope
             // clock above for user code to locate.
             parentFrameClock.withFrameNanos { frameTime ->
-                trace("recomposeFrame") {
-                    // Dispatch MonotonicFrameClock frames first; this may produce new
-                    // composer invalidations that we must handle during the same frame.
-                    if (broadcastFrameClock.hasAwaiters) {
+                // Dispatch MonotonicFrameClock frames first; this may produce new
+                // composer invalidations that we must handle during the same frame.
+                if (broadcastFrameClock.hasAwaiters) {
+                    trace("Recomposer:animation") {
                         // Propagate the frame time to anyone who is awaiting from the
                         // recomposer clock.
                         broadcastFrameClock.sendFrame(frameTime)
@@ -542,7 +544,9 @@
                         // Ensure any global changes are observed
                         Snapshot.sendApplyNotifications()
                     }
+                }
 
+                trace("Recomposer:recompose") {
                     // Drain any composer invalidations from snapshot changes and record
                     // composers to work on.
                     // We'll do these synchronously to make the current frame.
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
index 4307a69..3c49b27 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-@file:Suppress("UNCHECKED_CAST")
 @file:OptIn(InternalComposeApi::class)
 package androidx.compose.runtime.internal
 
@@ -47,173 +46,42 @@
 @Stable
 @OptIn(ComposeCompilerApi::class)
 /* ktlint-disable parameter-list-wrapping */ // TODO(https://github.com/pinterest/ktlint/issues/921): reenable
-internal class ComposableLambdaImpl(
-    val key: Int,
-    private val tracked: Boolean,
-    private val sourceInformation: String?
+internal expect class ComposableLambdaImpl(
+    key: Int,
+    tracked: Boolean,
+    sourceInformation: String?
 ) : ComposableLambda {
-    private var _block: Any? = null
-    private var scope: RecomposeScope? = null
-    private var scopes: MutableList<RecomposeScope>? = null
+    fun update(block: Any)
+}
 
-    private fun trackWrite() {
-        if (tracked) {
-            val scope = this.scope
-            if (scope != null) {
-                scope.invalidate()
-                this.scope = null
-            }
-            val scopes = this.scopes
-            if (scopes != null) {
-                for (index in 0 until scopes.size) {
-                    val item = scopes[index]
-                    item.invalidate()
-                }
-                scopes.clear()
-            }
-        }
-    }
-
-    private fun trackRead(composer: Composer) {
-        if (tracked) {
-            val scope = composer.recomposeScope
-            if (scope != null) {
-                // Find the first invalid scope and replace it or record it if no scopes are invalid
-                composer.recordUsed(scope)
-                val lastScope = this.scope
-                if (lastScope.replacableWith(scope)) {
-                    this.scope = scope
-                } else {
-                    val lastScopes = scopes
-                    if (lastScopes == null) {
-                        val newScopes = mutableListOf<RecomposeScope>()
-                        scopes = newScopes
-                        newScopes.add(scope)
-                    } else {
-                        for (index in 0 until lastScopes.size) {
-                            val scopeAtIndex = lastScopes[index]
-                            if (scopeAtIndex.replacableWith(scope)) {
-                                lastScopes[index] = scope
-                                return
-                            }
-                        }
-                        lastScopes.add(scope)
-                    }
-                }
-            }
-        }
-    }
-
-    fun update(block: Any) {
-        if (_block != block) {
-            val oldBlockNull = _block == null
-            _block = block
-            if (!oldBlockNull) {
-                trackWrite()
-            }
-        }
-    }
-
-    override operator fun invoke(c: Composer, changed: Int): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(0) else sameBits(0)
-        val result = (_block as (c: Composer, changed: Int) -> Any?)(c, dirty)
-        c.endRestartGroup()?.updateScope(this as (Composer, Int) -> Unit)
-        return result
-    }
-
-    override operator fun invoke(p1: Any?, c: Composer, changed: Int): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(1) else sameBits(1)
-        val result = (
-            _block as (
-                p1: Any?,
-                c: Composer,
-                changed: Int
-            ) -> Any?
-            )(
-            p1,
-            c,
-            dirty
+internal fun RecomposeScope?.replacableWith(other: RecomposeScope) =
+    this == null || (
+        this is RecomposeScopeImpl && other is RecomposeScopeImpl && (
+            !this.valid || this == other || this.anchor == other.anchor
+            )
         )
-        c.endRestartGroup()?.updateScope { nc, _ -> this(p1, nc, changed or 0b1) }
-        return result
-    }
 
-    override operator fun invoke(p1: Any?, p2: Any?, c: Composer, changed: Int): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(2) else sameBits(2)
-        val result = (_block as (p1: Any?, p2: Any?, c: Composer, changed: Int) -> Any?)(
-            p1,
-            p2,
-            c,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ -> this(p1, p2, nc, changed or 0b1) }
-        return result
-    }
+@ComposeCompilerApi
+@Stable
+expect interface ComposableLambda {
+    operator fun invoke(c: Composer, changed: Int): Any?
 
-    override operator fun invoke(p1: Any?, p2: Any?, p3: Any?, c: Composer, changed: Int): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(3) else sameBits(3)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                c: Composer,
-                changed: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            c,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ -> this(p1, p2, p3, nc, changed or 0b1) }
-        return result
-    }
+    operator fun invoke(p1: Any?, c: Composer, changed: Int): Any?
 
-    override operator fun invoke(
+    operator fun invoke(p1: Any?, p2: Any?, c: Composer, changed: Int): Any?
+
+    operator fun invoke(p1: Any?, p2: Any?, p3: Any?, c: Composer, changed: Int): Any?
+
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
         p4: Any?,
         c: Composer,
         changed: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(4) else sameBits(4)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                c: Composer,
-                changed: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            c,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, nc, changed or 0b1)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -221,36 +89,9 @@
         p5: Any?,
         c: Composer,
         changed: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(5) else sameBits(5)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                c: Composer,
-                changed: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            c,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, p5, nc, changed or 0b1)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -259,38 +100,9 @@
         p6: Any?,
         c: Composer,
         changed: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(6) else sameBits(6)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                c: Composer,
-                changed: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            c,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, p5, p6, nc, changed or 0b1)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -300,40 +112,9 @@
         p7: Any?,
         c: Composer,
         changed: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(7) else sameBits(7)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                c: Composer,
-                changed: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            c,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, p5, p6, p7, nc, changed or 0b1)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -344,42 +125,9 @@
         p8: Any?,
         c: Composer,
         changed: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(8) else sameBits(8)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                c: Composer,
-                changed: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            c,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, p5, p6, p7, p8, nc, changed or 0b1)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -391,44 +139,9 @@
         p9: Any?,
         c: Composer,
         changed: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed or if (c.changed(this)) differentBits(9) else sameBits(9)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                c: Composer,
-                changed: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            c,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, p5, p6, p7, p8, p9, nc, changed or 0b1)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -442,48 +155,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(10) else sameBits(10)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, nc, changed or 0b1, changed)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -498,50 +172,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(11) else sameBits(11)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                p11: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            p11,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, nc, changed or 0b1, changed1)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -557,52 +190,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(12) else sameBits(12)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                p11: Any?,
-                p12: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            p11,
-            p12,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, nc, changed or 0b1, changed1)
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -619,71 +209,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(13) else sameBits(13)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                p11: Any?,
-                p12: Any?,
-                p13: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            p11,
-            p12,
-            p13,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(
-                p1,
-                p2,
-                p3,
-                p4,
-                p5,
-                p6,
-                p7,
-                p8,
-                p9,
-                p10,
-                p11,
-                p12,
-                p13,
-                nc,
-                changed or 0b1,
-                changed1
-            )
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -701,74 +229,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(14) else sameBits(14)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                p11: Any?,
-                p12: Any?,
-                p13: Any?,
-                p14: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            p11,
-            p12,
-            p13,
-            p14,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(
-                p1,
-                p2,
-                p3,
-                p4,
-                p5,
-                p6,
-                p7,
-                p8,
-                p9,
-                p10,
-                p11,
-                p12,
-                p13,
-                p14,
-                nc,
-                changed or 0b1,
-                changed1
-            )
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -787,77 +250,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(15) else sameBits(15)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                p11: Any?,
-                p12: Any?,
-                p13: Any?,
-                p14: Any?,
-                p15: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            p11,
-            p12,
-            p13,
-            p14,
-            p15,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(
-                p1,
-                p2,
-                p3,
-                p4,
-                p5,
-                p6,
-                p7,
-                p8,
-                p9,
-                p10,
-                p11,
-                p12,
-                p13,
-                p14,
-                p15,
-                nc,
-                changed or 0b1,
-                changed1
-            )
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -877,80 +272,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(16) else sameBits(16)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                p11: Any?,
-                p12: Any?,
-                p13: Any?,
-                p14: Any?,
-                p15: Any?,
-                p16: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            p11,
-            p12,
-            p13,
-            p14,
-            p15,
-            p16,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(
-                p1,
-                p2,
-                p3,
-                p4,
-                p5,
-                p6,
-                p7,
-                p8,
-                p9,
-                p10,
-                p11,
-                p12,
-                p13,
-                p14,
-                p15,
-                p16,
-                nc,
-                changed or 0b1,
-                changed1
-            )
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -971,83 +295,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(17) else sameBits(17)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                p11: Any?,
-                p12: Any?,
-                p13: Any?,
-                p14: Any?,
-                p15: Any?,
-                p16: Any?,
-                p17: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            p11,
-            p12,
-            p13,
-            p14,
-            p15,
-            p16,
-            p17,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(
-                p1,
-                p2,
-                p3,
-                p4,
-                p5,
-                p6,
-                p7,
-                p8,
-                p9,
-                p10,
-                p11,
-                p12,
-                p13,
-                p14,
-                p15,
-                p16,
-                p17,
-                nc,
-                changed or 0b1,
-                changed1
-            )
-        }
-        return result
-    }
+    ): Any?
 
-    override operator fun invoke(
+    operator fun invoke(
         p1: Any?,
         p2: Any?,
         p3: Any?,
@@ -1069,126 +319,9 @@
         c: Composer,
         changed: Int,
         changed1: Int
-    ): Any? {
-        val c = c.startRestartGroup(key, sourceInformation)
-        trackRead(c)
-        val dirty = changed1 or if (c.changed(this)) differentBits(18) else sameBits(18)
-        val result = (
-            _block as (
-                p1: Any?,
-                p2: Any?,
-                p3: Any?,
-                p4: Any?,
-                p5: Any?,
-                p6: Any?,
-                p7: Any?,
-                p8: Any?,
-                p9: Any?,
-                p10: Any?,
-                p11: Any?,
-                p12: Any?,
-                p13: Any?,
-                p14: Any?,
-                p15: Any?,
-                p16: Any?,
-                p17: Any?,
-                p18: Any?,
-                c: Composer,
-                changed: Int,
-                changed1: Int
-            ) -> Any?
-            )(
-            p1,
-            p2,
-            p3,
-            p4,
-            p5,
-            p6,
-            p7,
-            p8,
-            p9,
-            p10,
-            p11,
-            p12,
-            p13,
-            p14,
-            p15,
-            p16,
-            p17,
-            p18,
-            c,
-            changed,
-            dirty
-        )
-        c.endRestartGroup()?.updateScope { nc, _ ->
-            this(
-                p1,
-                p2,
-                p3,
-                p4,
-                p5,
-                p6,
-                p7,
-                p8,
-                p9,
-                p10,
-                p11,
-                p12,
-                p13,
-                p14,
-                p15,
-                p16,
-                p17,
-                p18,
-                nc,
-                changed or 0b1,
-                changed1
-            )
-        }
-        return result
-    }
+    ): Any?
 }
 
-private fun RecomposeScope?.replacableWith(other: RecomposeScope) =
-    this == null || (
-        this is RecomposeScopeImpl && other is RecomposeScopeImpl && (
-            !this.valid || this == other || this.anchor == other.anchor
-            )
-        )
-
-@ComposeCompilerApi
-@Stable
-interface ComposableLambda :
-    Function2<Composer, Int, Any?>,
-    Function3<Any?, Composer, Int, Any?>,
-    Function4<Any?, Any?, Composer, Int, Any?>,
-    Function5<Any?, Any?, Any?, Composer, Int, Any?>,
-    Function6<Any?, Any?, Any?, Any?, Composer, Int, Any?>,
-    Function7<Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
-    Function8<Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
-    Function9<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
-    Function10<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
-    Function11<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
-    Function13<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Int,
-        Any?>,
-    Function14<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Int,
-        Any?>,
-    Function15<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer,
-        Int, Int, Any?>,
-    Function16<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
-        Composer, Int, Int, Any?>,
-    Function17<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
-        Composer, Int,
-        Int, Any?>,
-    Function18<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
-        Any?, Composer, Int, Int, Any?>,
-    Function19<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
-        Any?, Any?, Composer, Int, Int, Any?>,
-    Function20<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
-        Any?, Any?, Any?, Composer, Int, Int, Any?>,
-    Function21<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
-        Any?, Any?, Any?, Any?, Composer, Int, Int, Any?>
-
 @Suppress("unused")
 @ComposeCompilerApi
 fun composableLambda(
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.jvm.kt
new file mode 100644
index 0000000..8fe6f28
--- /dev/null
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.jvm.kt
@@ -0,0 +1,1171 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UNCHECKED_CAST")
+@file:OptIn(InternalComposeApi::class)
+package androidx.compose.runtime.internal
+
+import androidx.compose.runtime.ComposeCompilerApi
+import androidx.compose.runtime.Composer
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.RecomposeScope
+import androidx.compose.runtime.Stable
+
+/**
+ * A Restart is created to hold composable lambdas to track when they are invoked allowing
+ * the invocations to be invalidated when a new composable lambda is created during composition.
+ *
+ * This allows much of the call-graph to be skipped when a composable function is passed through
+ * multiple levels of composable functions.
+ */
+@Suppress("NAME_SHADOWING")
+@Stable
+@OptIn(ComposeCompilerApi::class)
+/* ktlint-disable parameter-list-wrapping */ // TODO(https://github.com/pinterest/ktlint/issues/921): reenable
+internal actual class ComposableLambdaImpl actual constructor(
+    val key: Int,
+    private val tracked: Boolean,
+    private val sourceInformation: String?
+) : ComposableLambda {
+    private var _block: Any? = null
+    private var scope: RecomposeScope? = null
+    private var scopes: MutableList<RecomposeScope>? = null
+
+    private fun trackWrite() {
+        if (tracked) {
+            val scope = this.scope
+            if (scope != null) {
+                scope.invalidate()
+                this.scope = null
+            }
+            val scopes = this.scopes
+            if (scopes != null) {
+                for (index in 0 until scopes.size) {
+                    val item = scopes[index]
+                    item.invalidate()
+                }
+                scopes.clear()
+            }
+        }
+    }
+
+    private fun trackRead(composer: Composer) {
+        if (tracked) {
+            val scope = composer.recomposeScope
+            if (scope != null) {
+                // Find the first invalid scope and replace it or record it if no scopes are invalid
+                composer.recordUsed(scope)
+                val lastScope = this.scope
+                if (lastScope.replacableWith(scope)) {
+                    this.scope = scope
+                } else {
+                    val lastScopes = scopes
+                    if (lastScopes == null) {
+                        val newScopes = mutableListOf<RecomposeScope>()
+                        scopes = newScopes
+                        newScopes.add(scope)
+                    } else {
+                        for (index in 0 until lastScopes.size) {
+                            val scopeAtIndex = lastScopes[index]
+                            if (scopeAtIndex.replacableWith(scope)) {
+                                lastScopes[index] = scope
+                                return
+                            }
+                        }
+                        lastScopes.add(scope)
+                    }
+                }
+            }
+        }
+    }
+
+    actual fun update(block: Any) {
+        if (_block != block) {
+            val oldBlockNull = _block == null
+            _block = block
+            if (!oldBlockNull) {
+                trackWrite()
+            }
+        }
+    }
+
+    override operator fun invoke(c: Composer, changed: Int): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(0) else sameBits(0)
+        val result = (_block as (c: Composer, changed: Int) -> Any?)(c, dirty)
+        c.endRestartGroup()?.updateScope(this as (Composer, Int) -> Unit)
+        return result
+    }
+
+    override operator fun invoke(p1: Any?, c: Composer, changed: Int): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(1) else sameBits(1)
+        val result = (
+            _block as (
+                p1: Any?,
+                c: Composer,
+                changed: Int
+            ) -> Any?
+            )(
+            p1,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ -> this(p1, nc, changed or 0b1) }
+        return result
+    }
+
+    override operator fun invoke(p1: Any?, p2: Any?, c: Composer, changed: Int): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(2) else sameBits(2)
+        val result = (_block as (p1: Any?, p2: Any?, c: Composer, changed: Int) -> Any?)(
+            p1,
+            p2,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ -> this(p1, p2, nc, changed or 0b1) }
+        return result
+    }
+
+    override operator fun invoke(p1: Any?, p2: Any?, p3: Any?, c: Composer, changed: Int): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(3) else sameBits(3)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                c: Composer,
+                changed: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ -> this(p1, p2, p3, nc, changed or 0b1) }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        c: Composer,
+        changed: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(4) else sameBits(4)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                c: Composer,
+                changed: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, nc, changed or 0b1)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        c: Composer,
+        changed: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(5) else sameBits(5)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                c: Composer,
+                changed: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, p5, nc, changed or 0b1)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        c: Composer,
+        changed: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(6) else sameBits(6)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                c: Composer,
+                changed: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, p5, p6, nc, changed or 0b1)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        c: Composer,
+        changed: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(7) else sameBits(7)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                c: Composer,
+                changed: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, p5, p6, p7, nc, changed or 0b1)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        c: Composer,
+        changed: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(8) else sameBits(8)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                c: Composer,
+                changed: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, p5, p6, p7, p8, nc, changed or 0b1)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        c: Composer,
+        changed: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed or if (c.changed(this)) differentBits(9) else sameBits(9)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                c: Composer,
+                changed: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            c,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, p5, p6, p7, p8, p9, nc, changed or 0b1)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(10) else sameBits(10)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, nc, changed or 0b1, changed)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        p11: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(11) else sameBits(11)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                p11: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            p11,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, nc, changed or 0b1, changed1)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        p11: Any?,
+        p12: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(12) else sameBits(12)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                p11: Any?,
+                p12: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            p11,
+            p12,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, nc, changed or 0b1, changed1)
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        p11: Any?,
+        p12: Any?,
+        p13: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(13) else sameBits(13)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                p11: Any?,
+                p12: Any?,
+                p13: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            p11,
+            p12,
+            p13,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(
+                p1,
+                p2,
+                p3,
+                p4,
+                p5,
+                p6,
+                p7,
+                p8,
+                p9,
+                p10,
+                p11,
+                p12,
+                p13,
+                nc,
+                changed or 0b1,
+                changed1
+            )
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        p11: Any?,
+        p12: Any?,
+        p13: Any?,
+        p14: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(14) else sameBits(14)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                p11: Any?,
+                p12: Any?,
+                p13: Any?,
+                p14: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            p11,
+            p12,
+            p13,
+            p14,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(
+                p1,
+                p2,
+                p3,
+                p4,
+                p5,
+                p6,
+                p7,
+                p8,
+                p9,
+                p10,
+                p11,
+                p12,
+                p13,
+                p14,
+                nc,
+                changed or 0b1,
+                changed1
+            )
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        p11: Any?,
+        p12: Any?,
+        p13: Any?,
+        p14: Any?,
+        p15: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(15) else sameBits(15)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                p11: Any?,
+                p12: Any?,
+                p13: Any?,
+                p14: Any?,
+                p15: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            p11,
+            p12,
+            p13,
+            p14,
+            p15,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(
+                p1,
+                p2,
+                p3,
+                p4,
+                p5,
+                p6,
+                p7,
+                p8,
+                p9,
+                p10,
+                p11,
+                p12,
+                p13,
+                p14,
+                p15,
+                nc,
+                changed or 0b1,
+                changed1
+            )
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        p11: Any?,
+        p12: Any?,
+        p13: Any?,
+        p14: Any?,
+        p15: Any?,
+        p16: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(16) else sameBits(16)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                p11: Any?,
+                p12: Any?,
+                p13: Any?,
+                p14: Any?,
+                p15: Any?,
+                p16: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            p11,
+            p12,
+            p13,
+            p14,
+            p15,
+            p16,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(
+                p1,
+                p2,
+                p3,
+                p4,
+                p5,
+                p6,
+                p7,
+                p8,
+                p9,
+                p10,
+                p11,
+                p12,
+                p13,
+                p14,
+                p15,
+                p16,
+                nc,
+                changed or 0b1,
+                changed1
+            )
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        p11: Any?,
+        p12: Any?,
+        p13: Any?,
+        p14: Any?,
+        p15: Any?,
+        p16: Any?,
+        p17: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(17) else sameBits(17)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                p11: Any?,
+                p12: Any?,
+                p13: Any?,
+                p14: Any?,
+                p15: Any?,
+                p16: Any?,
+                p17: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            p11,
+            p12,
+            p13,
+            p14,
+            p15,
+            p16,
+            p17,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(
+                p1,
+                p2,
+                p3,
+                p4,
+                p5,
+                p6,
+                p7,
+                p8,
+                p9,
+                p10,
+                p11,
+                p12,
+                p13,
+                p14,
+                p15,
+                p16,
+                p17,
+                nc,
+                changed or 0b1,
+                changed1
+            )
+        }
+        return result
+    }
+
+    override operator fun invoke(
+        p1: Any?,
+        p2: Any?,
+        p3: Any?,
+        p4: Any?,
+        p5: Any?,
+        p6: Any?,
+        p7: Any?,
+        p8: Any?,
+        p9: Any?,
+        p10: Any?,
+        p11: Any?,
+        p12: Any?,
+        p13: Any?,
+        p14: Any?,
+        p15: Any?,
+        p16: Any?,
+        p17: Any?,
+        p18: Any?,
+        c: Composer,
+        changed: Int,
+        changed1: Int
+    ): Any? {
+        val c = c.startRestartGroup(key, sourceInformation)
+        trackRead(c)
+        val dirty = changed1 or if (c.changed(this)) differentBits(18) else sameBits(18)
+        val result = (
+            _block as (
+                p1: Any?,
+                p2: Any?,
+                p3: Any?,
+                p4: Any?,
+                p5: Any?,
+                p6: Any?,
+                p7: Any?,
+                p8: Any?,
+                p9: Any?,
+                p10: Any?,
+                p11: Any?,
+                p12: Any?,
+                p13: Any?,
+                p14: Any?,
+                p15: Any?,
+                p16: Any?,
+                p17: Any?,
+                p18: Any?,
+                c: Composer,
+                changed: Int,
+                changed1: Int
+            ) -> Any?
+            )(
+            p1,
+            p2,
+            p3,
+            p4,
+            p5,
+            p6,
+            p7,
+            p8,
+            p9,
+            p10,
+            p11,
+            p12,
+            p13,
+            p14,
+            p15,
+            p16,
+            p17,
+            p18,
+            c,
+            changed,
+            dirty
+        )
+        c.endRestartGroup()?.updateScope { nc, _ ->
+            this(
+                p1,
+                p2,
+                p3,
+                p4,
+                p5,
+                p6,
+                p7,
+                p8,
+                p9,
+                p10,
+                p11,
+                p12,
+                p13,
+                p14,
+                p15,
+                p16,
+                p17,
+                p18,
+                nc,
+                changed or 0b1,
+                changed1
+            )
+        }
+        return result
+    }
+}
+
+@ComposeCompilerApi
+@Stable
+actual interface ComposableLambda :
+    Function2<Composer, Int, Any?>,
+    Function3<Any?, Composer, Int, Any?>,
+    Function4<Any?, Any?, Composer, Int, Any?>,
+    Function5<Any?, Any?, Any?, Composer, Int, Any?>,
+    Function6<Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+    Function7<Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+    Function8<Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+    Function9<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+    Function10<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+    Function11<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+    Function13<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Int,
+        Any?>,
+    Function14<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Int,
+        Any?>,
+    Function15<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer,
+        Int, Int, Any?>,
+    Function16<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+        Composer, Int, Int, Any?>,
+    Function17<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+        Composer, Int,
+        Int, Any?>,
+    Function18<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+        Any?, Composer, Int, Int, Any?>,
+    Function19<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+        Any?, Any?, Composer, Int, Int, Any?>,
+    Function20<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+        Any?, Any?, Any?, Composer, Int, Int, Any?>,
+    Function21<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+        Any?, Any?, Any?, Any?, Composer, Int, Int, Any?>
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionLocalTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
index a5745b8..f2dc064 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
@@ -450,36 +450,38 @@
         validate()
     }
 
+    @Composable
+    fun ReadSomeDataCompositionLocal(
+        compositionLocal: CompositionLocal<SomeData>,
+        composed: StableRef<Boolean>,
+    ) {
+        composed.value = true
+        Text(value = compositionLocal.current.value)
+    }
+
     @Test
     fun providingANewDataClassValueShouldNotRecompose() = compositionTest {
         val invalidates = mutableListOf<RecomposeScope>()
         fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
         val someDataCompositionLocal = compositionLocalOf(structuralEqualityPolicy()) { SomeData() }
-        var composed = false
-
-        @Composable
-        fun ReadSomeDataCompositionLocal(
-            compositionLocal: CompositionLocal<SomeData>
-        ) {
-            composed = true
-            Text(value = compositionLocal.current.value)
-        }
+        val composed = StableRef(false)
 
         compose {
             invalidates.add(currentRecomposeScope)
             CompositionLocalProvider(
                 someDataCompositionLocal provides SomeData("provided")
             ) {
-                ReadSomeDataCompositionLocal(someDataCompositionLocal)
+                ReadSomeDataCompositionLocal(someDataCompositionLocal, composed)
             }
         }
 
-        assertTrue(composed)
-        composed = false
+        assertTrue(composed.value)
+        composed.value = false
         doInvalidate()
         expectNoChanges()
-        assertFalse(composed)
+        assertFalse(composed.value)
     }
 }
 
-private data class SomeData(val value: String = "default")
\ No newline at end of file
+data class SomeData(val value: String = "default")
+@Stable class StableRef<T>(var value: T)
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LatchTest.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LatchTest.kt
index 807feef..7e8c149 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LatchTest.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LatchTest.kt
@@ -17,15 +17,12 @@
 package androidx.compose.runtime
 
 import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withTimeout
 import kotlin.test.Test
 import kotlin.test.assertTrue
 
-// OptIn for CoroutineStart.UNDISPATCHED
-@OptIn(ExperimentalCoroutinesApi::class)
 class LatchTest {
     @Test
     fun openDoesntSuspend() = runBlocking {
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/NewCodeGenTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/NewCodeGenTests.kt
index d69da23..012f6ee 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/NewCodeGenTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/NewCodeGenTests.kt
@@ -117,51 +117,50 @@
         validate()
     }
 
+    @Composable
+    fun PhoneView(phone: Phone, phoneCalled: StableCounter) {
+        phoneCalled.count++
+        Text(
+            "${if (phone.area.isBlank()) "" else "(${phone.area}) "}${
+            phone.prefix}-${phone.number}"
+        )
+    }
+
     @Test
     fun testComposableFunctionInvocationOneParameter() = compositionTest {
-        data class Phone(val area: String, val prefix: String, val number: String)
-
         var phone by mutableStateOf(Phone("123", "456", "7890"))
-        var phoneCalled = 0
+        val phoneCalled = StableCounter()
         var scope: RecomposeScope? = null
         compose {
-            @Composable
-            fun PhoneView(phone: Phone) {
-                phoneCalled++
-                Text(
-                    "${if (phone.area.isBlank()) "" else "(${phone.area}) "}${
-                    phone.prefix}-${phone.number}"
-                )
-            }
             scope = currentRecomposeScope
-            PhoneView(phone)
+            PhoneView(phone, phoneCalled)
         }
 
-        assertEquals(1, phoneCalled)
+        assertEquals(1, phoneCalled.count)
         scope?.invalidate()
         advance()
-        assertEquals(1, phoneCalled)
+        assertEquals(1, phoneCalled.count)
 
         phone = Phone("124", "456", "7890")
         advance()
-        assertEquals(2, phoneCalled)
+        assertEquals(2, phoneCalled.count)
+    }
+
+    @Composable
+    fun AddView(left: Int, right: Int, addCalled: StableCounter) {
+        addCalled.count++
+        Text("$left + $right = ${left + right}")
     }
 
     @Test
     fun testComposableFunctionInvocationTwoParameters() = compositionTest {
         var left by mutableStateOf(0)
         var right by mutableStateOf(1)
-        var addCalled = 0
+        val addCalled = StableCounter()
         var scope: RecomposeScope? = null
         compose {
-            @Composable
-            fun AddView(left: Int, right: Int) {
-                addCalled++
-                Text("$left + $right = ${left + right}")
-            }
-
             scope = currentRecomposeScope
-            AddView(left, right)
+            AddView(left, right, addCalled)
         }
 
         fun validate() {
@@ -170,27 +169,27 @@
             }
         }
         validate()
-        assertEquals(1, addCalled)
+        assertEquals(1, addCalled.count)
 
         scope?.invalidate()
         advance()
         validate()
-        assertEquals(1, addCalled)
+        assertEquals(1, addCalled.count)
 
         left = 1
         advance()
         validate()
-        assertEquals(2, addCalled)
+        assertEquals(2, addCalled.count)
 
         scope?.invalidate()
         advance()
         validate()
-        assertEquals(2, addCalled)
+        assertEquals(2, addCalled.count)
 
         right = 41
         advance()
         validate()
-        assertEquals(3, addCalled)
+        assertEquals(3, addCalled.count)
     }
 
     @Test
@@ -220,4 +219,9 @@
         advance()
         validate()
     }
-}
\ No newline at end of file
+}
+
+@Stable
+class StableCounter(var count: Int = 0)
+
+data class Phone(val area: String, val prefix: String, val number: String)
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
index bc460846..40425a0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
@@ -97,7 +97,8 @@
     /**
      * An area defined as a path.
      *
-     * Note that only convex paths can be used for drawing the shadow. See [Path.isConvex].
+     * Note that if you use this path for drawing the shadow on Android versions less than 10 the
+     * shadow will not be drawn for the concave paths. See [Path.isConvex].
      */
     class Generic(val path: Path) : Outline() {
         override val bounds: Rect
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
index 6954ac7..01317e9 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
@@ -76,7 +76,6 @@
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -94,11 +93,6 @@
 @RunWith(AndroidJUnit4::class)
 class ParameterFactoryTest {
     private val factory = ParameterFactory(InlineClassConverter())
-    private val node = MutableInspectorNode().apply {
-        width = 1000
-        height = 500
-        id = NODE_ID
-    }.build()
 
     @Before
     fun before() {
@@ -258,7 +252,6 @@
         )
     }
 
-    @Ignore
     @Test
     fun testCornerBasedShape() {
         validate(create("corner", RoundedCornerShape(2.0.dp, 0.5.dp, 2.5.dp, 0.7.dp))) {
@@ -271,18 +264,18 @@
         }
         validate(create("corner", CutCornerShape(2))) {
             parameter("corner", ParameterType.String, CutCornerShape::class.java.simpleName) {
-                parameter("bottomEnd", ParameterType.DimensionDp, 5.0f)
-                parameter("bottomStart", ParameterType.DimensionDp, 5.0f)
-                parameter("topEnd", ParameterType.DimensionDp, 5.0f)
-                parameter("topStart", ParameterType.DimensionDp, 5.0f)
+                parameter("bottomEnd", ParameterType.String, "2.0%")
+                parameter("bottomStart", ParameterType.String, "2.0%")
+                parameter("topEnd", ParameterType.String, "2.0%")
+                parameter("topStart", ParameterType.String, "2.0%")
             }
         }
         validate(create("corner", RoundedCornerShape(1.0f, 10.0f, 2.0f, 3.5f))) {
             parameter("corner", ParameterType.String, RoundedCornerShape::class.java.simpleName) {
-                parameter("bottomEnd", ParameterType.DimensionDp, 1.0f)
-                parameter("bottomStart", ParameterType.DimensionDp, 1.75f)
-                parameter("topEnd", ParameterType.DimensionDp, 5.0f)
-                parameter("topStart", ParameterType.DimensionDp, 0.5f)
+                parameter("bottomEnd", ParameterType.String, "2.0px")
+                parameter("bottomStart", ParameterType.String, "3.5px")
+                parameter("topEnd", ParameterType.String, "10.0px")
+                parameter("topStart", ParameterType.String, "1.0px")
             }
         }
     }
@@ -431,6 +424,66 @@
     }
 
     @Test
+    fun testMap() {
+        val map = mapOf(1 to "one", 2 to "two")
+        validate(create("map", map)) {
+            parameter("map", ParameterType.Iterable, "Map[2]") {
+                parameter("[1]", ParameterType.String, "one") {
+                    parameter("key", ParameterType.Int32, 1)
+                    parameter("value", ParameterType.String, "one")
+                }
+                parameter("[2]", ParameterType.String, "two") {
+                    parameter("key", ParameterType.Int32, 2)
+                    parameter("value", ParameterType.String, "two")
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testMapEntry() {
+        val entry = object : Map.Entry<String, String> {
+            override val key = "Hello"
+            override val value = "World"
+        }
+        validate(create("myEntry", entry)) {
+            parameter("myEntry", ParameterType.String, "World") {
+                parameter("key", ParameterType.String, "Hello")
+                parameter("value", ParameterType.String, "World")
+            }
+        }
+    }
+
+    @Test
+    fun testMapWithComplexTypes() {
+        val k1 = MyClass("k1")
+        val k2 = MyClass("k2")
+        val v1 = MyClass("v1")
+        val v2 = MyClass("v2")
+        val map = mapOf(k1 to v1, k2 to v2)
+        validate(create("map", map, maxRecursions = 3)) {
+            parameter("map", ParameterType.Iterable, "Map[2]") {
+                parameter("[MyClass]", ParameterType.String, "MyClass") {
+                    parameter("key", ParameterType.String, "MyClass") {
+                        parameter("name", ParameterType.String, "k1")
+                    }
+                    parameter("value", ParameterType.String, "MyClass") {
+                        parameter("name", ParameterType.String, "v1")
+                    }
+                }
+                parameter("[MyClass]", ParameterType.String, "MyClass") {
+                    parameter("key", ParameterType.String, "MyClass") {
+                        parameter("name", ParameterType.String, "k2")
+                    }
+                    parameter("value", ParameterType.String, "MyClass") {
+                        parameter("name", ParameterType.String, "v2")
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
     fun testShortIntArray() {
         val value = intArrayOf(10, 11, 12)
         val parameter = create("array", value)
@@ -836,7 +889,7 @@
     ): NodeParameter {
         val parameter = factory.create(
             ROOT_ID,
-            node,
+            NODE_ID,
             name,
             value,
             PARAM_INDEX,
@@ -869,7 +922,7 @@
     ): NodeParameter? =
         factory.expand(
             ROOT_ID,
-            node,
+            NODE_ID,
             name,
             value,
             reference,
@@ -980,7 +1033,7 @@
         val msg = "$trace${parameter.name}"
         assertWithMessage(msg).that(parameter.type).isEqualTo(type)
         assertWithMessage(msg).that(parameter.index).isEqualTo(expectedIndex)
-        assertWithMessage(msg).that(checkEquals(parameter.reference, ref)).isTrue()
+        assertWithMessage(msg).that(parameter.reference.toString()).isEqualTo(ref.toString())
         if (type != ParameterType.Lambda || value != null) {
             assertWithMessage(msg).that(parameter.value).isEqualTo(value)
         }
@@ -1007,13 +1060,16 @@
     var other: MyClass? = null
     var self: MyClass? = null
     var third: MyClass? = null
+
+    override fun hashCode(): Int = name.hashCode()
+    override fun equals(other: Any?): Boolean = name == (other as? MyClass)?.name
 }
 
 private fun NodeParameter.checkEquals(other: NodeParameter): Boolean {
     assertThat(other.name).isEqualTo(name)
     assertThat(other.type).isEqualTo(type)
     assertThat(other.value).isEqualTo(value)
-    assertThat(checkEquals(reference, other.reference)).isTrue()
+    assertThat(other.reference.toString()).isEqualTo(reference.toString())
     assertThat(other.elements.size).isEqualTo(elements.size)
     var hasReferences = reference != null
     elements.forEachIndexed { i, element ->
@@ -1021,9 +1077,3 @@
     }
     return hasReferences
 }
-
-private fun checkEquals(ref1: NodeParameterReference?, ref2: NodeParameterReference?): Boolean =
-    ref1 === ref2 ||
-        ref1?.nodeId == ref2?.nodeId &&
-        ref1?.parameterIndex == ref2?.parameterIndex &&
-        ref1?.indices.contentEquals(ref2?.indices)
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 305feee..36e1ba7 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -119,7 +119,7 @@
         return node.parameters.mapIndexed { index, parameter ->
             parameterFactory.create(
                 rootId,
-                node,
+                node.id,
                 parameter.name,
                 parameter.value,
                 index,
@@ -149,7 +149,7 @@
         val parameter = node.parameters[reference.parameterIndex]
         return parameterFactory.expand(
             rootId,
-            node,
+            node.id,
             parameter.name,
             parameter.value,
             reference,
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
index de5268d..23cfcc3 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
@@ -35,4 +35,10 @@
         parameterIndex: Int,
         indices: List<Int>
     ) : this(nodeId, parameterIndex, indices.asIntArray())
+
+    // For testing:
+    override fun toString(): String {
+        val suffix = if (indices.isNotEmpty()) ", ${indices.joinToString()}" else ""
+        return "[$nodeId, $parameterIndex$suffix]"
+    }
 }
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
index 483feab..d1673b4 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
@@ -121,7 +121,7 @@
      */
     fun create(
         rootId: Long,
-        node: InspectorNode,
+        nodeId: Long,
         name: String,
         value: Any?,
         parameterIndex: Int,
@@ -133,7 +133,7 @@
             return reflectionScope.withReflectiveAccess {
                 creator.create(
                     rootId,
-                    node,
+                    nodeId,
                     name,
                     value,
                     parameterIndex,
@@ -149,9 +149,10 @@
     /**
      * Create/expand the [NodeParameter] specified by [reference].
      *
-     * @param node is the [InspectorNode] with the id of [reference].nodeId.
-     * @param name is the name of the [reference].parameterIndex'th parameter of [node].
-     * @param value is the value of the [reference].parameterIndex'th parameter of [node].
+     * @param rootId is the root id of the specified [nodeId].
+     * @param nodeId is the [InspectorNode.id] of the node the parameter belongs to.
+     * @param name is the name of the [reference].parameterIndex'th parameter of the node.
+     * @param value is the value of the [reference].parameterIndex'th parameter of the node.
      * @param startIndex is the index of the 1st wanted element of a List/Array.
      * @param maxElements is the max number of elements wanted from a List/Array.
      * @param maxRecursions is the max recursion into composite types starting from reference.
@@ -159,7 +160,7 @@
      */
     fun expand(
         rootId: Long,
-        node: InspectorNode,
+        nodeId: Long,
         name: String,
         value: Any?,
         reference: NodeParameterReference,
@@ -173,7 +174,7 @@
             return reflectionScope.withReflectiveAccess {
                 creator.expand(
                     rootId,
-                    node,
+                    nodeId,
                     name,
                     value,
                     reference,
@@ -315,7 +316,7 @@
      */
     private inner class ParameterCreator {
         private var rootId = 0L
-        private var node: InspectorNode? = null
+        private var nodeId = 0L
         private var parameterIndex = 0
         private var maxRecursions = 0
         private var maxInitialIterableSize = 0
@@ -328,7 +329,7 @@
 
         fun create(
             rootId: Long,
-            node: InspectorNode,
+            nodeId: Long,
             name: String,
             value: Any?,
             parameterIndex: Int,
@@ -336,15 +337,15 @@
             maxInitialIterableSize: Int
         ): NodeParameter =
             try {
-                setup(rootId, node, parameterIndex, maxRecursions, maxInitialIterableSize)
-                create(name, value) ?: createEmptyParameter(name)
+                setup(rootId, nodeId, parameterIndex, maxRecursions, maxInitialIterableSize)
+                create(name, value, null) ?: createEmptyParameter(name)
             } finally {
                 setup()
             }
 
         fun expand(
             rootId: Long,
-            node: InspectorNode,
+            nodeId: Long,
             name: String,
             value: Any?,
             reference: NodeParameterReference,
@@ -353,17 +354,21 @@
             maxRecursions: Int,
             maxInitialIterableSize: Int
         ): NodeParameter? {
-            setup(rootId, node, reference.parameterIndex, maxRecursions, maxInitialIterableSize)
+            setup(rootId, nodeId, reference.parameterIndex, maxRecursions, maxInitialIterableSize)
+            var parent: Pair<String, Any?>? = null
             var new = Pair(name, value)
             for (i in reference.indices) {
+                parent = new
                 new = find(new.first, new.second, i) ?: return null
             }
             recursions = 0
             valueIndex.addAll(reference.indices.asSequence())
             val parameter = if (startIndex == 0) {
-                create(new.first, new.second)
+                create(new.first, new.second, parent?.second)
             } else {
-                createFromCompositeValue(new.first, new.second, startIndex, maxElements)
+                createFromCompositeValue(
+                    new.first, new.second, parent?.second, startIndex, maxElements
+                )
             }
             if (parameter == null && reference.indices.isEmpty()) {
                 return createEmptyParameter(name)
@@ -377,13 +382,13 @@
 
         private fun setup(
             newRootId: Long = 0,
-            newNode: InspectorNode? = null,
+            newNodeId: Long = 0,
             newParameterIndex: Int = 0,
             maxRecursions: Int = 0,
             maxInitialIterableSize: Int = 0
         ) {
             rootId = newRootId
-            node = newNode
+            nodeId = newNodeId
             parameterIndex = newParameterIndex
             this.maxRecursions = maxRecursions
             this.maxInitialIterableSize = maxInitialIterableSize
@@ -395,17 +400,18 @@
             }
         }
 
-        private fun create(name: String, value: Any?): NodeParameter? {
+        private fun create(name: String, value: Any?, parentValue: Any?): NodeParameter? {
             if (value == null) {
                 return null
             }
             createFromSimpleValue(name, value)?.let { return it }
 
-            val existing = valueIndexMap[value] ?: return createFromCompositeValue(name, value)
+            val existing =
+                valueIndexMap[value] ?: return createFromCompositeValue(name, value, parentValue)
 
             // Do not decompose an instance we already decomposed.
             // Instead reference the data that was already decomposed.
-            return createReferenceToExistingValue(name, value, existing)
+            return createReferenceToExistingValue(name, value, parentValue, existing)
         }
 
         private fun createFromSimpleValue(name: String, value: Any?): NodeParameter? {
@@ -420,7 +426,6 @@
                 is Boolean -> NodeParameter(name, ParameterType.Boolean, value)
                 is ComposableLambda -> createFromCLambda(name, value)
                 is Color -> NodeParameter(name, ParameterType.Color, value.toArgb())
-//              is CornerSize -> createFromCornerSize(name, value)
                 is Double -> NodeParameter(name, ParameterType.Double, value)
                 is Dp -> NodeParameter(name, DimensionDp, value.value)
                 is Enum<*> -> NodeParameter(name, ParameterType.String, value.toString())
@@ -444,6 +449,7 @@
         private fun createFromCompositeValue(
             name: String,
             value: Any?,
+            parentValue: Any?,
             startIndex: Int = 0,
             maxElements: Int = maxInitialIterableSize
         ): NodeParameter? = when {
@@ -451,6 +457,10 @@
             value is Modifier -> createFromModifier(name, value)
             value is InspectableValue -> createFromInspectableValue(name, value)
             value is Sequence<*> -> createFromSequence(name, value, value, startIndex, maxElements)
+            value is Map<*, *> ->
+                createFromSequence(name, value, value.asSequence(), startIndex, maxElements)
+            value is Map.Entry<*, *> ->
+                createFromMapEntry(name, value, parentValue)
             value is Iterable<*> ->
                 createFromSequence(name, value, value.asSequence(), startIndex, maxElements)
             value.javaClass.isArray -> createFromArray(name, value, startIndex, maxElements)
@@ -464,6 +474,8 @@
             value is Modifier -> findFromModifier(name, value, index)
             value is InspectableValue -> findFromInspectableValue(value, index)
             value is Sequence<*> -> findFromSequence(value, index)
+            value is Map<*, *> -> findFromSequence(value.asSequence(), index)
+            value is Map.Entry<*, *> -> findFromMapEntry(value, index)
             value is Iterable<*> -> findFromSequence(value.asSequence(), index)
             value.javaClass.isArray -> findFromArray(value, index)
             value is Offset -> findFromOffset(value, index)
@@ -474,11 +486,12 @@
         private fun createRecursively(
             name: String,
             value: Any?,
+            parentValue: Any?,
             index: Int
         ): NodeParameter? {
             valueIndex.add(index)
             recursions++
-            val parameter = create(name, value)?.apply {
+            val parameter = create(name, value, parentValue)?.apply {
                 this.index = index
             }
             recursions--
@@ -499,11 +512,14 @@
         private fun createReferenceToExistingValue(
             name: String,
             value: Any?,
+            parentValue: Any?,
             ref: NodeParameterReference
         ): NodeParameter? {
             val remember = recursions
             recursions = maxRecursions
-            val parameter = createFromCompositeValue(name, value)?.apply { reference = ref }
+            val parameter = createFromCompositeValue(name, value, parentValue)?.apply {
+                reference = ref
+            }
             recursions = remember
             return parameter
         }
@@ -523,7 +539,7 @@
             }
             val remember = recursions
             recursions = maxRecursions
-            val parameter = create("p", value)
+            val parameter = create("p", value, null)
             recursions = remember
             valueIndexMap.remove(value)
             return parameter != null
@@ -539,7 +555,20 @@
             if (value != null) {
                 val index = valueIndexToReference()
                 valueIndexMap[value] = index
-                valueLazyReferenceMap.remove(value)?.forEach { it.reference = index }
+            }
+            return this
+        }
+
+        /**
+         * Remove the [value] of this [NodeParameter] if there are no child elements.
+         */
+        private fun NodeParameter.removeIfEmpty(value: Any?): NodeParameter {
+            if (value != null) {
+                if (elements.isEmpty()) {
+                    valueIndexMap.remove(value)
+                }
+                val reference = valueIndexMap[value]
+                valueLazyReferenceMap.remove(value)?.forEach { it.reference = reference }
             }
             return this
         }
@@ -558,7 +587,7 @@
         }
 
         private fun valueIndexToReference(): NodeParameterReference =
-            NodeParameterReference(node!!.id, parameterIndex, valueIndex)
+            NodeParameterReference(nodeId, parameterIndex, valueIndex)
 
         private fun createEmptyParameter(name: String): NodeParameter =
             NodeParameter(name, ParameterType.String, "")
@@ -615,12 +644,6 @@
             return valueLookup[value]?.let { NodeParameter(name, ParameterType.String, it) }
         }
 
-//        private fun createFromCornerSize(name: String, value: CornerSize): NodeParameter {
-//            val size = Size(node!!.width.toFloat(), node!!.height.toFloat())
-//            val pixels = value.toPx(size, density)
-//            return NodeParameter(name, DimensionDp, with(density) { pixels.toDp().value })
-//        }
-
         // For now: select ResourceFontFont closest to W400 and Normal, and return the resId
         private fun createFromFontListFamily(
             name: String,
@@ -646,9 +669,9 @@
                 else -> {
                     val elements = parameter.store(value).elements
                     properties.values.mapIndexedNotNullTo(elements) { index, part ->
-                        createRecursively(part.name, valueOf(part, value), index)
+                        createRecursively(part.name, valueOf(part, value), value, index)
                     }
-                    parameter
+                    parameter.removeIfEmpty(value)
                 }
             }
         }
@@ -707,9 +730,9 @@
             }
             val elements = parameter.store(value).elements
             value.inspectableElements.mapIndexedNotNullTo(elements) { index, element ->
-                createRecursively(element.name, element.value, index)
+                createRecursively(element.name, element.value, value, index)
             }
-            return parameter
+            return parameter.removeIfEmpty(value)
         }
 
         private fun findFromInspectableValue(
@@ -724,6 +747,29 @@
             return Pair(element.name, element.value)
         }
 
+        private fun createFromMapEntry(
+            name: String,
+            entry: Map.Entry<*, *>,
+            parentValue: Any?
+        ): NodeParameter? {
+            val key = createRecursively("key", entry.key, entry, 0) ?: return null
+            val value = createRecursively("value", entry.value, entry, 1) ?: return null
+            val keyName = (key.value?.toString() ?: "").ifEmpty { "entry" }
+            val valueName = value.value?.toString()?.ifEmpty { null }
+            val nodeName = if (parentValue is Map<*, *>) "[$keyName]" else name
+            return NodeParameter(nodeName, ParameterType.String, valueName).apply {
+                elements.add(key)
+                elements.add(value)
+            }
+        }
+
+        private fun findFromMapEntry(entry: Map.Entry<*, *>, index: Int): Pair<String, Any?>? =
+            when (index) {
+                0 -> Pair("key", entry.key)
+                1 -> Pair("value", entry.value)
+                else -> null
+            }
+
         private fun createFromSequence(
             name: String,
             value: Any,
@@ -740,7 +786,9 @@
                     val rest = sequence.drop(startIndex).iterator()
                     var index = startIndex
                     while (rest.hasNext() && elements.size < maxElements) {
-                        createRecursively("[$index]", rest.next(), index)?.let { elements.add(it) }
+                        createRecursively("[$index]", rest.next(), value, index)?.let {
+                            elements.add(it)
+                        }
                         index++
                     }
                     while (rest.hasNext()) {
@@ -749,7 +797,7 @@
                             break
                         }
                     }
-                    parameter
+                    parameter.removeIfEmpty(value)
                 }
             }
         }
@@ -770,6 +818,7 @@
             is CharArray -> "CharArray[${value.size}]"
             is List<*> -> "List[${value.size}]"
             is Set<*> -> "Set[${value.size}]"
+            is Map<*, *> -> "Map[${value.size}]"
             is Collection<*> -> "Collection[${value.size}]"
             is Iterable<*> -> "Iterable"
             else -> "Sequence"
@@ -789,9 +838,9 @@
                     else -> {
                         val elements = parameter.elements
                         modifiers.mapIndexedNotNullTo(elements) { index, element ->
-                            createRecursively("", element, index)
+                            createRecursively("", element, value, index)
                         }
-                        parameter.store(value)
+                        parameter.store(value).removeIfEmpty(value)
                     }
                 }
             }
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
index 7a8eb63..1797d44 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
@@ -19,6 +19,7 @@
 package androidx.compose.ui.lint
 
 import androidx.compose.lint.Names
+import androidx.compose.lint.inheritsFrom
 import androidx.compose.lint.isComposable
 import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ComposableModifierFactory
 import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ModifierFactoryReturnType
@@ -34,7 +35,6 @@
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.PsiClass
 import com.intellij.psi.PsiType
-import com.intellij.psi.util.InheritanceUtil
 import org.jetbrains.kotlin.psi.KtCallableDeclaration
 import org.jetbrains.kotlin.psi.KtDeclarationWithBody
 import org.jetbrains.kotlin.psi.KtFunction
@@ -66,7 +66,7 @@
             val returnType = node.returnType ?: return
 
             // Ignore functions that do not return Modifier or something implementing Modifier
-            if (!InheritanceUtil.isInheritor(returnType, Names.Ui.Modifier.javaFqn)) return
+            if (!returnType.inheritsFrom(Names.Ui.Modifier)) return
 
             val source = node.sourcePsi
 
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
index a0f2fa1..89159ee 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
@@ -19,6 +19,7 @@
 package androidx.compose.ui.lint
 
 import androidx.compose.lint.Names
+import androidx.compose.lint.inheritsFrom
 import androidx.compose.lint.isComposable
 import androidx.compose.lint.returnsUnit
 import com.android.tools.lint.client.api.UElementHandler
@@ -31,7 +32,6 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.util.InheritanceUtil
 import org.jetbrains.kotlin.psi.KtNameReferenceExpression
 import org.jetbrains.kotlin.psi.KtParameter
 import org.jetbrains.uast.UElement
@@ -61,8 +61,7 @@
             if (!node.returnsUnit) return
 
             val modifierParameter = node.uastParameters.firstOrNull { parameter ->
-                parameter.sourcePsi is KtParameter &&
-                    InheritanceUtil.isInheritor(parameter.type, Names.Ui.Modifier.javaFqn)
+                parameter.sourcePsi is KtParameter && parameter.type.inheritsFrom(Names.Ui.Modifier)
             } ?: return
 
             // Need to strongly type this or else Kotlinc cannot resolve overloads for
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
index 3287fd4..ca66316 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
@@ -44,7 +44,6 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.async
 import kotlinx.coroutines.runBlocking
 import org.junit.Ignore
@@ -153,7 +152,6 @@
             wasIdleAfterApplySnapshot = composeIdlingResource.isIdleNow
 
             // Record idleness after the first recomposition
-            @OptIn(ExperimentalCoroutinesApi::class)
             scope.async(start = CoroutineStart.UNDISPATCHED) {
                 // Await a single recomposition
                 withFrameNanos {}
diff --git a/compose/ui/ui-text/api/1.0.0-beta04.txt b/compose/ui/ui-text/api/1.0.0-beta04.txt
index 6c4d62d..43d56d1 100644
--- a/compose/ui/ui-text/api/1.0.0-beta04.txt
+++ b/compose/ui/ui-text/api/1.0.0-beta04.txt
@@ -482,6 +482,9 @@
   public final class LayoutIntrinsicsKt {
   }
 
+  public final class SpannedExtensionsKt {
+  }
+
   public final class TempListUtilsKt {
   }
 
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 6c4d62d..43d56d1 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -482,6 +482,9 @@
   public final class LayoutIntrinsicsKt {
   }
 
+  public final class SpannedExtensionsKt {
+  }
+
   public final class TempListUtilsKt {
   }
 
diff --git a/compose/ui/ui-text/api/public_plus_experimental_1.0.0-beta04.txt b/compose/ui/ui-text/api/public_plus_experimental_1.0.0-beta04.txt
index 4ac26f3..fc1a6db 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_1.0.0-beta04.txt
@@ -488,6 +488,9 @@
   public final class LayoutIntrinsicsKt {
   }
 
+  public final class SpannedExtensionsKt {
+  }
+
   public final class TempListUtilsKt {
   }
 
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 4ac26f3..fc1a6db 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -488,6 +488,9 @@
   public final class LayoutIntrinsicsKt {
   }
 
+  public final class SpannedExtensionsKt {
+  }
+
   public final class TempListUtilsKt {
   }
 
diff --git a/compose/ui/ui-text/api/restricted_1.0.0-beta04.txt b/compose/ui/ui-text/api/restricted_1.0.0-beta04.txt
index 6c4d62d..43d56d1 100644
--- a/compose/ui/ui-text/api/restricted_1.0.0-beta04.txt
+++ b/compose/ui/ui-text/api/restricted_1.0.0-beta04.txt
@@ -482,6 +482,9 @@
   public final class LayoutIntrinsicsKt {
   }
 
+  public final class SpannedExtensionsKt {
+  }
+
   public final class TempListUtilsKt {
   }
 
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 6c4d62d..43d56d1 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -482,6 +482,9 @@
   public final class LayoutIntrinsicsKt {
   }
 
+  public final class SpannedExtensionsKt {
+  }
+
   public final class TempListUtilsKt {
   }
 
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
index 3587a8a..530c75b 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
@@ -3834,6 +3834,34 @@
     }
 
     @Test
+    fun testGetWordBoundary_spaces() {
+        val text = "ab cd  e"
+        val paragraph = simpleParagraph(
+            text = text,
+            style = TextStyle(
+                fontFamily = fontFamilyMeasureFont,
+                fontSize = 20.sp
+            )
+        )
+
+        // end of word (length+1) will select word
+        val singleSpaceStartResult = paragraph.getWordBoundary(text.indexOf('b') + 1)
+        assertThat(singleSpaceStartResult.start).isEqualTo(text.indexOf('a'))
+        assertThat(singleSpaceStartResult.end).isEqualTo(text.indexOf('b') + 1)
+
+        // beginning of word will select word
+        val singleSpaceEndResult = paragraph.getWordBoundary(text.indexOf('c'))
+        assertThat(singleSpaceEndResult.start).isEqualTo(text.indexOf('c'))
+        assertThat(singleSpaceEndResult.end).isEqualTo(text.indexOf('d') + 1)
+
+        // between spaces ("_ | _") where | is the requested offset and _ is the space, will
+        // return the exact collapsed range at offset/offset
+        val doubleSpaceResult = paragraph.getWordBoundary(text.indexOf('d') + 2)
+        assertThat(doubleSpaceResult.start).isEqualTo(text.indexOf('d') + 2)
+        assertThat(doubleSpaceResult.end).isEqualTo(text.indexOf('d') + 2)
+    }
+
+    @Test
     fun testGetWordBoundary_Bidi() {
         val text = "abc \u05d0\u05d1\u05d2 def"
         val paragraph = simpleParagraph(
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
index a3e3778..7b87d30 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
@@ -267,7 +267,7 @@
         )
     }
 
-    private val wordBoundary: WordBoundary by lazy {
+    private val wordBoundary: WordBoundary by lazy(LazyThreadSafetyMode.NONE) {
         WordBoundary(textLocale, layout.text)
     }
 
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 2fd4c74..08eb74e 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
@@ -47,13 +47,13 @@
     resourceLoader: Font.ResourceLoader
 ) : ParagraphIntrinsics {
 
-    override val minIntrinsicWidth: Float by lazy {
+    override val minIntrinsicWidth: Float by lazy(LazyThreadSafetyMode.NONE) {
         infoList.fastMaxBy {
             it.intrinsics.minIntrinsicWidth
         }?.intrinsics?.minIntrinsicWidth ?: 0f
     }
 
-    override val maxIntrinsicWidth: Float by lazy {
+    override val maxIntrinsicWidth: Float by lazy(LazyThreadSafetyMode.NONE) {
         infoList.fastMaxBy {
             it.intrinsics.maxIntrinsicWidth
         }?.intrinsics?.maxIntrinsicWidth ?: 0f
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
index cbbdc9f..47e4b661 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
@@ -221,7 +221,7 @@
     /**
      * Returns the TextRange of the word at the given character offset. Characters not
      * part of a word, such as spaces, symbols, and punctuation, have word breaks
-     * on both sides. In such cases, this method will return TextRange(offset, offset+1).
+     * on both sides. In such cases, this method will return TextRange(offset, offset).
      * Word boundaries are defined more precisely in Unicode Standard Annex #29
      * http://www.unicode.org/reports/tr29/#Word_Boundaries
      */
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 80ae2fc..e11d9cb 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
@@ -39,7 +39,7 @@
          *
          * @sample androidx.compose.ui.text.samples.FontFamilySansSerifSample
          *
-         * @see [CSS sans-serif](https://www.w3.org/TR/css-fonts-3/#sans-serif)
+         * See [CSS sans-serif](https://www.w3.org/TR/css-fonts-3/#sans-serif)
          */
         val SansSerif = GenericFontFamily("sans-serif")
 
@@ -48,7 +48,7 @@
          *
          * @sample androidx.compose.ui.text.samples.FontFamilySerifSample
          *
-         * @see [CSS serif](https://www.w3.org/TR/css-fonts-3/#serif)
+         * See [CSS serif](https://www.w3.org/TR/css-fonts-3/#serif)
          */
         val Serif = GenericFontFamily("serif")
 
@@ -57,7 +57,7 @@
          *
          * @sample androidx.compose.ui.text.samples.FontFamilyMonospaceSample
          *
-         * @see [CSS monospace](https://www.w3.org/TR/css-fonts-3/#monospace)
+         * See [CSS monospace](https://www.w3.org/TR/css-fonts-3/#monospace)
          */
         val Monospace = GenericFontFamily("monospace")
 
@@ -69,7 +69,7 @@
          *
          * @sample androidx.compose.ui.text.samples.FontFamilyCursiveSample
          *
-         * @see [CSS cursive](https://www.w3.org/TR/css-fonts-3/#cursive)
+         * See [CSS cursive](https://www.w3.org/TR/css-fonts-3/#cursive)
          */
         val Cursive = GenericFontFamily("cursive")
     }
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 18b58e1..bf40c4f 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
@@ -36,7 +36,7 @@
 /**
  * Commit final [text] to the text box and set the new cursor position.
  *
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#commitText(java.lang.CharSequence,%20int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#commitText(java.lang.CharSequence,%20int)>
  */
 class CommitTextCommand(
     /**
@@ -111,7 +111,7 @@
 /**
  * Mark a certain region of text as composing text.
  *
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setComposingRegion(int,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setComposingRegion(int,%2520int)>
  */
 class SetComposingRegionCommand(
     /**
@@ -169,7 +169,7 @@
  * Replace the currently composing text with the given text, and set the new cursor position. Any
  * composing text set previously will be removed automatically.
  *
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setComposingText(java.lang.CharSequence,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setComposingText(java.lang.CharSequence,%2520int)>
  */
 class SetComposingTextCommand(
     /**
@@ -255,7 +255,7 @@
  * Before and after refer to the order of the characters in the string, not to their visual
  * representation.
  *
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#deleteSurroundingText(int,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#deleteSurroundingText(int,%2520int)>
  */
 class DeleteSurroundingTextCommand(
     /**
@@ -307,7 +307,7 @@
  * * This command does nothing if there are one or more invalid surrogate pairs
  * in the requested range.
  *
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#deleteSurroundingTextInCodePoints(int,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#deleteSurroundingTextInCodePoints(int,%2520int)>
  */
 class DeleteSurroundingTextInCodePointsCommand(
     /**
@@ -380,7 +380,7 @@
  * Sets the selection on the text. When [start] and [end] have the same value, it sets the cursor
  * position.
  *
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setSelection(int,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setSelection(int,%2520int)>
  */
 class SetSelectionCommand(
     /**
@@ -429,7 +429,7 @@
  * removing any special composing styling or other state that was around it. The cursor position
  * remains unchanged.
  *
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#finishComposingText()>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#finishComposingText()>
  */
 class FinishComposingTextCommand : EditCommand {
 
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
index 6924d17..e145592 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
@@ -87,6 +87,7 @@
      * There is no guarantee that the keyboard will be shown. The software keyboard or
      * system service may silently ignore this request.
      */
+    // TODO(b/183448615) @InternalTextApi
     fun showSoftwareKeyboard() {
         if (_currentInputSession.get() != null) {
             platformTextInputService.showSoftwareKeyboard()
@@ -96,6 +97,7 @@
     /**
      * Hide onscreen keyboard.
      */
+    // TODO(b/183448615) @InternalTextApi
     fun hideSoftwareKeyboard(): Unit = platformTextInputService.hideSoftwareKeyboard()
 }
 /**
@@ -194,7 +196,7 @@
      * @return false if this session expired and no action was performed
      */
     fun showSoftwareKeyboard(): Boolean = ensureOpenSession {
-        textInputService.showSoftwareKeyboard()
+        platformTextInputService.showSoftwareKeyboard()
     }
 
     /**
@@ -209,7 +211,7 @@
      * @return false if this session expired and no action was performed
      */
     fun hideSoftwareKeyboard(): Boolean = ensureOpenSession {
-        textInputService.hideSoftwareKeyboard()
+        platformTextInputService.hideSoftwareKeyboard()
     }
 }
 
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
index 6e2e895..0ddf205 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
@@ -53,6 +53,7 @@
 import org.jetbrains.skija.Typeface
 import org.jetbrains.skija.paragraph.Alignment as SkAlignment
 import org.jetbrains.skija.paragraph.BaselineMode
+import org.jetbrains.skija.paragraph.Direction as SkDirection
 import org.jetbrains.skija.paragraph.LineMetrics
 import org.jetbrains.skija.paragraph.ParagraphBuilder
 import org.jetbrains.skija.paragraph.ParagraphStyle
@@ -184,13 +185,17 @@
         return path
     }
 
-    private val cursorWidth = 2.0f
-    override fun getCursorRect(offset: Int) =
-        getBoxForwardByOffset(offset)?.let { box ->
-            Rect(box.rect.left, box.rect.top, box.rect.left + cursorWidth, box.rect.bottom)
-        } ?: getBoxBackwardByOffset(offset)?.let { box ->
-            Rect(box.rect.right, box.rect.top, box.rect.right + cursorWidth, box.rect.bottom)
-        } ?: Rect(0f, 0f, cursorWidth, paragraphIntrinsics.builder.defaultHeight)
+    override fun getCursorRect(offset: Int): Rect {
+        val horizontal = getHorizontalPosition(offset, true)
+        val line = lineMetricsForOffset(offset)!!
+
+        return Rect(
+            horizontal,
+            (line.baseline - line.ascent).toFloat(),
+            horizontal,
+            (line.baseline + line.descent).toFloat()
+        )
+    }
 
     override fun getLineLeft(lineIndex: Int): Float =
         lineMetrics.getOrNull(lineIndex)?.left?.toFloat() ?: 0f
@@ -258,10 +263,22 @@
     }
 
     override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float {
-        return if (usePrimaryDirection) {
-            getHorizontalPositionForward(offset) ?: getHorizontalPositionBackward(offset) ?: 0f
-        } else {
-            getHorizontalPositionBackward(offset) ?: getHorizontalPositionForward(offset) ?: 0f
+        val prevBox = getBoxBackwardByOffset(offset)
+        val nextBox = getBoxForwardByOffset(offset)
+        return when {
+            prevBox == null -> {
+                val line = lineMetricsForOffset(offset)!!
+                return when (getParagraphDirection(offset)) {
+                    ResolvedTextDirection.Ltr -> line.left.toFloat()
+                    ResolvedTextDirection.Rtl -> line.right.toFloat()
+                }
+            }
+
+            nextBox == null || usePrimaryDirection || nextBox.direction == prevBox.direction ->
+                prevBox.cursorHorizontalPosition()
+
+            else ->
+                nextBox.cursorHorizontalPosition(true)
         }
     }
 
@@ -314,17 +331,15 @@
         return null
     }
 
-    private fun getHorizontalPositionForward(from: Int) =
-        getBoxForwardByOffset(from)?.rect?.left
-
-    private fun getHorizontalPositionBackward(to: Int) =
-        getBoxBackwardByOffset(to)?.rect?.right
-
     override fun getParagraphDirection(offset: Int): ResolvedTextDirection =
-        ResolvedTextDirection.Ltr
+        paragraphIntrinsics.textDirection
 
     override fun getBidiRunDirection(offset: Int): ResolvedTextDirection =
-        ResolvedTextDirection.Ltr
+        when (getBoxForwardByOffset(offset)?.direction) {
+            org.jetbrains.skija.paragraph.Direction.RTL -> ResolvedTextDirection.Rtl
+            org.jetbrains.skija.paragraph.Direction.LTR -> ResolvedTextDirection.Ltr
+            null -> ResolvedTextDirection.Ltr
+        }
 
     override fun getOffsetForPosition(position: Offset): Int {
         return para.getGlyphPositionAtCoordinate(position.x, position.y).position
@@ -336,11 +351,15 @@
     }
 
     override fun getWordBoundary(offset: Int): TextRange {
-        if (text[offset].isWhitespace()) {
-            return TextRange(offset, offset)
-        }
-        para.getWordBoundary(offset).let {
-            return TextRange(it.start, it.end)
+        return when {
+            (text[offset].isLetterOrDigit()) -> para.getWordBoundary(offset).let {
+                TextRange(it.start, it.end)
+            }
+            (text.getOrNull(offset - 1)?.isLetterOrDigit() ?: false) ->
+                para.getWordBoundary(offset - 1).let {
+                    TextRange(it.start, it.end)
+                }
+            else -> TextRange(offset, offset)
         }
     }
 
@@ -534,7 +553,8 @@
     var maxLines: Int = Int.MAX_VALUE,
     val spanStyles: List<Range<SpanStyle>>,
     val placeholders: List<Range<Placeholder>>,
-    val density: Density
+    val density: Density,
+    val textDirection: ResolvedTextDirection
 ) {
     private lateinit var initialStyle: SpanStyle
     private lateinit var defaultStyle: ComputedStyle
@@ -744,6 +764,7 @@
         style.textAlign?.let {
             pStyle.alignment = it.toSkAlignment()
         }
+        pStyle.direction = textDirection.toSkDirection()
         return pStyle
     }
 
@@ -824,11 +845,11 @@
     }
 }
 
-fun Shadow.toSkShadow(): SkShadow {
+internal fun Shadow.toSkShadow(): SkShadow {
     return SkShadow(color.toArgb(), offset.x, offset.y, blurRadius.toDouble())
 }
 
-fun TextAlign.toSkAlignment(): SkAlignment {
+internal fun TextAlign.toSkAlignment(): SkAlignment {
     return when (this) {
         TextAlign.Left -> SkAlignment.LEFT
         TextAlign.Right -> SkAlignment.RIGHT
@@ -838,3 +859,17 @@
         TextAlign.End -> SkAlignment.END
     }
 }
+
+internal fun ResolvedTextDirection.toSkDirection(): SkDirection {
+    return when (this) {
+        ResolvedTextDirection.Ltr -> SkDirection.LTR
+        ResolvedTextDirection.Rtl -> SkDirection.RTL
+    }
+}
+
+internal fun TextBox.cursorHorizontalPosition(opposite: Boolean = false): Float {
+    return when (direction) {
+        SkDirection.LTR, null -> if (opposite) rect.left else rect.right
+        SkDirection.RTL -> if (opposite) rect.right else rect.left
+    }
+}
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraphIntrinsics.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraphIntrinsics.desktop.kt
index 5aca88f..1860b3c 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraphIntrinsics.desktop.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraphIntrinsics.desktop.kt
@@ -21,6 +21,9 @@
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.resolveTextDirection
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.text.style.TextDirection
 import androidx.compose.ui.unit.Density
 import org.jetbrains.skija.paragraph.Paragraph
 import kotlin.math.ceil
@@ -52,6 +55,7 @@
 ) : ParagraphIntrinsics {
 
     val fontLoader = resourceLoader as FontLoader
+    val textDirection = resolveTextDirection(style.textDirection)
     val builder: ParagraphBuilder
     var para: Paragraph
     init {
@@ -61,7 +65,8 @@
             textStyle = style,
             spanStyles = spanStyles,
             placeholders = placeholders,
-            density = density
+            density = density,
+            textDirection = textDirection
         )
         para = builder.build()
 
@@ -70,4 +75,26 @@
 
     override val minIntrinsicWidth = ceil(para.getMinIntrinsicWidth())
     override val maxIntrinsicWidth = ceil(para.getMaxIntrinsicWidth())
+
+    private fun resolveTextDirection(direction: TextDirection?): ResolvedTextDirection {
+        return when (direction) {
+            TextDirection.Ltr -> ResolvedTextDirection.Ltr
+            TextDirection.Rtl -> ResolvedTextDirection.Rtl
+            TextDirection.Content -> contentBasedTextDirection() ?: ResolvedTextDirection.Ltr
+            TextDirection.ContentOrLtr -> contentBasedTextDirection() ?: ResolvedTextDirection.Ltr
+            TextDirection.ContentOrRtl -> contentBasedTextDirection() ?: ResolvedTextDirection.Rtl
+            null -> ResolvedTextDirection.Ltr
+        }
+    }
+
+    private fun contentBasedTextDirection(): ResolvedTextDirection? {
+        for (char in text) {
+            when (char.directionality) {
+                CharDirectionality.LEFT_TO_RIGHT -> return ResolvedTextDirection.Ltr
+                CharDirectionality.RIGHT_TO_LEFT -> return ResolvedTextDirection.Rtl
+                else -> continue
+            }
+        }
+        return null
+    }
 }
diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
index e8d8c27..13af274 100644
--- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
+++ b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.platform.FontLoader
 import androidx.compose.ui.text.platform.Font
+import androidx.compose.ui.text.style.TextDirection
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.sp
 import com.google.common.truth.Truth
@@ -132,6 +133,91 @@
         }
     }
 
+    @Test
+    fun getHorizontalPositionForOffset_primary_Bidi_singleLine_textDirectionDefault() {
+        with(defaultDensity) {
+            val ltrText = "abc"
+            val rtlText = "\u05D0\u05D1\u05D2"
+            val text = ltrText + rtlText
+            val fontSize = 50.sp
+            val fontSizeInPx = fontSize.toPx()
+            val width = text.length * fontSizeInPx
+            val paragraph = simpleParagraph(
+                text = text,
+                style = TextStyle(fontSize = fontSize),
+                width = width
+            )
+
+            for (i in 0..ltrText.length) {
+                Truth.assertThat(paragraph.getHorizontalPosition(i, true))
+                    .isEqualTo(fontSizeInPx * i)
+            }
+
+            for (i in 1 until rtlText.length) {
+                Truth.assertThat(paragraph.getHorizontalPosition(i + ltrText.length, true))
+                    .isEqualTo(width - fontSizeInPx * i)
+            }
+        }
+    }
+
+    @Test
+    fun getHorizontalPositionForOffset_notPrimary_Bidi_singleLine_textDirectionLtr() {
+        with(defaultDensity) {
+            val ltrText = "abc"
+            val rtlText = "\u05D0\u05D1\u05D2"
+            val text = ltrText + rtlText
+            val fontSize = 50.sp
+            val fontSizeInPx = fontSize.toPx()
+            val width = text.length * fontSizeInPx
+            val paragraph = simpleParagraph(
+                text = text,
+                style = TextStyle(
+                    fontSize = fontSize,
+                    textDirection = TextDirection.Ltr
+                ),
+                width = width
+            )
+
+            for (i in ltrText.indices) {
+                Truth.assertThat(paragraph.getHorizontalPosition(i, false))
+                    .isEqualTo(fontSizeInPx * i)
+            }
+
+            for (i in rtlText.indices) {
+                Truth.assertThat(paragraph.getHorizontalPosition(i + ltrText.length, false))
+                    .isEqualTo(width - fontSizeInPx * i)
+            }
+
+            Truth.assertThat(paragraph.getHorizontalPosition(text.length, false))
+                .isEqualTo(width - rtlText.length * fontSizeInPx)
+        }
+    }
+
+    @Test
+    fun getWordBoundary_spaces() {
+        val text = "ab cd  e"
+        val paragraph = simpleParagraph(
+            text = text,
+            style = TextStyle(
+                fontFamily = fontFamilyMeasureFont,
+                fontSize = 20.sp
+            )
+        )
+
+        val singleSpaceStartResult = paragraph.getWordBoundary(text.indexOf('b') + 1)
+        Truth.assertThat(singleSpaceStartResult.start).isEqualTo(text.indexOf('a'))
+        Truth.assertThat(singleSpaceStartResult.end).isEqualTo(text.indexOf('b') + 1)
+
+        val singleSpaceEndResult = paragraph.getWordBoundary(text.indexOf('c'))
+
+        Truth.assertThat(singleSpaceEndResult.start).isEqualTo(text.indexOf('c'))
+        Truth.assertThat(singleSpaceEndResult.end).isEqualTo(text.indexOf('d') + 1)
+
+        val doubleSpaceResult = paragraph.getWordBoundary(text.indexOf('d') + 2)
+        Truth.assertThat(doubleSpaceResult.start).isEqualTo(text.indexOf('d') + 2)
+        Truth.assertThat(doubleSpaceResult.end).isEqualTo(text.indexOf('d') + 2)
+    }
+
     private fun simpleParagraph(
         text: String = "",
         style: TextStyle? = null,
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
index f89d206..98e3857c 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
@@ -186,7 +186,7 @@
     companion object {
         /**
          * A value that [maxWidth] or [maxHeight] will be set to when the constraint should
-         * be considered infinite. [hasBoundedHeight] or [hasBoundedWidth] will be
+         * be considered infinite. [hasBoundedWidth] or [hasBoundedHeight] will be
          * `false` when [maxWidth] or [maxHeight] is [Infinity], respectively.
          */
         const val Infinity = Int.MAX_VALUE
diff --git a/compose/ui/ui-viewbinding/api/1.0.0-beta04.txt b/compose/ui/ui-viewbinding/api/1.0.0-beta04.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/1.0.0-beta04.txt
+++ b/compose/ui/ui-viewbinding/api/1.0.0-beta04.txt
@@ -7,15 +7,3 @@
 
 }
 
-package androidx.compose.ui.viewinterop.databinding {
-
-  public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
-    method public android.widget.LinearLayout getRoot();
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
-    field public final android.widget.FrameLayout second;
-  }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/current.ignore b/compose/ui/ui-viewbinding/api/current.ignore
new file mode 100644
index 0000000..0b8fcf4
--- /dev/null
+++ b/compose/ui/ui-viewbinding/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.compose.ui.viewinterop.databinding:
+    Removed package androidx.compose.ui.viewinterop.databinding
diff --git a/compose/ui/ui-viewbinding/api/current.txt b/compose/ui/ui-viewbinding/api/current.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/current.txt
+++ b/compose/ui/ui-viewbinding/api/current.txt
@@ -7,15 +7,3 @@
 
 }
 
-package androidx.compose.ui.viewinterop.databinding {
-
-  public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
-    method public android.widget.LinearLayout getRoot();
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
-    field public final android.widget.FrameLayout second;
-  }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/public_plus_experimental_1.0.0-beta04.txt b/compose/ui/ui-viewbinding/api/public_plus_experimental_1.0.0-beta04.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/ui/ui-viewbinding/api/public_plus_experimental_1.0.0-beta04.txt
@@ -7,15 +7,3 @@
 
 }
 
-package androidx.compose.ui.viewinterop.databinding {
-
-  public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
-    method public android.widget.LinearLayout getRoot();
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
-    field public final android.widget.FrameLayout second;
-  }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/public_plus_experimental_current.txt b/compose/ui/ui-viewbinding/api/public_plus_experimental_current.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-viewbinding/api/public_plus_experimental_current.txt
@@ -7,15 +7,3 @@
 
 }
 
-package androidx.compose.ui.viewinterop.databinding {
-
-  public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
-    method public android.widget.LinearLayout getRoot();
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
-    field public final android.widget.FrameLayout second;
-  }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/restricted_1.0.0-beta04.txt b/compose/ui/ui-viewbinding/api/restricted_1.0.0-beta04.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/restricted_1.0.0-beta04.txt
+++ b/compose/ui/ui-viewbinding/api/restricted_1.0.0-beta04.txt
@@ -7,15 +7,3 @@
 
 }
 
-package androidx.compose.ui.viewinterop.databinding {
-
-  public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
-    method public android.widget.LinearLayout getRoot();
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
-    field public final android.widget.FrameLayout second;
-  }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/restricted_current.ignore b/compose/ui/ui-viewbinding/api/restricted_current.ignore
new file mode 100644
index 0000000..0b8fcf4
--- /dev/null
+++ b/compose/ui/ui-viewbinding/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.compose.ui.viewinterop.databinding:
+    Removed package androidx.compose.ui.viewinterop.databinding
diff --git a/compose/ui/ui-viewbinding/api/restricted_current.txt b/compose/ui/ui-viewbinding/api/restricted_current.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/restricted_current.txt
+++ b/compose/ui/ui-viewbinding/api/restricted_current.txt
@@ -7,15 +7,3 @@
 
 }
 
-package androidx.compose.ui.viewinterop.databinding {
-
-  public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
-    method public android.widget.LinearLayout getRoot();
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
-    method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
-    field public final android.widget.FrameLayout second;
-  }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/build.gradle b/compose/ui/ui-viewbinding/build.gradle
index 9a0d080..046b10d 100644
--- a/compose/ui/ui-viewbinding/build.gradle
+++ b/compose/ui/ui-viewbinding/build.gradle
@@ -31,6 +31,11 @@
 
     implementation(KOTLIN_STDLIB)
     implementation(project(":compose:ui:ui"))
+    implementation(project(":compose:ui:ui-util"))
+    implementation(VIEW_BINDING)
+    // Required to ensure that Fragments inflated by AndroidViewBinding
+    // actually appear after configuration changes
+    implementation("androidx.fragment:fragment-ktx:1.3.2")
 
     androidTestImplementation(project(":compose:foundation:foundation"))
     androidTestImplementation(project(":compose:test-utils"))
@@ -47,9 +52,3 @@
     description = "Compose integration with ViewBinding"
     legacyDisableKotlinStrictApiMode = true
 }
-
-android {
-    buildFeatures {
-        viewBinding true
-    }
-}
diff --git a/compose/ui/ui-viewbinding/samples/build.gradle b/compose/ui/ui-viewbinding/samples/build.gradle
index 93545c3..a73c3a1 100644
--- a/compose/ui/ui-viewbinding/samples/build.gradle
+++ b/compose/ui/ui-viewbinding/samples/build.gradle
@@ -34,6 +34,16 @@
     implementation(project(":compose:runtime:runtime"))
     implementation(project(":compose:ui:ui"))
     implementation(project(":compose:ui:ui-viewbinding"))
+    // Used when creating layouts that contain a FragmentContainerView
+    implementation("androidx.fragment:fragment-ktx:1.3.2")
+
+    androidTestImplementation(project(":compose:foundation:foundation"))
+    androidTestImplementation(project(":compose:test-utils"))
+    androidTestImplementation(project(":activity:activity-compose"))
+    androidTestImplementation(project(":internal-testutils-runtime"))
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(JUNIT)
+    androidTestImplementation(TRUTH)
 }
 
 androidx {
diff --git a/compose/ui/ui-viewbinding/samples/src/androidTest/AndroidManifest.xml b/compose/ui/ui-viewbinding/samples/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..d6b8107
--- /dev/null
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.compose.ui.viewbinding.samples">
+    <application>
+        <activity android:name="androidx.compose.ui.samples.InflatedFragmentActivity"/>
+        <activity android:name="androidx.compose.ui.samples.ChildInflatedFragmentActivity"/>
+        <activity android:name="androidx.compose.ui.samples.EmptyFragmentActivity"/>
+    </application>
+</manifest>
diff --git a/compose/ui/ui-viewbinding/src/androidTest/java/androidx/compose/ui/viewinterop/AndroidViewBindingTest.kt b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/AndroidViewBindingTest.kt
similarity index 89%
rename from compose/ui/ui-viewbinding/src/androidTest/java/androidx/compose/ui/viewinterop/AndroidViewBindingTest.kt
rename to compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/AndroidViewBindingTest.kt
index 327a41f..f1b25c9 100644
--- a/compose/ui/ui-viewbinding/src/androidTest/java/androidx/compose/ui/viewinterop/AndroidViewBindingTest.kt
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/AndroidViewBindingTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.viewinterop
+package androidx.compose.ui.samples
 
 import android.os.Build
 import androidx.compose.foundation.layout.requiredSize
@@ -33,7 +33,8 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.databinding.TestLayoutBinding
+import androidx.compose.ui.viewinterop.AndroidViewBinding
+import androidx.compose.ui.viewbinding.samples.databinding.SampleLayoutBinding
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.MediumTest
@@ -54,7 +55,7 @@
     @Test
     fun drawing() {
         rule.setContent {
-            AndroidViewBinding(TestLayoutBinding::inflate, Modifier.testTag("layout"))
+            AndroidViewBinding(SampleLayoutBinding::inflate, Modifier.testTag("layout"))
         }
 
         val size = 50.dp
@@ -69,7 +70,7 @@
     fun update() {
         val color = mutableStateOf(Color.Gray)
         rule.setContent {
-            AndroidViewBinding(TestLayoutBinding::inflate, Modifier.testTag("layout")) {
+            AndroidViewBinding(SampleLayoutBinding::inflate, Modifier.testTag("layout")) {
                 second.setBackgroundColor(color.value.toArgb())
             }
         }
@@ -96,7 +97,7 @@
             val sizeIpx = with(density) { size.roundToPx() }
             CompositionLocalProvider(LocalDensity provides density) {
                 AndroidViewBinding(
-                    TestLayoutBinding::inflate,
+                    SampleLayoutBinding::inflate,
                     Modifier.requiredSize(size).onGloballyPositioned {
                         Truth.assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
                     }
diff --git a/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRecreateTest.kt b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRecreateTest.kt
new file mode 100644
index 0000000..8dea4a0
--- /dev/null
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRecreateTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.samples
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.activity.compose.setContent
+import androidx.compose.ui.viewbinding.samples.R
+import androidx.compose.ui.viewbinding.samples.databinding.TestFragmentLayoutBinding
+import androidx.compose.ui.viewinterop.AndroidViewBinding
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.add
+import androidx.fragment.app.commit
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val PARENT_FRAGMENT_CONTAINER_ID = 1
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FragmentRecreateTest {
+
+    @Test
+    fun testRecreateFragment() {
+        with(ActivityScenario.launch(InflatedFragmentActivity::class.java)) {
+            val fragment = withActivity {
+                supportFragmentManager.findFragmentById(R.id.fragment_container)!!
+            }
+            assertThat(fragment.requireView().parent).isNotNull()
+
+            recreate()
+
+            val recreatedFragment = withActivity {
+                supportFragmentManager.findFragmentById(R.id.fragment_container)!!
+            }
+            assertThat(recreatedFragment.requireView().parent).isNotNull()
+        }
+    }
+
+    @Test
+    fun testRecreateChildFragment() {
+        with(ActivityScenario.launch(ChildInflatedFragmentActivity::class.java)) {
+            val parentFragment = withActivity {
+                supportFragmentManager.findFragmentById(PARENT_FRAGMENT_CONTAINER_ID)!!
+            }
+            val fragment = parentFragment.childFragmentManager
+                .findFragmentById(R.id.fragment_container)
+            assertWithMessage("Fragment should be added as a child fragment")
+                .that(fragment).isNotNull()
+            assertThat(fragment!!.requireView().parent).isNotNull()
+
+            recreate()
+
+            val recreatedParentFragment = withActivity {
+                supportFragmentManager.findFragmentById(PARENT_FRAGMENT_CONTAINER_ID)!!
+            }
+            val recreatedFragment = recreatedParentFragment.childFragmentManager
+                .findFragmentById(R.id.fragment_container)
+            assertWithMessage("Fragment should be added as a child fragment")
+                .that(recreatedFragment).isNotNull()
+            assertThat(recreatedFragment!!.requireView().parent).isNotNull()
+        }
+    }
+}
+
+class InflatedFragmentActivity : FragmentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContent {
+            AndroidViewBinding(TestFragmentLayoutBinding::inflate)
+        }
+    }
+}
+
+class ChildInflatedFragmentActivity : FragmentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(
+            FragmentContainerView(this).apply {
+                id = PARENT_FRAGMENT_CONTAINER_ID
+            }
+        )
+        if (savedInstanceState == null) {
+            supportFragmentManager.commit {
+                setReorderingAllowed(true)
+                add<ParentFragment>(PARENT_FRAGMENT_CONTAINER_ID)
+            }
+        }
+    }
+
+    class ParentFragment : Fragment() {
+        override fun onCreateView(
+            inflater: LayoutInflater,
+            container: ViewGroup?,
+            savedInstanceState: Bundle?
+        ) = ComposeView(requireContext()).apply {
+            setContent {
+                AndroidViewBinding(TestFragmentLayoutBinding::inflate)
+            }
+        }
+    }
+}
diff --git a/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRemoveTest.kt b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRemoveTest.kt
new file mode 100644
index 0000000..1255dbd
--- /dev/null
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRemoveTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.samples
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.viewbinding.samples.R
+import androidx.compose.ui.viewbinding.samples.databinding.TestFragmentLayoutBinding
+import androidx.compose.ui.viewinterop.AndroidViewBinding
+import androidx.fragment.app.FragmentActivity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FragmentRemoveTest {
+
+    @get:Rule
+    val rule = createAndroidComposeRule<InflatedFragmentActivity>()
+
+    @Test
+    fun testRemoval() {
+        var show by mutableStateOf(true)
+
+        rule.setContent {
+            if (show) {
+                AndroidViewBinding(TestFragmentLayoutBinding::inflate)
+            }
+        }
+
+        var fragment = rule.activity.supportFragmentManager
+            .findFragmentById(R.id.fragment_container)
+        assertWithMessage("Fragment should be present when AndroidViewBinding is in the hierarchy")
+            .that(fragment)
+            .isNotNull()
+
+        show = false
+
+        rule.waitForIdle()
+
+        fragment = rule.activity.supportFragmentManager
+            .findFragmentById(R.id.fragment_container)
+        assertWithMessage("Fragment should be removed when the AndroidViewBinding is removed")
+            .that(fragment)
+            .isNull()
+    }
+}
+
+class EmptyFragmentActivity : FragmentActivity()
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/InflatedFragment.kt
similarity index 76%
copy from compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
copy to compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/InflatedFragment.kt
index 5ece3c2..e94e7fc 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/InflatedFragment.kt
@@ -14,8 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.catalog
+package androidx.compose.ui.samples
 
-import androidx.compose.integration.demos.common.ActivityDemo
+import androidx.compose.ui.viewbinding.samples.R
+import androidx.fragment.app.Fragment
 
-val MaterialCatalog = ActivityDemo("Material Catalog", CatalogActivity::class)
+class InflatedFragment : Fragment(R.layout.sample_layout)
diff --git a/compose/ui/ui-viewbinding/samples/src/main/AndroidManifest.xml b/compose/ui/ui-viewbinding/samples/src/main/AndroidManifest.xml
index 2709cbb..f27fc71 100644
--- a/compose/ui/ui-viewbinding/samples/src/main/AndroidManifest.xml
+++ b/compose/ui/ui-viewbinding/samples/src/main/AndroidManifest.xml
@@ -14,4 +14,4 @@
   limitations under the License.
   -->
 
-<manifest package="androidx.compose.ui.viewinterop.samples" />
+<manifest package="androidx.compose.ui.viewbinding.samples" />
diff --git a/compose/ui/ui-viewbinding/samples/src/main/java/androidx/compose/ui/samples/AndroidViewBindingSample.kt b/compose/ui/ui-viewbinding/samples/src/main/java/androidx/compose/ui/samples/AndroidViewBindingSample.kt
index 0449083..1555523 100644
--- a/compose/ui/ui-viewbinding/samples/src/main/java/androidx/compose/ui/samples/AndroidViewBindingSample.kt
+++ b/compose/ui/ui-viewbinding/samples/src/main/java/androidx/compose/ui/samples/AndroidViewBindingSample.kt
@@ -19,8 +19,8 @@
 import android.graphics.Color
 import androidx.annotation.Sampled
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.viewbinding.samples.databinding.SampleLayoutBinding
 import androidx.compose.ui.viewinterop.AndroidViewBinding
-import androidx.compose.ui.viewinterop.samples.databinding.SampleLayoutBinding
 
 @Sampled
 @Composable
diff --git a/compose/ui/ui-viewbinding/samples/src/main/res/layout/test_fragment_layout.xml b/compose/ui/ui-viewbinding/samples/src/main/res/layout/test_fragment_layout.xml
new file mode 100644
index 0000000..0806f66
--- /dev/null
+++ b/compose/ui/ui-viewbinding/samples/src/main/res/layout/test_fragment_layout.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.fragment.app.FragmentContainerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/fragment_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:name="androidx.compose.ui.samples.InflatedFragment"/>
\ No newline at end of file
diff --git a/compose/ui/ui-viewbinding/src/main/AndroidManifest.xml b/compose/ui/ui-viewbinding/src/main/AndroidManifest.xml
index 6e4f4af..4cfbcc0 100644
--- a/compose/ui/ui-viewbinding/src/main/AndroidManifest.xml
+++ b/compose/ui/ui-viewbinding/src/main/AndroidManifest.xml
@@ -14,4 +14,4 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<manifest package="androidx.compose.ui.viewinterop" />
+<manifest package="androidx.compose.ui.viewbinding" />
diff --git a/compose/ui/ui-viewbinding/src/main/java/androidx/compose/ui/viewinterop/AndroidViewBinding.kt b/compose/ui/ui-viewbinding/src/main/java/androidx/compose/ui/viewinterop/AndroidViewBinding.kt
index 55eaac8..145df82 100644
--- a/compose/ui/ui-viewbinding/src/main/java/androidx/compose/ui/viewinterop/AndroidViewBinding.kt
+++ b/compose/ui/ui-viewbinding/src/main/java/androidx/compose/ui/viewinterop/AndroidViewBinding.kt
@@ -22,9 +22,20 @@
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.node.Ref
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.util.fastForEach
+import androidx.core.view.forEach
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.commit
+import androidx.fragment.app.findFragment
 import androidx.viewbinding.ViewBinding
 
 /**
@@ -51,11 +62,32 @@
     update: T.() -> Unit = {}
 ) {
     val viewBindingRef = remember { Ref<T>() }
-    val viewBlock: (Context) -> View = remember {
+    val localView = LocalView.current
+    // Find the parent fragment, if one exists. This will let us ensure that
+    // fragments inflated via a FragmentContainerView are properly nested
+    // (which, in turn, allows the fragments to properly save/restore their state)
+    val parentFragment = remember(localView) {
+        try {
+            localView.findFragment<Fragment>()
+        } catch (e: IllegalStateException) {
+            // findFragment throws if no parent fragment is found
+            null
+        }
+    }
+    val fragmentContainerViews = remember { mutableStateListOf<FragmentContainerView>() }
+    val viewBlock: (Context) -> View = remember(localView) {
         { context ->
-            val inflater = LayoutInflater.from(context)
+            // Inflated fragments are automatically nested properly when
+            // using the parent fragment's LayoutInflater
+            val inflater = parentFragment?.layoutInflater ?: LayoutInflater.from(context)
             val viewBinding = factory(inflater, FrameLayout(context), false)
             viewBindingRef.value = viewBinding
+            // Find all FragmentContainerView instances in the newly inflated layout
+            fragmentContainerViews.clear()
+            val rootGroup = viewBinding.root as? ViewGroup
+            if (rootGroup != null) {
+                findFragmentContainerViews(rootGroup, fragmentContainerViews)
+            }
             viewBinding.root
         }
     }
@@ -64,4 +96,41 @@
         modifier = modifier,
         update = { viewBindingRef.value?.update() }
     )
+
+    // Set up a DisposableEffect for each FragmentContainerView that will
+    // clean up inflated fragments when the AndroidViewBinding is disposed
+    val localContext = LocalContext.current
+    fragmentContainerViews.fastForEach { container ->
+        DisposableEffect(localContext, container) {
+            // Find the right FragmentManager
+            val fragmentManager = parentFragment?.childFragmentManager
+                ?: (localContext as? FragmentActivity)?.supportFragmentManager
+            // Now find the fragment inflated via the FragmentContainerView
+            val existingFragment = fragmentManager?.findFragmentById(container.id)
+            onDispose {
+                if (existingFragment != null && !fragmentManager.isStateSaved) {
+                    // If the state isn't saved, that means that some state change
+                    // has removed this Composable from the hierarchy
+                    fragmentManager.commit {
+                        remove(existingFragment)
+                    }
+                }
+            }
+        }
+    }
+}
+
+private fun findFragmentContainerViews(
+    viewGroup: ViewGroup,
+    list: MutableList<FragmentContainerView>
+) {
+    if (viewGroup is FragmentContainerView) {
+        list += viewGroup
+    } else {
+        viewGroup.forEach {
+            if (it is ViewGroup) {
+                findFragmentContainerViews(it, list)
+            }
+        }
+    }
 }
diff --git a/compose/ui/ui-viewbinding/src/main/res/layout/test_layout.xml b/compose/ui/ui-viewbinding/src/main/res/layout/test_layout.xml
deleted file mode 100644
index 85ba5ee..0000000
--- a/compose/ui/ui-viewbinding/src/main/res/layout/test_layout.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright 2020 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <FrameLayout
-        android:layout_width="50dp"
-        android:layout_height="50dp"
-        android:background="#0000FF" />
-    <FrameLayout
-        android:id="@+id/second"
-        android:layout_width="50dp"
-        android:layout_height="50dp"
-        android:background="#000000" />
-</LinearLayout>
\ No newline at end of file
diff --git a/compose/ui/ui/api/1.0.0-beta04.txt b/compose/ui/ui/api/1.0.0-beta04.txt
index 27cc180..a926e4a 100644
--- a/compose/ui/ui/api/1.0.0-beta04.txt
+++ b/compose/ui/ui/api/1.0.0-beta04.txt
@@ -346,6 +346,9 @@
   public final class FocusTraversalKt {
   }
 
+  public final class TwoDimensionalFocusSearchKt {
+  }
+
 }
 
 package androidx.compose.ui.graphics {
@@ -2071,11 +2074,11 @@
   }
 
   public final class ColorResources_androidKt {
-    method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
   }
 
   public final class FontResources_androidKt {
-    method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
   }
 
   public final class ImageResources_androidKt {
@@ -2088,16 +2091,16 @@
   }
 
   public final class PrimitiveResources_androidKt {
-    method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
-    method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
-    method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
   }
 
   public final class StringResources_androidKt {
-    method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
   }
 
   public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 27cc180..a926e4a 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -346,6 +346,9 @@
   public final class FocusTraversalKt {
   }
 
+  public final class TwoDimensionalFocusSearchKt {
+  }
+
 }
 
 package androidx.compose.ui.graphics {
@@ -2071,11 +2074,11 @@
   }
 
   public final class ColorResources_androidKt {
-    method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
   }
 
   public final class FontResources_androidKt {
-    method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
   }
 
   public final class ImageResources_androidKt {
@@ -2088,16 +2091,16 @@
   }
 
   public final class PrimitiveResources_androidKt {
-    method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
-    method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
-    method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
   }
 
   public final class StringResources_androidKt {
-    method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
   }
 
   public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/public_plus_experimental_1.0.0-beta04.txt b/compose/ui/ui/api/public_plus_experimental_1.0.0-beta04.txt
index 223598a..76230b4 100644
--- a/compose/ui/ui/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/ui/ui/api/public_plus_experimental_1.0.0-beta04.txt
@@ -438,6 +438,9 @@
   public final class FocusTraversalKt {
   }
 
+  public final class TwoDimensionalFocusSearchKt {
+  }
+
 }
 
 package androidx.compose.ui.graphics {
@@ -2048,13 +2051,16 @@
 
   @androidx.compose.ui.ExperimentalComposeUiApi public final class LocalSoftwareKeyboardController {
     method @androidx.compose.runtime.Composable public androidx.compose.ui.platform.SoftwareKeyboardController? getCurrent();
+    method public infix androidx.compose.runtime.ProvidedValue<androidx.compose.ui.platform.SoftwareKeyboardController> provides(androidx.compose.ui.platform.SoftwareKeyboardController softwareKeyboardController);
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.platform.SoftwareKeyboardController? current;
     field public static final androidx.compose.ui.platform.LocalSoftwareKeyboardController INSTANCE;
   }
 
-  @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftwareKeyboardController {
-    method public void hideSoftwareKeyboard();
-    method public void showSoftwareKeyboard();
+  @androidx.compose.runtime.Stable @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftwareKeyboardController {
+    method public void hide();
+    method @Deprecated public default void hideSoftwareKeyboard();
+    method public void show();
+    method @Deprecated public default void showSoftwareKeyboard();
   }
 
   public final class TestTagKt {
@@ -2204,11 +2210,11 @@
   }
 
   public final class ColorResources_androidKt {
-    method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
   }
 
   public final class FontResources_androidKt {
-    method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
   }
 
   public final class ImageResources_androidKt {
@@ -2221,16 +2227,16 @@
   }
 
   public final class PrimitiveResources_androidKt {
-    method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
-    method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
-    method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
   }
 
   public final class StringResources_androidKt {
-    method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
   }
 
   public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 223598a..76230b4 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -438,6 +438,9 @@
   public final class FocusTraversalKt {
   }
 
+  public final class TwoDimensionalFocusSearchKt {
+  }
+
 }
 
 package androidx.compose.ui.graphics {
@@ -2048,13 +2051,16 @@
 
   @androidx.compose.ui.ExperimentalComposeUiApi public final class LocalSoftwareKeyboardController {
     method @androidx.compose.runtime.Composable public androidx.compose.ui.platform.SoftwareKeyboardController? getCurrent();
+    method public infix androidx.compose.runtime.ProvidedValue<androidx.compose.ui.platform.SoftwareKeyboardController> provides(androidx.compose.ui.platform.SoftwareKeyboardController softwareKeyboardController);
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.platform.SoftwareKeyboardController? current;
     field public static final androidx.compose.ui.platform.LocalSoftwareKeyboardController INSTANCE;
   }
 
-  @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftwareKeyboardController {
-    method public void hideSoftwareKeyboard();
-    method public void showSoftwareKeyboard();
+  @androidx.compose.runtime.Stable @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftwareKeyboardController {
+    method public void hide();
+    method @Deprecated public default void hideSoftwareKeyboard();
+    method public void show();
+    method @Deprecated public default void showSoftwareKeyboard();
   }
 
   public final class TestTagKt {
@@ -2204,11 +2210,11 @@
   }
 
   public final class ColorResources_androidKt {
-    method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
   }
 
   public final class FontResources_androidKt {
-    method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
   }
 
   public final class ImageResources_androidKt {
@@ -2221,16 +2227,16 @@
   }
 
   public final class PrimitiveResources_androidKt {
-    method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
-    method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
-    method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
   }
 
   public final class StringResources_androidKt {
-    method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
   }
 
   public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/restricted_1.0.0-beta04.txt b/compose/ui/ui/api/restricted_1.0.0-beta04.txt
index a43f540..8864bcd 100644
--- a/compose/ui/ui/api/restricted_1.0.0-beta04.txt
+++ b/compose/ui/ui/api/restricted_1.0.0-beta04.txt
@@ -346,6 +346,9 @@
   public final class FocusTraversalKt {
   }
 
+  public final class TwoDimensionalFocusSearchKt {
+  }
+
 }
 
 package androidx.compose.ui.graphics {
@@ -2101,11 +2104,11 @@
   }
 
   public final class ColorResources_androidKt {
-    method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
   }
 
   public final class FontResources_androidKt {
-    method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
   }
 
   public final class ImageResources_androidKt {
@@ -2118,16 +2121,16 @@
   }
 
   public final class PrimitiveResources_androidKt {
-    method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
-    method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
-    method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
   }
 
   public final class StringResources_androidKt {
-    method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
   }
 
   public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index a43f540..8864bcd 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -346,6 +346,9 @@
   public final class FocusTraversalKt {
   }
 
+  public final class TwoDimensionalFocusSearchKt {
+  }
+
 }
 
 package androidx.compose.ui.graphics {
@@ -2101,11 +2104,11 @@
   }
 
   public final class ColorResources_androidKt {
-    method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
   }
 
   public final class FontResources_androidKt {
-    method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
   }
 
   public final class ImageResources_androidKt {
@@ -2118,16 +2121,16 @@
   }
 
   public final class PrimitiveResources_androidKt {
-    method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
-    method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
-    method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
   }
 
   public final class StringResources_androidKt {
-    method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
-    method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
   }
 
   public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SoftwareKeyboardControllerDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SoftwareKeyboardControllerDemo.kt
index ea2650c..e88deb2 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SoftwareKeyboardControllerDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SoftwareKeyboardControllerDemo.kt
@@ -63,7 +63,7 @@
         Button(
             onClick = {
                 isHidden = true
-                keyboardController?.hideSoftwareKeyboard()
+                keyboardController?.hide()
             },
             enabled = !isHidden,
             modifier = Modifier.padding(vertical = 8.dp)
@@ -74,7 +74,7 @@
             onClick = {
                 isHidden = false
                 focusRequester.requestFocus()
-                keyboardController?.showSoftwareKeyboard()
+                keyboardController?.show()
             },
             enabled = isHidden
         ) {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 645f31e..6912493 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -17,9 +17,17 @@
 package androidx.compose.ui.demos
 
 import androidx.compose.foundation.demos.text.SoftwareKeyboardControllerDemo
+import androidx.compose.integration.demos.common.ComposableDemo
+import androidx.compose.integration.demos.common.DemoCategory
 import androidx.compose.ui.demos.autofill.ExplicitAutofillTypesDemo
+import androidx.compose.ui.demos.focus.CustomFocusOrderDemo
+import androidx.compose.ui.demos.focus.FocusInDialogDemo
+import androidx.compose.ui.demos.focus.FocusInPopupDemo
+import androidx.compose.ui.demos.focus.FocusManagerMoveFocusDemo
+import androidx.compose.ui.demos.focus.FocusSearchDemo
 import androidx.compose.ui.demos.focus.FocusableDemo
 import androidx.compose.ui.demos.focus.ReuseFocusRequesterDemo
+import androidx.compose.ui.demos.gestures.DetectTapGesturesDemo
 import androidx.compose.ui.demos.gestures.DoubleTapGestureFilterDemo
 import androidx.compose.ui.demos.gestures.DoubleTapInTapDemo
 import androidx.compose.ui.demos.gestures.DragAndScaleGestureFilterDemo
@@ -30,6 +38,7 @@
 import androidx.compose.ui.demos.gestures.LongPressGestureDetectorDemo
 import androidx.compose.ui.demos.gestures.NestedLongPressDemo
 import androidx.compose.ui.demos.gestures.NestedPressingDemo
+import androidx.compose.ui.demos.gestures.NestedScrollDispatchDemo
 import androidx.compose.ui.demos.gestures.NestedScrollingDemo
 import androidx.compose.ui.demos.gestures.PointerInputDuringSubComp
 import androidx.compose.ui.demos.gestures.PopupDragDemo
@@ -37,17 +46,9 @@
 import androidx.compose.ui.demos.gestures.RawDragGestureFilterDemo
 import androidx.compose.ui.demos.gestures.ScaleGestureFilterDemo
 import androidx.compose.ui.demos.gestures.ScrollGestureFilterDemo
-import androidx.compose.ui.demos.gestures.DetectTapGesturesDemo
 import androidx.compose.ui.demos.gestures.VerticalScrollerInDrawerDemo
 import androidx.compose.ui.demos.keyinput.KeyInputDemo
 import androidx.compose.ui.demos.viewinterop.ViewInteropDemo
-import androidx.compose.integration.demos.common.ComposableDemo
-import androidx.compose.integration.demos.common.DemoCategory
-import androidx.compose.ui.demos.focus.CustomFocusOrderDemo
-import androidx.compose.ui.demos.focus.FocusInDialogDemo
-import androidx.compose.ui.demos.focus.FocusInPopupDemo
-import androidx.compose.ui.demos.focus.FocusManagerMoveFocusDemo
-import androidx.compose.ui.demos.gestures.NestedScrollDispatchDemo
 import androidx.compose.ui.samples.NestedScrollConnectionSample
 
 private val GestureDemos = DemoCategory(
@@ -108,6 +109,7 @@
         ComposableDemo("Focus Within Dialog") { FocusInDialogDemo() },
         ComposableDemo("Focus Within Popup") { FocusInPopupDemo() },
         ComposableDemo("Reuse Focus Requester") { ReuseFocusRequesterDemo() },
+        ComposableDemo("Focus Search") { FocusSearchDemo() },
         ComposableDemo("Custom Focus Order") { CustomFocusOrderDemo() },
         ComposableDemo("FocusManager.moveFocus()") { FocusManagerMoveFocusDemo() }
     )
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusSearchDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusSearchDemo.kt
new file mode 100644
index 0000000..4d26bd2
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusSearchDemo.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.demos.focus
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.isFocused
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color.Companion.Black
+import androidx.compose.ui.graphics.Color.Companion.Red
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun FocusSearchDemo() {
+    Column {
+        Text("Use Tab/Shift+Tab or arrow keys to move focus:")
+        Box(Modifier.padding(10.dp).size(330.dp, 280.dp)) {
+            FocusableBox(Modifier.offset(10.dp, 10.dp).size(40.dp, 40.dp))
+            FocusableBox(Modifier.offset(60.dp, 10.dp).size(40.dp, 40.dp))
+            Row(Modifier.offset(110.dp, 10.dp)) {
+                FocusableBox(Modifier.size(23.dp, 20.dp))
+                FocusableBox(Modifier.padding(horizontal = 10.dp).size(23.dp, 20.dp))
+                FocusableBox(Modifier.size(23.dp, 20.dp))
+            }
+            FocusableBox(Modifier.offset(210.dp, 10.dp).size(40.dp, 120.dp))
+            FocusableBox(Modifier.offset(260.dp, 10.dp).size(60.dp, 260.dp)) {
+                FocusableBox(Modifier.offset(10.dp, 10.dp).size(40.dp, 40.dp))
+                FocusableBox(Modifier.offset(10.dp, 60.dp).size(40.dp, 40.dp))
+                FocusableBox(Modifier.offset(10.dp, 110.dp).size(40.dp, 40.dp))
+                FocusableBox(Modifier.offset(10.dp, 160.dp).size(40.dp, 40.dp))
+                FocusableBox(Modifier.offset(10.dp, 210.dp).size(40.dp, 40.dp))
+            }
+            FocusableBox(Modifier.offset(60.dp, 60.dp).size(18.dp, 18.dp))
+            FocusableBox(Modifier.offset(82.dp, 60.dp).size(18.dp, 18.dp))
+            FocusableBox(Modifier.offset(110.dp, 40.dp).size(40.dp, 40.dp))
+            FocusableBox(Modifier.offset(160.dp, 40.dp).size(40.dp, 40.dp))
+            FocusableBox(Modifier.offset(10.dp, 60.dp).size(40.dp, 210.dp))
+            FocusableBox(Modifier.offset(60.dp, 90.dp).size(140.dp, 40.dp))
+            FocusableBox(Modifier.offset(60.dp, 140.dp).size(190.dp, 130.dp)) {
+                FocusableBox(Modifier.offset(10.dp, 10.dp).size(170.dp, 110.dp)) {
+                    FocusableBox(Modifier.offset(10.dp, 10.dp).size(150.dp, 90.dp)) {
+                        FocusableBox(Modifier.offset(10.dp, 10.dp).size(130.dp, 70.dp)) {
+                            FocusableBox(Modifier.offset(10.dp, 15.dp).size(40.dp, 40.dp))
+                            FocusableBox(Modifier.offset(58.dp, 15.dp).size(15.dp, 15.dp))
+                            FocusableBox(Modifier.offset(58.dp, 40.dp).size(15.dp, 15.dp))
+                            FocusableBox(Modifier.offset(80.dp, 15.dp).size(40.dp, 40.dp))
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun FocusableBox(
+    modifier: Modifier = Modifier,
+    content: @Composable BoxScope.() -> Unit = {}
+) {
+    var borderColor by remember { mutableStateOf(Black) }
+    Box(
+        modifier = modifier
+            .onFocusChanged { borderColor = if (it.isFocused) Red else Black }
+            .border(2.dp, borderColor)
+            .focusable(),
+        content = content
+    )
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SoftwareKeyboardControllerSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SoftwareKeyboardControllerSample.kt
index dd27b86..49313dc 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SoftwareKeyboardControllerSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SoftwareKeyboardControllerSample.kt
@@ -55,7 +55,7 @@
             setText,
             keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
             keyboardActions = KeyboardActions(
-                onDone = { keyboardController?.hideSoftwareKeyboard() }
+                onDone = { keyboardController?.hide() }
             ),
             modifier = Modifier
                 .focusRequester(focusRequester)
@@ -65,7 +65,7 @@
         Button(
             onClick = {
                 focusRequester.requestFocus()
-                keyboardController?.showSoftwareKeyboard()
+                keyboardController?.show()
             },
             modifier = Modifier.fillMaxWidth()
         ) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index adfa884..837f607 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -20,6 +20,11 @@
 import android.graphics.RectF
 import android.os.Build
 import android.os.Bundle
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_HOVER_ENTER
+import android.view.MotionEvent.ACTION_HOVER_MOVE
+import android.view.View
 import android.view.ViewGroup
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityNodeInfo
@@ -29,7 +34,9 @@
 import android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_SELECTION
 import android.view.accessibility.AccessibilityNodeProvider
 import android.view.accessibility.AccessibilityRecord
-import androidx.activity.ComponentActivity
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.TextView
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
@@ -63,6 +70,7 @@
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.textSelectionRange
 import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsOff
@@ -79,6 +87,7 @@
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
 import androidx.compose.ui.window.Dialog
 import androidx.core.view.ViewCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@@ -96,6 +105,7 @@
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -107,13 +117,14 @@
 import org.mockito.ArgumentMatcher
 import org.mockito.ArgumentMatchers.any
 import org.mockito.internal.matchers.apachecommons.ReflectionEquals
+import java.lang.reflect.Method
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalFoundationApi::class)
 class AndroidAccessibilityTest {
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
+    val rule = createAndroidComposeRule<TestActivity>()
 
     private lateinit var androidComposeView: AndroidComposeView
     private lateinit var container: OpenComposeView
@@ -872,24 +883,17 @@
             }
         }
 
-        var rootNodeBoundsLeft = 0f
-        var rootNodeBoundsTop = 0f
-        rule.runOnIdle {
-            val rootNode = androidComposeView.semanticsOwner.rootSemanticsNode
-            rootNodeBoundsLeft = rootNode.boundsInWindow.left
-            rootNodeBoundsTop = rootNode.boundsInWindow.top
-        }
-
         val toggleableNode = rule.onNodeWithTag(tag)
             .fetchSemanticsNode("couldn't find node with tag $tag")
-        val toggleableNodeBounds = toggleableNode.boundsInWindow
+        val toggleableNodeBounds = toggleableNode.boundsInRoot
 
-        val toggleableNodeId = delegate.getVirtualViewAt(
-            (toggleableNodeBounds.left + toggleableNodeBounds.right) / 2 - rootNodeBoundsLeft,
-            (toggleableNodeBounds.top + toggleableNodeBounds.bottom) / 2 - rootNodeBoundsTop
+        val toggleableNodeFound = delegate.findSemanticsNodeAt(
+            (toggleableNodeBounds.left + toggleableNodeBounds.right) / 2,
+            (toggleableNodeBounds.top + toggleableNodeBounds.bottom) / 2,
+            androidComposeView.semanticsOwner.rootSemanticsNode
         )
-
-        assertEquals(toggleableNode.id, toggleableNodeId)
+        assertNotNull(toggleableNodeFound)
+        assertEquals(toggleableNode.id, toggleableNodeFound!!.id)
     }
 
     @Test
@@ -914,28 +918,187 @@
             }
         }
 
-        var rootNodeBoundsLeft = 0f
-        var rootNodeBoundsTop = 0f
-        rule.runOnIdle {
-            val rootNode = androidComposeView.semanticsOwner.rootSemanticsNode
-            rootNodeBoundsLeft = rootNode.boundsInWindow.left
-            rootNodeBoundsTop = rootNode.boundsInWindow.top
-        }
-
         val overlappedChildOneNode = rule.onNodeWithTag(childOneTag)
             .fetchSemanticsNode("couldn't find node with tag $childOneTag")
         val overlappedChildTwoNode = rule.onNodeWithTag(childTwoTag)
             .fetchSemanticsNode("couldn't find node with tag $childTwoTag")
-        val overlappedChildNodeBounds = overlappedChildTwoNode.boundsInWindow
-        val overlappedChildNodeId = delegate.getVirtualViewAt(
-            (overlappedChildNodeBounds.left + overlappedChildNodeBounds.right) / 2 -
-                rootNodeBoundsLeft,
-            (overlappedChildNodeBounds.top + overlappedChildNodeBounds.bottom) / 2 -
-                rootNodeBoundsTop
+        val overlappedChildNodeBounds = overlappedChildTwoNode.boundsInRoot
+        val overlappedChildNode = delegate.findSemanticsNodeAt(
+            (overlappedChildNodeBounds.left + overlappedChildNodeBounds.right) / 2,
+            (overlappedChildNodeBounds.top + overlappedChildNodeBounds.bottom) / 2,
+            androidComposeView.semanticsOwner.rootSemanticsNode
+        )
+        assertNotNull(overlappedChildNode)
+        assertEquals(overlappedChildOneNode.id, overlappedChildNode!!.id)
+        assertNotEquals(overlappedChildTwoNode.id, overlappedChildNode.id)
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.P)
+    fun testViewInterop_findViewByAccessibilityId() {
+        val androidViewTag = "androidView"
+        container.setContent {
+            Column {
+                AndroidView(
+                    { context ->
+                        LinearLayout(context).apply {
+                            addView(TextView(context).apply { text = "Text1" })
+                            addView(TextView(context).apply { text = "Text2" })
+                        }
+                    },
+                    Modifier.testTag(androidViewTag)
+                )
+                BasicText("text")
+            }
+        }
+
+        val getViewRootImplMethod = View::class.java.getDeclaredMethod("getViewRootImpl")
+        getViewRootImplMethod.isAccessible = true
+        val rootView = getViewRootImplMethod.invoke(container)
+
+        val forName = Class::class.java.getMethod("forName", String::class.java)
+        val getDeclaredMethod = Class::class.java.getMethod(
+            "getDeclaredMethod",
+            String::class.java,
+            arrayOf<Class<*>>()::class.java
         )
 
-        assertEquals(overlappedChildOneNode.id, overlappedChildNodeId)
-        assertNotEquals(overlappedChildTwoNode.id, overlappedChildNodeId)
+        val viewRootImplClass = forName.invoke(null, "android.view.ViewRootImpl") as Class<*>
+        val getAccessibilityInteractionControllerMethod = getDeclaredMethod.invoke(
+            viewRootImplClass,
+            "getAccessibilityInteractionController",
+            arrayOf<Class<*>>()
+        ) as Method
+        getAccessibilityInteractionControllerMethod.isAccessible = true
+        val accessibilityInteractionController =
+            getAccessibilityInteractionControllerMethod.invoke(rootView)
+
+        val accessibilityInteractionControllerClass =
+            forName.invoke(null, "android.view.AccessibilityInteractionController") as Class<*>
+        val findViewByAccessibilityIdMethod =
+            getDeclaredMethod.invoke(
+                accessibilityInteractionControllerClass,
+                "findViewByAccessibilityId",
+                arrayOf<Class<*>>(Int::class.java)
+            ) as Method
+        findViewByAccessibilityIdMethod.isAccessible = true
+
+        val androidView = rule.onNodeWithTag(androidViewTag)
+            .fetchSemanticsNode("can't find node with tag $androidViewTag")
+        val viewGroup = androidComposeView.androidViewsHandler
+            .layoutNodeToHolder[androidView.layoutNode]!!.view as ViewGroup
+        val getAccessibilityViewIdMethod = View::class.java
+            .getDeclaredMethod("getAccessibilityViewId")
+        getAccessibilityViewIdMethod.isAccessible = true
+
+        val textTwo = viewGroup.getChildAt(1)
+        val textViewTwoId = getAccessibilityViewIdMethod.invoke(textTwo)
+        val foundView = findViewByAccessibilityIdMethod.invoke(
+            accessibilityInteractionController,
+            textViewTwoId
+        )
+        assertNotNull(foundView)
+        assertEquals(textTwo, foundView)
+    }
+
+    @Test
+    fun testViewInterop_viewChildExists() {
+        val colTag = "ColTag"
+        val buttonText = "button text"
+        container.setContent {
+            Column(Modifier.testTag(colTag)) {
+                AndroidView(::Button) {
+                    it.text = buttonText
+                    it.setOnClickListener {}
+                }
+                BasicText("text")
+            }
+        }
+
+        val colSemanticsNode = rule.onNodeWithTag(colTag)
+            .fetchSemanticsNode("can't find node with tag $colTag")
+        val colAccessibilityNode = provider.createAccessibilityNodeInfo(colSemanticsNode.id)
+        assertEquals(2, colAccessibilityNode.childCount)
+        assertEquals(2, colSemanticsNode.children.size)
+        val buttonHolder = androidComposeView.androidViewsHandler
+            .layoutNodeToHolder[colSemanticsNode.children[0].layoutNode]
+        assertNotNull(buttonHolder)
+        assertEquals(
+            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
+            buttonHolder!!.importantForAccessibility
+        )
+        assertEquals(buttonText, (buttonHolder.getChildAt(0) as Button).text)
+    }
+
+    @Test
+    fun testViewInterop_hoverEnterExit() {
+        val colTag = "ColTag"
+        val textTag = "TextTag"
+        val buttonText = "button text"
+        container.setContent {
+            Column(Modifier.testTag(colTag)) {
+                AndroidView(::Button) {
+                    it.text = buttonText
+                    it.setOnClickListener {}
+                }
+                BasicText(text = "text", modifier = Modifier.testTag(textTag))
+            }
+        }
+
+        val colSemanticsNode = rule.onNodeWithTag(colTag)
+            .fetchSemanticsNode("can't find node with tag $colTag")
+        rule.runOnUiThread {
+            val bounds = colSemanticsNode.children[0].boundsInRoot
+            val hoverEnter = MotionEvent.obtain(
+                0 /* downTime */, 0 /* eventTime */,
+                ACTION_HOVER_ENTER, (bounds.left + bounds.right) / 2 /* x */,
+                (bounds.top + bounds.bottom) / 2/* y */, 0 /* metaState*/
+            )
+            hoverEnter.source = InputDevice.SOURCE_CLASS_POINTER
+            assertTrue(androidComposeView.dispatchHoverEvent(hoverEnter))
+            assertEquals(
+                AndroidComposeViewAccessibilityDelegateCompat.InvalidId,
+                delegate.hoveredVirtualViewId
+            )
+        }
+        rule.runOnIdle {
+            verify(container, times(1)).requestSendAccessibilityEvent(
+                eq(androidComposeView),
+                argThat(
+                    ArgumentMatcher {
+                        it.eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
+                    }
+                )
+            )
+        }
+
+        val textNode = rule.onNodeWithTag(textTag)
+            .fetchSemanticsNode("can't find node with tag $textTag")
+        rule.runOnUiThread {
+            val bounds = textNode.boundsInRoot
+            val hoverEnter = MotionEvent.obtain(
+                0 /* downTime */, 0 /* eventTime */,
+                ACTION_HOVER_MOVE, (bounds.left + bounds.right) / 2 /* x */,
+                (bounds.top + bounds.bottom) / 2/* y */, 0 /* metaState*/
+            )
+            hoverEnter.source = InputDevice.SOURCE_CLASS_POINTER
+            assertTrue(androidComposeView.dispatchHoverEvent(hoverEnter))
+            assertEquals(
+                textNode.id,
+                delegate.hoveredVirtualViewId
+            )
+        }
+        // verify hover exit accessibility event is sent from the previously hovered view
+        rule.runOnIdle {
+            verify(container, times(1)).requestSendAccessibilityEvent(
+                eq(androidComposeView),
+                argThat(
+                    ArgumentMatcher {
+                        it.eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
+                    }
+                )
+            )
+        }
     }
 
     @Test
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 59e9c0d..74117be 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -22,7 +22,6 @@
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.FrameLayout
-import androidx.activity.ComponentActivity
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.size
@@ -44,37 +43,38 @@
 import androidx.compose.ui.semantics.CustomAccessibilityAction
 import androidx.compose.ui.semantics.LiveRegionMode
 import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.ScrollAxisRange
 import androidx.compose.ui.semantics.SemanticsModifierCore
 import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.semantics.SemanticsPropertyReceiver
-import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.SemanticsWrapper
 import androidx.compose.ui.semantics.collapse
-import androidx.compose.ui.semantics.heading
 import androidx.compose.ui.semantics.copyText
 import androidx.compose.ui.semantics.customActions
 import androidx.compose.ui.semantics.cutText
 import androidx.compose.ui.semantics.disabled
-import androidx.compose.ui.semantics.stateDescription
-import androidx.compose.ui.semantics.progressBarRangeInfo
 import androidx.compose.ui.semantics.dismiss
 import androidx.compose.ui.semantics.expand
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.heading
 import androidx.compose.ui.semantics.horizontalScrollAxisRange
 import androidx.compose.ui.semantics.liveRegion
 import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.semantics.onLongClick
 import androidx.compose.ui.semantics.pasteText
+import androidx.compose.ui.semantics.progressBarRangeInfo
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.setProgress
 import androidx.compose.ui.semantics.setSelection
 import androidx.compose.ui.semantics.setText
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.text
 import androidx.compose.ui.semantics.textSelectionRange
 import androidx.compose.ui.semantics.verticalScrollAxisRange
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.text.AnnotatedString
@@ -107,7 +107,7 @@
 @RunWith(AndroidJUnit4::class)
 class AndroidComposeViewAccessibilityDelegateCompatTest {
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
+    val rule = createAndroidComposeRule<TestActivity>()
 
     private lateinit var accessibilityDelegate: AndroidComposeViewAccessibilityDelegateCompat
     private lateinit var container: ViewGroup
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
index 6c03937..380b9b7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
@@ -19,12 +19,12 @@
 import android.content.Context
 import android.os.Bundle
 import android.view.ViewGroup
-import androidx.activity.ComponentActivity
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.unit.Constraints
 import androidx.lifecycle.Lifecycle
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -108,7 +108,7 @@
     }
 }
 
-class RecyclerViewActivity : ComponentActivity() {
+class RecyclerViewActivity : TestActivity() {
 
     private lateinit var recyclerView: RecyclerView
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt
new file mode 100644
index 0000000..384b376a
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import android.view.KeyEvent as AndroidKeyEvent
+import android.view.KeyEvent.ACTION_DOWN as KeyDown
+import android.view.KeyEvent.META_SHIFT_ON as Shift
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyEventToFocusDirectionTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private lateinit var owner: Owner
+
+    @Before
+    fun setup() {
+        rule.setContent {
+            owner = LocalView.current as Owner
+        }
+    }
+
+    @Test
+    fun left() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionLeft.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Left)
+    }
+
+    @Test
+    fun right() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionRight.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Right)
+    }
+
+    @Test
+    fun up() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionUp.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Up)
+    }
+
+    @Test
+    fun down() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionDown.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Down)
+    }
+
+    @Test
+    fun tab_next() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Tab.nativeKeyCode))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Next)
+    }
+
+    @Test
+    fun shiftTab_previous() {
+        // Arrange.
+        val keyEvent = KeyEvent(AndroidKeyEvent(0L, 0L, KeyDown, Key.Tab.nativeKeyCode, 0, Shift))
+
+        // Act.
+        val focusDirection = owner.getFocusDirection(keyEvent)
+
+        // Assert.
+        Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Previous)
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocus.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocus.kt
new file mode 100644
index 0000000..302d3dc
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocus.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import android.view.View
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection.Down
+import androidx.compose.ui.focus.FocusDirection.Left
+import androidx.compose.ui.focus.FocusDirection.Right
+import androidx.compose.ui.focus.FocusDirection.Up
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val invalid = "Not applicable to a 2D focus search."
+
+@MediumTest
+@RunWith(Parameterized::class)
+class TwoDimensionalFocusTraversalInitialFocus(private val focusDirection: FocusDirection) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun initParameters() = listOf(Left, Right, Up, Down)
+    }
+
+    @Test
+    fun initialFocus() {
+        // Arrange.
+        lateinit var view: View
+        lateinit var focusManager: FocusManager
+        val isFocused = MutableList(4) { mutableStateOf(false) }
+        rule.setContent {
+            view = LocalView.current
+            focusManager = LocalFocusManager.current
+            Column {
+                Row {
+                    FocusableBox(isFocused[0])
+                    FocusableBox(isFocused[1])
+                }
+                Row {
+                    FocusableBox(isFocused[2])
+                    FocusableBox(isFocused[3])
+                }
+            }
+        }
+        rule.runOnIdle { view.requestFocus() }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> assertThat(isFocused.values).containsExactly(false, false, true, false)
+                Down -> assertThat(isFocused.values).containsExactly(true, false, false, false)
+                Left -> assertThat(isFocused.values).containsExactly(false, false, false, true)
+                Right -> assertThat(isFocused.values).containsExactly(true, false, false, false)
+                else -> error(invalid)
+            }
+        }
+    }
+
+    @Test
+    fun initialFocus_whenThereIsOnlyOneFocusable() {
+        // Arrange.
+        var isFocused = mutableStateOf(false)
+        lateinit var view: View
+        lateinit var focusManager: FocusManager
+        rule.setContent {
+            view = LocalView.current
+            focusManager = LocalFocusManager.current
+            FocusableBox(isFocused)
+        }
+        rule.runOnIdle { view.requestFocus() }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle { assertThat(isFocused.value).isTrue() }
+    }
+
+    @Test
+    fun doesNotCrash_whenThereIsNoFocusable() {
+        // Arrange.
+        lateinit var view: View
+        lateinit var focusManager: FocusManager
+        rule.setContent {
+            view = LocalView.current
+            focusManager = LocalFocusManager.current
+            BasicText("Hello")
+        }
+        rule.runOnIdle { view.requestFocus() }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+    }
+
+    @Test
+    fun initialFocus_notTriggeredIfActiveElementIsNotRoot() {
+        // Arrange.
+        lateinit var focusManager: FocusManager
+        var isColumnFocused = false
+        val isFocused = MutableList(4) { mutableStateOf(false) }
+        val initialFocusRequester = FocusRequester()
+        rule.setContent {
+            focusManager = LocalFocusManager.current
+            Column(
+                Modifier
+                    .focusRequester(initialFocusRequester)
+                    .onFocusChanged { isColumnFocused = it.isFocused }
+                    .focusModifier()
+            ) {
+                Row {
+                    FocusableBox(isFocused[0])
+                    FocusableBox(isFocused[1])
+                }
+                Row {
+                    FocusableBox(isFocused[2])
+                    FocusableBox(isFocused[3])
+                }
+            }
+        }
+        rule.runOnIdle { initialFocusRequester.requestFocus() }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(isColumnFocused).isTrue()
+            assertThat(isFocused.values).containsExactly(false, false, false, false)
+        }
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun movesFocusAmongSiblingsDeepInTheFocusHierarchy() {
+        // Arrange.
+        lateinit var focusManager: FocusManager
+        val isFocused = MutableList(2) { mutableStateOf(false) }
+        val (item1, item2) = FocusRequester.createRefs()
+        val siblings = @Composable {
+            FocusableBox(isFocused = isFocused[0], focusRequester = item1)
+            FocusableBox(isFocused = isFocused[1], focusRequester = item2)
+        }
+        val initialFocusedItem = when (focusDirection) {
+            Up, Left -> item2
+            Down, Right -> item1
+            else -> error(invalid)
+        }
+        rule.setContent {
+            focusManager = LocalFocusManager.current
+            FocusableBox {
+                FocusableBox {
+                    FocusableBox {
+                        FocusableBox {
+                            FocusableBox {
+                                FocusableBox {
+                                    when (focusDirection) {
+                                        Up, Down -> Column { siblings() }
+                                        Left, Right -> Row { siblings() }
+                                        else -> error(invalid)
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        rule.runOnIdle { initialFocusedItem.requestFocus() }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up, Left -> assertThat(isFocused.values).containsExactly(true, false)
+                Down, Right -> assertThat(isFocused.values).containsExactly(false, true)
+                else -> error(invalid)
+            }
+        }
+    }
+}
+
+@Composable
+private fun FocusableBox(
+    isFocused: MutableState<Boolean>? = null,
+    x: Int = 0,
+    y: Int = 0,
+    width: Int = 10,
+    height: Int = 10,
+    focusRequester: FocusRequester? = null,
+    content: @Composable () -> Unit = {}
+) {
+    Layout(
+        content = content,
+        modifier = Modifier
+            .offset { IntOffset(x, y) }
+            .focusRequester(focusRequester ?: FocusRequester())
+            .onFocusChanged { if (isFocused != null) isFocused.value = it.isFocused }
+            .focusModifier(),
+        measurePolicy = remember(width, height) {
+            MeasurePolicy { measurables, _ ->
+                val constraint = Constraints(width, width, height, height)
+                layout(width, height) {
+                    measurables.forEach {
+                        val placeable = it.measure(constraint)
+                        placeable.place(0, 0)
+                    }
+                }
+            }
+        }
+    )
+}
+
+private val MutableList<MutableState<Boolean>>.values get() = this.map { it.value }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
new file mode 100644
index 0000000..652e5ca
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
@@ -0,0 +1,5078 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection.Down
+import androidx.compose.ui.focus.FocusDirection.Left
+import androidx.compose.ui.focus.FocusDirection.Right
+import androidx.compose.ui.focus.FocusDirection.Up
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val invalid = "Not applicable to a 2D focus search."
+
+@RunWith(Parameterized::class)
+class TwoDimensionalFocusTraversalThreeItemsTest(private val focusDirection: FocusDirection) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private lateinit var focusManager: FocusManager
+    private val initialFocus: FocusRequester = FocusRequester()
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "direction={0}")
+        fun initParameters() = listOf(Left, Right, Up, Down)
+    }
+
+    /**
+     *   __________                    __________       *                            __________
+     *  |   Next  |                   |  Closer |       *              ^            |   Next  |
+     *  |   Item  |                   |   Item  |       *              |            |   Item  |
+     *  |_________|                   |_________|       *          Direction        |_________|
+     *                        ____________              *          of Search
+     *                       |  focused  |              *              |
+     *                       |    Item   |              *              |
+     *                       |___________|              *         ____________
+     *                                                  *        |  focused  |       __________
+     *                                                  *        |    Item   |      |  Closer |
+     *          <---- Direction of Search ---           *        |___________|      |  Item   |
+     *                                                  *                           |_________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *   __________                    _________        *                            __________
+     *  |  Closer |                   |  Next  |        *                           |  Closer |
+     *  |   Item  |                   |  Item  |        *         ____________      |   Item  |
+     *  |_________|                   |________|        *        |  focused  |      |_________|
+     *           ____________                           *        |    Item   |
+     *          |  focused  |                           *        |___________|
+     *          |    Item   |                           *
+     *          |___________|                           *              |              _________
+     *                                                  *          Direction         |  Next  |
+     *          ---- Direction of Search --->           *          of Search         |  Item  |
+     *                                                  *              |             |________|
+     *                                                  *              V
+     */
+    @MediumTest
+    @Test
+    fun validItemIsPickedEvenThoughThereIsACloserItem1() {
+        // Arrange.
+        val (focusedItem, closerItem, nextItem) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 0, 20, 20)
+                    FocusableBox(nextItem, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 40, 20, 20)
+                    FocusableBox(nextItem, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 20, 20)
+                    FocusableBox(nextItem, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(nextItem, 30, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(nextItem.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *        __________
+     *                                                  *       |   Next  |          ^
+     *                        ____________              *       |   Item  |          |
+     *                       |  focused  |              *       |_________|       Direction
+     *                       |    Item   |              *                         of Search
+     *                       |___________|              *                            |
+     *   _________                     __________       *                            |
+     *  |  Next  |                    |  Closer |       *                       ____________
+     *  |  Item  |                    |   Item  |       *        __________    |  focused  |
+     *  |________|                    |_________|       *       |  Closer |    |    Item   |
+     *          <---- Direction of Search ---           *       |  Item   |    |___________|
+     *                                                  *       |_________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *           ____________                           *         __________
+     *          |  focused  |                           *        |  Closer |
+     *          |    Item   |                           *        |   Item  |     ____________
+     *          |___________|                           *        |_________|    |  focused  |
+     *    __________                    _________       *                       |    Item   |
+     *   |  Closer |                   |  Next  |       *                       |___________|
+     *   |   Item  |                   |  Item  |       *
+     *   |_________|                   |________|       *          _________          |
+     *                                                  *         |  Next  |      Direction
+     *          ---- Direction of Search --->           *         |  Item  |      of Search
+     *                                                  *         |________|          |
+     *                                                  *                             V
+     */
+    @LargeTest
+    @Test
+    fun validItemIsPickedEvenThoughThereIsACloserItem2() {
+        // Arrange.
+        val (focusedItem, closerItem, nextItem) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 30, 20, 20)
+                    FocusableBox(nextItem, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 40, 20, 20)
+                    FocusableBox(nextItem, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(nextItem, 40, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 20, 20)
+                    FocusableBox(nextItem, 0, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(nextItem.value).isTrue()
+        }
+    }
+
+    /**
+     *    _________                                     *   _________
+     *   |  Next  |                                     *  |  Next  |     ^
+     *   |  Item  |                                     *  |  Item  |     |
+     *   |________|                                     *  |________|  Direction
+     *                        ____________              *             of Search
+     *                       |  focused  |              *                 |
+     *                       |    Item   |              *                 |
+     *                       |___________|              *          ____________
+     *                               __________         *         |  focused  |
+     *                              |  Closer |         *         |    Item   |      __________
+     *                              |   Item  |         *         |___________|     |  Closer |
+     *                              |_________|         *                           |   Item  |
+     *          <---- Direction of Search ---           *                           |_________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *   __________                                     *                            __________
+     *  |  Closer |                                     *                           |  Closer |
+     *  |   Item  |                                     *           ____________    |   Item  |
+     *  |_________|                                     *          |  focused  |    |_________|
+     *          ____________                            *          |    Item   |
+     *         |  focused  |                            *          |___________|
+     *         |    Item   |                            *                 |
+     *         |___________|                            *   _________  Direction
+     *                                 _________        *  |  Next  |  of Search
+     *                                |  Next  |        *  |  Item  |     |
+     *                                |  Item  |        *  |________|     |
+     *                                |________|        *                 V
+     *          ---- Direction of Search --->           *
+     *                                                  *
+     *                                                  *
+     */
+    @LargeTest
+    @Test
+    fun validItemIsPickedEvenThoughThereIsACloserItem3() {
+        // Arrange.
+        val (focusedItem, closerItem, nextItem) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 60, 20, 20)
+                    FocusableBox(nextItem, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 40, 20, 20)
+                    FocusableBox(nextItem, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 20, 20)
+                    FocusableBox(nextItem, 40, 60, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 10, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 0, 20, 20)
+                    FocusableBox(nextItem, 30, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(nextItem.value).isTrue()
+        }
+    }
+
+    /**
+     *                                __________        *                               _________
+     *                               |  Closer |        *                     ^        |  Next  |
+     *                               |   Item  |        *                     |        |  Item  |
+     *                               |_________|        *                  Direction   |________|
+     *                        ____________              *                  of Search
+     *                       |  focused  |              *                     |
+     *                       |    Item   |              *                     |
+     *                       |___________|              *                ____________
+     *      _________                                   *               |  focused  |
+     *     |  Next  |                                   *   __________  |    Item   |
+     *     |  Item  |                                   *  |  Closer |  |___________|
+     *     |________|                                   *  |   Item  |
+     *          <---- Direction of Search ---           *  |_________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                _________         *    __________
+     *                               |  Next  |         *   |  Closer |
+     *                               |  Item  |         *   |   Item  |   ____________
+     *                               |________|         *   |_________|  |  focused  |
+     *           ____________                           *                |    Item   |
+     *          |  focused  |                           *                |___________|
+     *          |    Item   |                           *                      |
+     *          |___________|                           *                  Direction    _________
+     *   __________                                     *                  of Search   |  Next  |
+     *  |  Closer |                                     *                      |       |  Item  |
+     *  |   Item  |                                     *                      |       |________|
+     *  |_________|                                     *                      V
+     *          ---- Direction of Search --->           *
+     *                                                  *
+     *                                                  *
+     */
+    @LargeTest
+    @Test
+    fun validItemIsPickedEvenThoughThereIsACloserItem4() {
+        // Arrange.
+        val (focusedItem, closerItem, nextItem) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 0, 20, 20)
+                    FocusableBox(nextItem, 0, 60, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 40, 20, 20)
+                    FocusableBox(nextItem, 60, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 60, 20, 20)
+                    FocusableBox(nextItem, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 20, 20)
+                    FocusableBox(nextItem, 60, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(nextItem.value).isTrue()
+        }
+    }
+
+    /**
+     *                  __________                      *         ____________          ^
+     *                 |  Closer |                      *        |    Item   |          |
+     *                 |   Item  |                      *        |  in beam  |      Direction
+     *                 |_________|                      *        |___________|     of Search
+     *     __________         ____________              *                               |
+     *    |  Item   |        |  focused  |              *                               |
+     *    | in beam |        |    Item   |              *                            __________
+     *    |_________|        |___________|              *         ____________      |  Closer |
+     *                                                  *        |  focused  |      |  Item   |
+     *                                                  *        |    Item   |      |_________|
+     *          <---- Direction of Search ---           *        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         ____________
+     *                |  Closer |                       *        |  focused  |
+     *                |   Item  |                       *        |    Item   |       __________
+     *                |_________|                       *        |___________|      |  Closer |
+     *         ____________         __________          *                           |   Item  |
+     *        |  focused  |        |  Item   |          *                           |_________|
+     *        |    Item   |        | in beam |          *
+     *        |___________|        |_________|          *         ____________
+     *                                                  *        |    Item   |          |
+     *          ---- Direction of Search --->           *        |  in beam  |      Direction
+     *                                                  *        |___________|      of Search
+     *                                                  *                               |
+     *                                                  *                               V
+     */
+    @MediumTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis1() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                      ___________                 *              _________        ^
+     *                     |  Closer  |                 *             |  Item  |        |
+     *                     |   Item   |                 *             | in beam|     Direction
+     *                     |__________|                 *             |________|     of Search
+     *   _______________                                *                               |
+     *  | Item in Beam |         ____________           *                               |
+     *  |______________|        |  focused  |           *                            __________
+     *                          |    Item   |           *        ____________       |  Closer |
+     *                          |___________|           *       |  focused  |       |  Item   |
+     *                                                  *       |    Item   |       |_________|
+     *          <---- Direction of Search ---           *       |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *        ____________
+     *                |  Closer |                       *       |  focused  |
+     *                |   Item  |                       *       |    Item   |        __________
+     *                |_________|                       *       |___________|       |  Closer |
+     *                              _______________     *                           |   Item  |
+     *         ____________        | Item in Beam |     *                           |_________|
+     *        |  focused  |        |______________|     *
+     *        |    Item   |                             *              _________
+     *        |___________|                             *             |  Item  |        |
+     *                                                  *             | in beam|    Direction
+     *          ---- Direction of Search --->           *             |________|    of Search
+     *                                                  *                               |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis2() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 20, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 40, 20, 20)
+                    FocusableBox(itemInBeam, 60, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 60, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 20, 20)
+                    FocusableBox(itemInBeam, 60, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                       ___________                *            _________          ^
+     *                      |  Closer  |                *           |  Item  |          |
+     *                      |   Item   |                *           | in beam|       Direction
+     *                      |__________|                *           |________|       of Search
+     *   _______________         ____________           *                               |
+     *  | Item in Beam |        |  focused  |           *                               |
+     *  |______________|        |    Item   |           *                            __________
+     *                          |___________|           *         ____________      |  Closer |
+     *                                                  *        |  focused  |      |  Item   |
+     *                                                  *        |    Item   |      |_________|
+     *          <---- Direction of Search ---           *        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         ____________
+     *                |  Closer |                       *        |  focused  |
+     *                |   Item  |                       *        |    Item   |       __________
+     *                |_________|                       *        |___________|      |  Closer |
+     *         ____________          _______________    *                           |   Item  |
+     *        |  focused  |         | Item in Beam |    *                           |_________|
+     *        |    Item   |         |______________|    *
+     *        |___________|                             *            _________
+     *                                                  *           |  Item  |          |
+     *          ---- Direction of Search --->           *           | in beam|      Direction
+     *                                                  *           |________|      of Search
+     *                                                  *                               |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis3() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 10, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 30, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 20)
+                    FocusableBox(itemInBeam, 10, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                          __________              *           _______             ^
+     *                         |  Closer |              *          | Item |             |
+     *                         |   Item  |              *          |  in  |          Direction
+     *                         |_________|              *          | Beam |          of Search
+     *                               ____________       *          |______|             |
+     *      _______________         |           |       *                               |
+     *     | Item in Beam |         |  focused  |       *                            __________
+     *     |______________|         |    Item   |       *         ____________      |  Closer |
+     *                              |___________|       *        |  focused  |      |  Item   |
+     *                                                  *        |    Item   |      |_________|
+     *          <---- Direction of Search ---           *        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         ____________
+     *                |  Closer |                       *        |  focused  |
+     *                |   Item  |                       *        |    Item   |       __________
+     *                |_________|                       *        |___________|      |  Closer |
+     *         ____________                             *                           |   Item  |
+     *        |           |           _______________   *                           |_________|
+     *        |  focused  |          | Item in Beam |   *
+     *        |    Item   |          |______________|   *            _______
+     *        |___________|                             *           | Item |            |
+     *                                                  *           |  in  |        Direction
+     *                                                  *           | Beam |        of Search
+     *         ---- Direction of Search --->            *           |______|            |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis4() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 30, 20, 20)
+                    FocusableBox(itemInBeam, 10, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 40, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 10, 20, 20)
+                    FocusableBox(itemInBeam, 10, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                        __________                *         _________             ^
+     *                       |  Closer |                *        |        |             |
+     *                       |   Item  |                *        |  Item  |          Direction
+     *                       |_________|                *        | in beam|          of Search
+     *                           ____________           *        |________|             |
+     *    _______________       |  focused  |           *                               |
+     *   | Item in Beam |       |    Item   |           *                            __________
+     *   |______________|       |___________|           *         ______________    |  Closer |
+     *                                                  *        |   focused   |    |  Item   |
+     *                                                  *        |    Item     |    |_________|
+     *          <---- Direction of Search ---           *        |_____________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         _____________
+     *                |  Closer |                       *        |   focused  |
+     *                |   Item  |                       *        |    Item    |       __________
+     *                |_________|                       *        |____________|      |  Closer |
+     *         ____________                             *                            |   Item  |
+     *        |  focused  |         _______________     *                            |_________|
+     *        |    Item   |        | Item in Beam |     *
+     *        |___________|        |______________|     *         _________
+     *                                                  *        |        |             |
+     *          ---- Direction of Search --->           *        |  Item  |         Direction
+     *                                                  *        | in beam|         of Search
+     *                                                  *        |        |             |
+     *                                                  *        |________|             V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis5() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 40, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                         __________               *                               ^
+     *                        |  Closer |               *      _________                |
+     *                        |   Item  |               *     |  Item  |             Direction
+     *                        |_________|               *     | in beam|             of Search
+     *                             ____________         *     |________|                |
+     *                            |  focused  |         *                               |
+     *    _______________         |    Item   |         *                           __________
+     *   |              |         |___________|         *                          |  Closer |
+     *   | Item in Beam |                               *          ____________    |  Item   |
+     *   |______________|                               *         |  focused  |    |_________|
+     *                                                  *         |    Item   |
+     *          <---- Direction of Search ---           *         |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *          ____________
+     *                |  Closer |                       *         |  focused  |
+     *                |   Item  |                       *         |    Item   |      __________
+     *                |_________|                       *         |___________|     |  Closer |
+     *         ____________                             *                           |   Item  |
+     *        |  focused  |                             *                           |_________|
+     *        |    Item   |         _______________     *
+     *        |___________|        |              |     *      _________
+     *                             | Item in Beam |     *     |  Item   |                |
+     *                             |______________|     *     | in beam |            Direction
+     *                                                  *     |________ |            of Search
+     *         ---- Direction of Search --->            *                                |
+     *                                                  *                                V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis6() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 10, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 40, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 40, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                  __________                      *         ____________            ^
+     *                 |  Closer |                      *        |    Item   |            |
+     *                 |   Item  |                      *        |  in beam  |        Direction
+     *     __________  |_________|________              *        |___________|       of Search
+     *    |  Item   |        |  focused  |              *                                 |
+     *    | in beam |        |    Item   |              *                     _________   |
+     *    |_________|        |___________|              *         ___________|  Closer |
+     *                                                  *        |  focused  |  Item   |
+     *                                                  *        |    Item   |_________|
+     *          <---- Direction of Search ---           *        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         ____________
+     *                |  Closer |                       *        |  focused  |
+     *                |   Item  |                       *        |    Item   |__________
+     *         _______|_________|   __________          *        |___________|  Closer |
+     *        |  focused  |        |  Item   |          *                    |   Item  |
+     *        |    Item   |        | in beam |          *                    |_________|
+     *        |___________|        |_________|          *
+     *                                                  *         ____________
+     *          ---- Direction of Search --->           *        |    Item   |          |
+     *                                                  *        |  in beam  |      Direction
+     *                                                  *        |___________|      of Search
+     *                                                  *                               |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis7() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 20, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 20, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *              _________        ^
+     *                        ___________               *             |  Item  |        |
+     *                       |  Closer  |               *             | in beam|     Direction
+     *   _______________     |   Item   |               *             |________|     of Search
+     *  | Item in Beam |     |__________|________       *                               |
+     *  |______________|            |  focused  |       *                               |
+     *                              |    Item   |       *                    __________
+     *                              |___________|       *        ___________|  Closer |
+     *                                                  *       |  focused  |  Item   |
+     *                                                  *       |    Item   |_________|
+     *          <---- Direction of Search ---           *       |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *            __________                            *        ____________
+     *           |  Closer |                            *       |  focused  |
+     *           |   Item  |        _______________     *       |    Item   |__________
+     *    _______|_________|       | Item in Beam |     *       |___________|  Closer |
+     *   |  focused  |             |______________|     *                   |   Item  |
+     *   |    Item   |                                  *                   |_________|
+     *   |___________|                                  *
+     *                                                  *              _________
+     *                                                  *             |  Item  |        |
+     *          ---- Direction of Search --->           *             | in beam|    Direction
+     *                                                  *             |________|    of Search
+     *                                                  *                               |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis8() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 10, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 10, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 10, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 20)
+                    FocusableBox(itemInBeam, 10, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                      ___________                 *            _________          ^
+     *                     |  Closer  |                 *           |        |          |
+     *                     |   Item   |                 *           |  Item  |       Direction
+     *   _______________   |__________|________         *           | in beam|       of Search
+     *  | Item in Beam |          |  focused  |         *           |________|          |
+     *  |______________|          |    Item   |         *                               |
+     *                            |___________|         *                     __________
+     *                                                  *         ___________|  Closer |
+     *                                                  *        |  focused  |  Item   |
+     *          <---- Direction of Search ---           *        |    Item   |_________|
+     *                                                  *        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         ____________
+     *                |  Closer |                       *        |  focused  |
+     *                |   Item  |                       *        |    Item   |__________
+     *         _______|_________|    _______________    *        |___________|  Closer |
+     *        |  focused  |         | Item in Beam |    *                    |   Item  |
+     *        |    Item   |         |______________|    *                    |_________|
+     *        |___________|                             *
+     *                                                  *            _________
+     *                                                  *           |        |          |
+     *          ---- Direction of Search --->           *           |  Item  |      Direction
+     *                                                  *           | in beam|      of Search
+     *                                                  *           |________|          |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis9() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 20, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 10, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 20, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 20)
+                    FocusableBox(itemInBeam, 10, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                            __________            *           _______             ^
+     *                           |  Closer |            *          | Item |             |
+     *                           |   Item  |            *          |  in  |          Direction
+     *                           |_________|__________  *          | Beam |          of Search
+     *      _______________              |           |  *          |______|             |
+     *     | Item in Beam |              |  focused  |  *                               |
+     *     |______________|              |    Item   |  *                     __________
+     *                                   |___________|  *        ____________|  Closer |
+     *                                                  *       |  focused   |  Item   |
+     *          <---- Direction of Search ---           *       |    Item    |_________|
+     *                                                  *       |____________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *        _____________
+     *                |  Closer |                       *       |   focused  |
+     *                |   Item  |                       *       |     Item   |__________
+     *         _______|_________|                       *       |____________|  Closer |
+     *        |           |           _______________   *                    |   Item  |
+     *        |  focused  |          | Item in Beam |   *                    |_________|
+     *        |    Item   |          |______________|   *
+     *        |___________|                             *           _______
+     *                                                  *          | Item |            |
+     *                                                  *          |  in  |        Direction
+     *         ---- Direction of Search --->            *          | Beam |        of Search
+     *                                                  *          |______|            |
+     *                                                  *                              V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis10() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 10, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 30, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 20)
+                    FocusableBox(itemInBeam, 10, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                        __________                *         _________             ^
+     *                       |  Closer |                *        |        |             |
+     *                       |   Item  |                *        |  Item  |          Direction
+     *                       |_________|______          *        | in beam|          of Search
+     *    _______________        |  focused  |          *        |________|             |
+     *   | Item in Beam |        |    Item   |          *                               |
+     *   |______________|        |___________|          *                      __________
+     *                                                  *         ____________|  Closer |
+     *                                                  *        |   focused  |  Item   |
+     *                                                  *        |    Item    |_________|
+     *          <---- Direction of Search ---           *        |____________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                  *         _____________
+     *                 __________                       *        |   focused  |
+     *                |  Closer |                       *        |    Item    |__________
+     *                |   Item  |                       *        |____________|  Closer |
+     *         _______|_________|                       *                     |   Item  |
+     *        |  focused  |         _______________     *                     |_________|
+     *        |    Item   |        | Item in Beam |     *
+     *        |___________|        |______________|     *         _________
+     *                                                  *        |        |             |
+     *          ---- Direction of Search --->           *        |  Item  |         Direction
+     *                                                  *        | in beam|         of Search
+     *                                                  *        |________|             |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis11() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 30, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                          __________              *    _________                  ^
+     *                         |  Closer |              *   |  Item  |                  |
+     *                         |   Item  |              *   | in beam|               Direction
+     *                         |_________|_________     *   |________|               of Search
+     *                                |  focused  |     *                               |
+     *    _______________             |    Item   |     *                               |
+     *   | Item in Beam |             |___________|     *                     __________
+     *   |______________|                               *         ___________|  Closer |
+     *                                                  *        |  focused  |  Item   |
+     *          <---- Direction of Search ---           *        |    Item   |_________|
+     *                                                  *        |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *            __________                            *         ____________
+     *           |  Closer |                            *        |  focused  |
+     *           |   Item  |                            *        |    Item   |__________
+     *    _______|_________|                            *        |___________|  Closer |
+     *   |  focused  |                                  *                    |   Item  |
+     *   |    Item   |              _______________     *                    |_________|
+     *   |___________|             | Item in Beam |     *
+     *                             |______________|     *    _________
+     *                                                  *   |  Item  |                  |
+     *         ---- Direction of Search --->            *   | in beam|              Direction
+     *                                                  *   |________|              of Search
+     *                                                  *                               |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis12() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 10, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *     __________         ____________              *          ^           ____________
+     *    |  Item   |        |  focused  |              *          |          |    Item   |
+     *    | in beam |        |    Item   |              *      Direction      |  in beam  |
+     *    |_________|        |___________|              *     of Search       |___________|
+     *                   __________                     *          |
+     *                  |  Closer |                     *          |
+     *                  |   Item  |                     *       __________
+     *                  |_________|                     *      |  Closer |     ____________
+     *                                                  *      |  Item   |    |  focused  |
+     *                                                  *      |_________|    |    Item   |
+     *          <---- Direction of Search ---           *                     |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                  *                      ____________
+     *         ____________            __________       *                     |  focused  |
+     *        |  focused  |           |  Item   |       *       __________    |    Item   |
+     *        |    Item   |           | in beam |       *      |  Closer |    |___________|
+     *        |___________|           |_________|       *      |   Item  |
+     *                 __________                       *      |_________|
+     *                |  Closer |                       *
+     *                |   Item  |                       *                      ____________
+     *                |_________|                       *          |          |    Item   |
+     *                                                  *      Direction      |  in beam  |
+     *        ---- Direction of Search --->             *      of Search      |___________|
+     *                                                  *          |
+     *                                                  *          V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis13() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 30, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *   _______________                                *         ^                 _________
+     *  | Item in Beam |          ____________          *         |                |  Item  |
+     *  |______________|         |  focused  |          *      Direction           | in beam|
+     *                           |    Item   |          *      of Search           |________|
+     *                           |___________|          *         |
+     *                     ___________                  *         |
+     *                    |  Closer  |                  *      __________
+     *                    |   Item   |                  *     |  Closer |     ____________
+     *                    |__________|                  *     |  Item   |    |  focused  |
+     *                                                  *     |_________|    |    Item   |
+     *          <---- Direction of Search ---           *                    |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                _______________   *                     ____________
+     *         ____________          | Item in Beam |   *                    |  focused  |
+     *        |  focused  |          |______________|   *     __________     |    Item   |
+     *        |    Item   |                             *    |  Closer |     |___________|
+     *        |___________|                             *    |   Item  |
+     *                  __________                      *    |_________|
+     *                 |  Closer |                      *
+     *                 |   Item  |                      *                           _________
+     *                 |_________|                      *        |                 |  Item  |
+     *                                                  *    Direction             | in beam|
+     *         ---- Direction of Search --->            *    of Search             |________|
+     *                                                  *        |
+     *                                                  *        V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis14() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 40, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 40, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 40, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *         ^             _________
+     *   _______________              ____________      *         |            |  Item  |
+     *  | Item in Beam |             |  focused  |      *     Direction        | in beam|
+     *  |______________|             |    Item   |      *     of Search        |________|
+     *                               |___________|      *         |
+     *                         ___________              *         |
+     *                        |  Closer  |              *     __________
+     *                        |   Item   |              *    |  Closer |     ____________
+     *                        |__________|              *    |  Item   |    |  focused  |
+     *                                                  *    |_________|    |    Item   |
+     *          <---- Direction of Search ---           *                   |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                  *                    ____________
+     *     ____________             _______________     *                   |  focused  |
+     *    |  focused  |            | Item in Beam |     *     __________    |    Item   |
+     *    |    Item   |            |______________|     *    |  Closer |    |___________|
+     *    |___________|                                 *    |   Item  |
+     *             __________                           *    |_________|
+     *            |  Closer |                           *
+     *            |   Item  |                           *                       _________
+     *            |_________|                           *         |            |  Item  |
+     *                                                  *     Direction        | in beam|
+     *         ---- Direction of Search --->            *     of Search        |________|
+     *                                                  *         |
+     *                                                  *         V
+     *
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis15() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 40, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                  ____________    *       ^               _______
+     *   _______________               |           |    *       |              | Item |
+     *  | Item in Beam |               |  focused  |    *    Direction         |  in  |
+     *  |______________|               |    Item   |    *    of Search         | Beam |
+     *                                 |___________|    *       |              |______|
+     *                            __________            *       |
+     *                           |  Closer |            *    __________
+     *                           |   Item  |            *   |  Closer |       ____________
+     *                           |_________|            *   |  Item   |      |  focused  |
+     *                                                  *   |_________|      |    Item   |
+     *          <---- Direction of Search ---           *                    |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *      ____________                                *                    ____________
+     *     |           |              _______________   *                   |  focused  |
+     *     |  focused  |             | Item in Beam |   *     __________    |    Item   |
+     *     |    Item   |             |______________|   *    |  Closer |    |___________|
+     *     |___________|                                *    |   Item  |
+     *              __________                          *    |_________|
+     *             |  Closer |                          *
+     *             |   Item  |                          *                       _______
+     *             |_________|                          *        |             | Item |
+     *                                                  *    Direction         |  in  |
+     *           ---- Direction of Search --->          *    of Search         | Beam |
+     *                                                  *        |             |______|
+     *                                                  *        V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis16() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 40, 20, 20)
+                    FocusableBox(itemInBeam, 0, 10, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 40, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 40, 20, 20)
+                    FocusableBox(itemInBeam, 40, 10, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 40, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *           ^             _________
+     *                                 ____________     *           |            |  Item  |
+     *    _______________             |  focused  |     *        Direction       | in beam|
+     *   | Item in Beam |             |    Item   |     *        of Search       |________|
+     *   |______________|             |___________|     *           |
+     *                           __________             *           |
+     *                          |  Closer |             *        __________
+     *                          |   Item  |             *       |  Closer |       _____________
+     *                          |_________|             *       |  Item   |      |  focused   |
+     *                                                  *       |_________|      |    Item    |
+     *          <---- Direction of Search ---           *                        |____________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *       ____________                               *                          _____________
+     *      |  focused  |           _______________     *                         |  focused   |
+     *      |    Item   |          | Item in Beam |     *        __________       |    Item    |
+     *      |___________|          |______________|     *       |  Closer |       |____________|
+     *             __________                           *       |   Item  |
+     *            |  Closer |                           *       |_________|
+     *            |   Item  |                           *
+     *            |_________|                           *                          _________
+     *                                                  *           |             |  Item  |
+     *        ---- Direction of Search --->             *       Direction         | in beam|
+     *                                                  *       of Search         |________|
+     *                                                  *           |
+     *                                                  *           V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis17() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 10, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 40, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 10, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 30, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                            ____________          *           ^          _________
+     *                           |  focused  |          *           |         |  Item  |
+     *   _______________         |    Item   |          *        Direction    | in beam|
+     *  |              |         |___________|          *        of Search    |________|
+     *  | Item in Beam |                                *           |
+     *  |______________|                                *           |
+     *                       __________                 *       __________
+     *                      |  Closer |                 *      |  Closer |          ____________
+     *                      |   Item  |                 *      |  Item   |         |  focused  |
+     *                      |_________|                 *      |_________|         |    Item   |
+     *          <---- Direction of Search ---           *                          |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *        ____________                              *                           ____________
+     *       |  focused  |                              *                          |  focused  |
+     *       |    Item   |         _______________      *       __________         |    Item   |
+     *       |___________|        |              |      *      |  Closer |         |___________|
+     *                            | Item in Beam |      *      |   Item  |
+     *                            |______________|      *      |_________|
+     *                                                  *
+     *              __________                          *           |          __________
+     *             |  Closer |                          *       Direction     |  Item   |
+     *             |   Item  |                          *       of Search     | in beam |
+     *             |_________|                          *           |         |_________|
+     *                                                  *           V
+     *        ---- Direction of Search --->             *
+     *                                                  *
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis18() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 40, 20, 20)
+                    FocusableBox(itemInBeam, 0, 10, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 40, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 40, 20, 20)
+                    FocusableBox(itemInBeam, 40, 10, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 30, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *                                 ^
+     *     __________           ____________            *         ____________            |
+     *    |  Item   |          |  focused  |            *        |    Item   |        Direction
+     *    | in beam |          |    Item   |            *        |  in beam  |       of Search
+     *    |_________|     _____|___________|            *        |___________|            |
+     *                   |  Closer |                    *                     __________  |
+     *                   |   Item  |                    *                    |  Closer |
+     *                   |_________|                    *         ___________|  Item   |
+     *                                                  *        |  focused  |_________|
+     *          <---- Direction of Search ---           *        |    Item   |
+     *                                                  *        |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                  *                   ____________
+     *                                                  *                  |  focused  |
+     *                                                  *          ________|    Item   |
+     *         ____________            __________       *         | Closer |___________|
+     *        |  focused  |           |  Item   |       *         |  Item  |
+     *        |    Item   |           | in beam |       *         |________|
+     *        |___________|______     |_________|       *                   ____________
+     *                |  Closer |                       *           |      |    Item   |
+     *                |   Item  |                       *       Direction  |  in beam  |
+     *                |_________|                       *       of Search  |___________|
+     *                                                  *           |
+     *         ---- Direction of Search --->            *           V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis19() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 20, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *       ^             _________
+     *   _______________                                *       |            |  Item  |
+     *  | Item in Beam |            ____________        *    Direction       | in beam|
+     *  |______________|           |  focused  |        *    of Search       |________|
+     *                             |    Item   |        *       |
+     *                       ______|___________|        *       |
+     *                      |  Closer  |                *    __________
+     *                      |   Item   |                *   |  Closer |___________
+     *                      |__________|                *   |  Item   |  focused |
+     *                                                  *   |_________|    Item  |
+     *          <---- Direction of Search ---           *             |__________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                              _______________     *               ____________
+     *   ____________              | Item in Beam |     *              |  focused  |
+     *  |  focused  |              |______________|     *     _________|   Item    |
+     *  |    Item   |                                   *    |  Closer |___________|
+     *  |___________|______                             *    |   Item  |
+     *          |  Closer |                             *    |_________|
+     *          |   Item  |                             *
+     *          |_________|                             *         |            _________
+     *                                                  *     Direction       |  Item  |
+     *            ---- Direction of Search --->         *     of Search       | in beam|
+     *                                                  *         |           |________|
+     *                                                  *         V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis20() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 10, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 30, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *         ^             _________
+     *   _______________            ____________        *         |            |  Item  |
+     *  | Item in Beam |           |  focused  |        *     Direction        | in beam|
+     *  |______________|           |    Item   |        *     of Search        |________|
+     *                       ______|___________|        *         |
+     *                      |  Closer  |                *       __________
+     *                      |   Item   |                *      |  Closer |
+     *                      |__________|                *      |  Item   |_______________
+     *                                                  *      |_________|    focused   |
+     *          <---- Direction of Search ---           *                |      Item    |
+     *                                                  *                |______________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                  *                 _______________
+     *                                                  *                |   focused    |
+     *                                                  *       _________|     Item     |
+     *    ____________            _______________       *      |  Closer |______________|
+     *   |  focused  |           | Item in Beam |       *      |   Item  |
+     *   |    Item   |           |______________|       *      |_________|
+     *   |___________|______                            *
+     *           |  Closer |                            *         |             _________
+     *           |   Item  |                            *     Direction        |  Item  |
+     *           |_________|                            *     of Search        | in beam|
+     *                                                  *         |            |________|
+     *                                                  *         V
+     *          ---- Direction of Search --->           *
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis21() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 30, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                 ____________     *                   _______           ^
+     *      _______________           |           |     *                  | Item |           |
+     *     | Item in Beam |           |  focused  |     *                  |  in  |        Direction
+     *     |______________|           |    Item   |     *                  | Beam |        of Search
+     *                         _______|___________|     *                  |______|           |
+     *                        |  Closer |               *                                     |
+     *                        |   Item  |               *     __________
+     *                        |_________|               *    |  Closer |
+     *                                                  *    |  Item   |______________
+     *          <---- Direction of Search ---           *    |_________|   focused   |
+     *                                                  *              |     Item    |
+     *                                                  *              |_____________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *      ____________                                *               ______________
+     *     |           |              _______________   *              |   focused   |
+     *     |  focused  |             | Item in Beam |   *     _________|     Item    |
+     *     |    Item   |             |______________|   *    |  Closer |_____________|
+     *     |___________|______                          *    |   Item  |
+     *             |  Closer |                          *    |_________|
+     *             |   Item  |                          *
+     *             |_________|                          *                   _______         |
+     *                                                  *                  | Item |     Direction
+     *         ---- Direction of Search --->            *                  |  in  |     of Search
+     *                                                  *                  | Beam |         |
+     *                                                  *                  |______|         V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis22() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 10, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 40, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 10, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 30, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *              _________           ^
+     *                           ____________           *             |  Item  |           |
+     *    _______________       |  focused  |           *             | in beam|        Direction
+     *   | Item in Beam |       |    Item   |           *             |________|        of Search
+     *   |______________|    ___|___________|           *                                  |
+     *                      |  Closer |                 *    __________                               |
+     *                      |   Item  |                 *   |  Closer |
+     *                      |_________|                 *   |  Item   |____________
+     *                                                  *   |_________|  focused  |
+     *                                                  *             |    Item   |
+     *          <---- Direction of Search ---           *             |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         ____________                             *              ____________
+     *        |  focused  |           _______________   *             |  focused  |
+     *        |    Item   |          | Item in Beam |   *    _________|    Item   |
+     *        |___________|______    |______________|   *   |  Closer |___________|
+     *                |  Closer |                       *   |   Item  |                     |
+     *                |   Item  |                       *   |_________|                 Direction
+     *                |_________|                       *              _________       of Search
+     *                                                  *             |  Item  |           |
+     *                                                  *             | in beam|           V
+     *       ---- Direction of Search --->              *             |________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis23() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 10, 20, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 20, 0, 10, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 20, 20)
+                    FocusableBox(itemInBeam, 40, 10, 20, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 20, 40, 10, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *          _________            ^
+     *                             ____________         *         |  Item  |            |
+     *                            |  focused  |         *         | in beam|         Direction
+     *     _______________        |    Item   |         *         |________|         of Search
+     *    | Item in Beam |      __|___________|         *                               |
+     *    |______________|     |  Closer |              *                               |
+     *                         |   Item  |              *    __________
+     *                         |_________|              *   |  Closer |____________
+     *                                                  *   |  Item   |  focused  |
+     *          <---- Direction of Search ---           *   |_________|    Item   |
+     *                                                  *             |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *     ____________                                 *              ____________
+     *    |  focused  |                                 *             |  focused  |
+     *    |    Item   |            _______________      *    _________|    Item   |
+     *    |___________|______     | Item in Beam |      *   |  Closer |___________|
+     *            |  Closer |     |______________|      *   |   Item  |
+     *            |   Item  |                           *   |_________|
+     *            |_________|                           *                               |
+     *                                                  *          _________        Direction
+     *         ---- Direction of Search --->            *         |  Item  |        of Search
+     *                                                  *         | in beam|            |
+     *                                                  *         |________|            V
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemWithOverlappingMajorAxis24() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 10, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 10, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 20, 20)
+                    FocusableBox(itemInBeam, 40, 10, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 10, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                  __________                      *                               ^
+     *                 |  Closer |                      *                               |
+     *                 |   Item  |                      *         ____________      Direction
+     *                 |_________|                      *        |    Item   |      of Search
+     *     ______________________________               *        |  in beam  |          |
+     *    |  Item    |   |   Focused    |               *        |___________|          |
+     *    | in beam  |   |    Item      |               *        |           |       __________
+     *    |__________|___|______________|               *        |___________|      |  Closer |
+     *                                                  *        |  focused  |      |  Item   |
+     *                                                  *        |    Item   |      |_________|
+     *          <---- Direction of Search ---           *        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *              __________                          *
+     *             |  Closer |                          *         ____________
+     *             |   Item  |                          *        |  focused  |       __________
+     *             |_________|                          *        |    Item   |      |  Closer |
+     *         ______________________________           *        |___________|      |   Item  |
+     *        |  focused  |    |    Item    |           *        |           |      |_________|
+     *        |    Item   |    |   in beam  |           *        |___________|
+     *        |___________|____|____________|           *        |    Item   |          |
+     *                                                  *        |  in beam  |      Direction
+     *          ---- Direction of Search --->           *        |___________|      of Search
+     *                                                  *                               |
+     *                                                  *                               V
+     */
+    @MediumTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis1() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 0, 40, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 40, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 40, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 40)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 40, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 20, 30, 40, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 20, 20, 40)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *                             ^
+     *                                                  *                             |
+     *                  __________                      *                          Direction
+     *                 |  Closer |                      *                          of Search
+     *                 |   Item  |                      *                             |
+     *                 |_________|                      *            _________        |
+     *     ________________________________             *           |  Item  |    __________
+     *    |  Item in Beam  |  |  focused  |             *           | in beam|   |         |
+     *    |________________|__|    Item   |             *         __|________|   |  Closer |
+     *                     |______________|             *        |  |________|   |  Item   |
+     *                                                  *        |  focused  |   |_________|
+     *                                                  *        |    Item   |
+     *          <---- Direction of Search ---           *        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         ____________
+     *                |  Closer |                       *        |  focused  |
+     *                |   Item  |                       *        |    Item   |       __________
+     *                |_________|                       *        |   ________|      |  Closer |
+     *         ______________________________           *        |__|________|      |   Item  |
+     *        |  focused  |  | Item in Beam |           *           |  Item  |      |_________|
+     *        |    Item   |__|______________|           *           | in beam|          |
+     *        |______________|                          *           |________|      Direction
+     *                                                  *                           of Search
+     *          ---- Direction of Search --->           *                               |
+     *                                                  *                               V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis2() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 30, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 0, 30, 30, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 30)
+                    FocusableBox(itemInBeam, 10, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 20, 30, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 30)
+                    FocusableBox(itemInBeam, 10, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *                            ^
+     *                  __________                      *                            |
+     *                 |  Closer |                      *                         Direction
+     *                 |   Item  |                      *                         of Search
+     *                 |_________|                      *                            |
+     *                      _______________             *        _________           |
+     *     ________________|___  focused  |             *       |  Item  |       __________
+     *    |  Item in Beam  |  |   Item    |             *       | in beam|      |         |
+     *    |________________|__|___________|             *       |________|___   |  Closer |
+     *                                                  *       |________|  |   |  Item   |
+     *                                                  *       |  focused  |   |_________|
+     *          <---- Direction of Search ---           *       |    Item   |
+     *                                                  *       |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *
+     *                |  Closer |                       *        ____________
+     *                |   Item  |                       *       |  focused  |
+     *                |_________|                       *       |    Item   |       __________
+     *         _______________                          *       |_________  |      |  Closer |
+     *        |  focused   __|_______________           *       |________|__|      |   Item  |
+     *        |    Item   |  | Item in Beam |           *       |  Item  |         |_________|
+     *        |___________|__|______________|           *       | in beam|             |
+     *                                                  *       |________|         Direction
+     *          ---- Direction of Search --->           *                          of Search
+     *                                                  *                              |
+     *                                                  *                              V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis3() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 30, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 0, 40, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 30)
+                    FocusableBox(itemInBeam, 0, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 20, 40, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 30)
+                    FocusableBox(itemInBeam, 0, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                  __________                      *                                 ^
+     *                 |  Closer |                      *                                 |
+     *                 |   Item  |                      *                             Direction
+     *                 |_________|                      *                             of Search
+     *                      _______________             *           _________             |
+     *      _______________|___           |             *          |  Item  |             |
+     *     | Item in Beam  |  |  focused  |             *          | in beam|         __________
+     *     |_______________|__|    Item   |             *         _|________|__      |  Closer |
+     *                     |______________|             *        | |________| |      |  Item   |
+     *                                                  *        |   focused  |      |_________|
+     *          <---- Direction of Search ---           *        |    Item    |
+     *                                                  *        |____________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         ____________
+     *                |  Closer |                       *        |  focused   |
+     *                |   Item  |                       *        |    Item    |       __________
+     *                |_________|                       *        |  _________ |      |  Closer |
+     *         _______________                          *        |_|________|_|      |   Item  |
+     *        |            __|________________          *          |  Item  |        |_________|
+     *        |  focused  |  | Item in Beam  |          *          | in beam|            |
+     *        |    Item   |__|_______________|          *          |________|        Direction
+     *        |______________|                          *                            of Search
+     *                                                  *                                |
+     *                                                  *                                V
+     *         ---- Direction of Search --->            *
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis4() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 30, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 0, 40, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 20, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 40, 10, 20, 30)
+                    FocusableBox(itemInBeam, 10, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 20, 40, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 40, 10, 20, 30)
+                    FocusableBox(itemInBeam, 10, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                  __________                      *
+     *                 |  Closer |                      *    ____________                 ^
+     *                 |   Item  |                      *   |    Item   |                 |
+     *     ____________|_________|_______               *   |  in beam  |             Direction
+     *    |  Item    |   |   Focused    |               *   |___________|             of Search
+     *    | in beam  |   |    Item      |               *   |           |__________       |
+     *    |__________|___|______________|               *   |___________|  Closer |       |
+     *                                                  *   |  focused  |  Item   |
+     *                                                  *   |    Item   |_________|
+     *          <---- Direction of Search ---           *   |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *              __________                          *    ____________
+     *             |  Closer |                          *   |  focused  |__________
+     *             |   Item  |                          *   |    Item   |  Closer |
+     *         ____|_________|______________            *   |___________|   Item  |       |
+     *        |  focused  |   |    Item    |            *   |           |_________|   Direction
+     *        |    Item   |   |   in beam  |            *   |___________|             of Search
+     *        |___________|___|____________|            *   |    Item   |                 |
+     *                                                  *   |  in beam  |                 V
+     *          ---- Direction of Search --->           *   |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis5() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 10, 20, 40, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 20, 30, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 10, 20, 40, initialFocus)
+                    FocusableBox(closerItem, 20, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 40, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 20, 20, 30, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 20, 20, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *                                   ^
+     *                  __________                      *                                   |
+     *                 |  Closer |                      *                                Direction
+     *                 |   Item  |                      *            _________           of Search
+     *     ____________|_________|_________             *           |  Item  |__________    |
+     *    |  Item in Beam  |  |  focused  |             *           | in beam|         |    |
+     *    |________________|__|    Item   |             *         __|________|  Closer |
+     *                     |______________|             *        |  |________|  Item   |
+     *                                                  *        |  focused  |_________|
+     *                                                  *        |    Item   |
+     *          <---- Direction of Search ---           *        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                  *         ____________
+     *                 __________                       *        |  focused  |
+     *                |  Closer |                       *        |    Item   |__________
+     *                |   Item  |                       *        |   ________|         |
+     *         _______|_________|____________           *        |__|________|  Closer |    |
+     *        |  focused  |  | Item in Beam |           *           |  Item  |   Item  | Direction
+     *        |    Item   |__|______________|           *           | in beam|_________| of Search
+     *        |______________|                          *           |________|              |
+     *                                                  *                                   V
+     *          ---- Direction of Search --->           *
+     *                                                  *
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis6() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 20, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 0, 20, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 30)
+                    FocusableBox(itemInBeam, 10, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 20, 20, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 30)
+                    FocusableBox(itemInBeam, 10, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                  __________                      *
+     *                 |  Closer |                      *
+     *                 |   Item  |                      *                                   ^
+     *                 |_________|_________             *        _________                  |
+     *     ________________|___  focused  |             *       |  Item  |   __________  Direction
+     *    |  Item in Beam  |  |   Item    |             *       | in beam|  |         |  of Search
+     *    |________________|__|___________|             *       |________|__|  Closer |     |
+     *                                                  *       |________|  |  Item   |     |
+     *                                                  *       |  focused  |_________|
+     *          <---- Direction of Search ---           *       |    Item   |
+     *                                                  *       |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *        ____________
+     *                |  Closer |                       *       |  focused  |
+     *                |   Item  |                       *       |    Item   |
+     *         _______|_________|                       *       |           |__________
+     *        |  focused   __|_______________           *       |_________  |  Closer |      |
+     *        |    Item   |  | Item in Beam |           *       |________|__|   Item  |  Direction
+     *        |___________|__|______________|           *       |  Item  |  |_________|  of Search
+     *                                                  *       | in beam|                   |
+     *          ---- Direction of Search --->           *       |________|                   V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis7() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 20, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 0, 30, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 30)
+                    FocusableBox(itemInBeam, 0, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 20, 30, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 20, 10, 20, 30)
+                    FocusableBox(itemInBeam, 0, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                  __________                      *                                  ^
+     *                 |  Closer |                      *                                  |
+     *                 |   Item  |                      *                              Direction
+     *                 |_________|_________             *           _________          of Search
+     *      _______________|___           |             *          |  Item  |              |
+     *     | Item in Beam  |  |  focused  |             *          | in beam|  __________  |
+     *     |_______________|__|    Item   |             *         _|________|_|  Closer |
+     *                     |______________|             *        | |________| |  Item   |
+     *                                                  *        |   focused  |_________|
+     *          <---- Direction of Search ---           *        |    Item    |
+     *                                                  *        |____________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                 __________                       *         _____________
+     *                |  Closer |                       *        |  focused   |
+     *                |   Item  |                       *        |    Item    |__________
+     *         _______|_________|                       *        |  _________ |  Closer |
+     *        |            __|________________          *        |_|________|_|   Item  |
+     *        |  focused  |  | Item in Beam  |          *          |  Item  | |_________|    |
+     *        |    Item   |__|_______________|          *          | in beam|            Direction
+     *        |______________|                          *          |________|            of Search
+     *                                                  *                                    |
+     *                                                  *                                    V
+     *         ---- Direction of Search --->            *
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis8() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 20, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 0, 30, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 20, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 30)
+                    FocusableBox(itemInBeam, 10, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 20, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 30, 20)
+                    FocusableBox(itemInBeam, 20, 30, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 30)
+                    FocusableBox(itemInBeam, 10, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *           ^
+     *     ______________________________               *           |
+     *    |  Item    |   |   Focused    |               *       Direction         ____________
+     *    | in beam  |   |    Item      |               *       of Search        |    Item   |
+     *    |__________|___|______________|               *           |            |  in beam  |
+     *                  __________                      *           |            |___________|
+     *                 |  Closer |                      *        __________      |           |
+     *                 |   Item  |                      *       |  Closer |      |___________|
+     *                 |_________|                      *       |  Item   |      |  focused  |
+     *                                                  *       |_________|      |    Item   |
+     *          <---- Direction of Search ---           *                        |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         _____________________________            *
+     *        |  focused  |   |    Item    |            *                         ____________
+     *        |    Item   |   |   in beam  |            *        __________      |  focused  |
+     *        |___________|___|____________|            *       |  Closer |      |    Item   |
+     *             __________                           *       |   Item  |      |___________|
+     *            |  Closer |                           *       |_________|      |           |
+     *            |   Item  |                           *                        |___________|
+     *            |_________|                           *           |            |    Item   |
+     *                                                  *       Direction        |  in beam  |
+     *            ---- Direction of Search --->         *       of Search        |___________|
+     *                                                  *           |
+     *                                                  *           V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis9() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 10, 0, 40, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 30, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 10, 20, 40, initialFocus)
+                    FocusableBox(closerItem, 0, 20, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 20, 20)
+                    FocusableBox(itemInBeam, 20, 0, 30, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 40, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 30, 20, 20, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *         ^
+     *                                                  *         |
+     *     ________________________________             *      Direction
+     *    |  Item in Beam  |  |  focused  |             *      of Search
+     *    |________________|__|    Item   |             *         |
+     *                     |______________|             *         |              _________
+     *                  __________                      *     __________        |  Item  |
+     *                 |  Closer |                      *    |         |        | in beam|
+     *                 |   Item  |                      *    |  Closer |      __|________|
+     *                 |_________|                      *    |  Item   |     |  |________|
+     *                                                  *    |_________|     |  focused  |
+     *                                                  *                    |    Item   |
+     *          <---- Direction of Search ---           *                    |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         ______________________________           *                     ____________
+     *        |  focused  |  | Item in Beam |           *     __________     |  focused  |
+     *        |    Item   |__|______________|           *    |  Closer |     |    Item   |
+     *        |______________|                          *    |   Item  |     |   ________|
+     *                 __________                       *    |         |     |__|________|
+     *                |  Closer |                       *    |_________|        |  Item  |
+     *                |   Item  |                       *        |              | in beam|
+     *                |_________|                       *    Direction          |________|
+     *                                                  *    of Search
+     *          ---- Direction of Search --->           *        |
+     *                                                  *        V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis10() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 30, 20)
+                    FocusableBox(itemInBeam, 0, 0, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 40, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 30, 20)
+                    FocusableBox(itemInBeam, 20, 0, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 40, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *        ^
+     *                                                  *        |
+     *                      _______________             *     Direction
+     *     ________________|___  focused  |             *     of Search
+     *    |  Item in Beam  |  |   Item    |             *        |
+     *    |________________|__|___________|             *        |           _________
+     *                  __________                      *    __________     |  Item  |
+     *                 |  Closer |                      *   |         |     | in beam|
+     *                 |   Item  |                      *   |  Closer |     |________|___
+     *                 |_________|                      *   |  Item   |     |________|  |
+     *                                                  *   |_________|     |  focused  |
+     *          <---- Direction of Search ---           *                   |    Item   |
+     *                                                  *                   |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         _______________                          *
+     *        |  focused   __|_______________           *                    ____________
+     *        |    Item   |  | Item in Beam |           *                   |  focused  |
+     *        |___________|__|______________|           *    __________     |    Item   |
+     *                 __________                       *   |  Closer |     |_________  |
+     *                |  Closer |                       *   |   Item  |     |________|__|
+     *                |   Item  |                       *   |_________|     |  Item  |
+     *                |_________|                       *       |           | in beam|
+     *                                                  *   Direction       |________|
+     *           ---- Direction of Search --->          *   of Search
+     *                                                  *       |
+     *                                                  *       V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis11() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 30, 20)
+                    FocusableBox(itemInBeam, 0, 10, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 30, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 30, 20)
+                    FocusableBox(itemInBeam, 20, 10, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 30, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                      _______________             *           ^
+     *      _______________|___           |             *           |
+     *     | Item in Beam  |  |  focused  |             *       Direction
+     *     |_______________|__|    Item   |             *       of Search
+     *                     |______________|             *           |            _________
+     *                  __________                      *           |           |  Item  |
+     *                 |  Closer |                      *       __________      | in beam|
+     *                 |   Item  |                      *      |  Closer |     _|________|__
+     *                 |_________|                      *      |  Item   |    | |________| |
+     *                                                  *      |_________|    |   focused  |
+     *          <---- Direction of Search ---           *                     |    Item    |
+     *                                                  *                     |____________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         _______________                          *                      ____________
+     *        |            __|________________          *                     |  focused   |
+     *        |  focused  |  | Item in Beam  |          *       __________    |    Item    |
+     *        |    Item   |__|_______________|          *      |  Closer |    |  _________ |
+     *        |______________|                          *      |   Item  |    |_|________|_|
+     *                 __________                       *      |_________|      |  Item  |
+     *                |  Closer |                       *          |            | in beam|
+     *                |   Item  |                       *      Direction        |________|
+     *                |_________|                       *      of Search
+     *                                                  *          |
+     *         ---- Direction of Search --->            *          V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis12() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 0, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 40, 30, 20)
+                    FocusableBox(itemInBeam, 0, 10, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 20, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 40, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 40, 30, 20)
+                    FocusableBox(itemInBeam, 20, 10, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 40, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *     ______________________________               *                 ____________       ^
+     *    |  Item    |   |   Focused    |               *                |    Item   |       |
+     *    | in beam  |   |    Item      |               *                |  in beam  |   Direction
+     *    |__________|___|______________|               *                |___________|   of Search
+     *                 |  Closer |                      *       _________|           |       |
+     *                 |   Item  |                      *      |  Closer |___________|       |
+     *                 |_________|                      *      |  Item   |  focused  |
+     *                                                  *      |_________|    Item   |
+     *          <---- Direction of Search ---           *                |___________|
+     *                                                  *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         _____________________________            *                 ____________
+     *        |  focused  |   |    Item    |            *       _________|  focused  |
+     *        |    Item   |   |   in beam  |            *      |  Closer |    Item   |
+     *        |___________|___|____________|            *      |   Item  |___________|
+     *            |  Closer |                           *      |_________|           |       |
+     *            |   Item  |                           *                |___________|   Direction
+     *            |_________|                           *                |    Item   |   of Search
+     *                                                  *                |  in beam  |       |
+     *          ---- Direction of Search --->           *                |___________|       V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis13() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 10, 0, 40, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 30, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 10, 20, 40, initialFocus)
+                    FocusableBox(closerItem, 0, 20, 20, 20)
+                    FocusableBox(itemInBeam, 20, 0, 20, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 20, 20)
+                    FocusableBox(itemInBeam, 20, 0, 30, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 20, 40, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 20, 20, 20, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *     ________________________________             *                                ^
+     *    |  Item in Beam  |  |  focused  |             *                _________       |
+     *    |________________|__|    Item   |             *   __________  |  Item  |    Direction
+     *                  ___|______________|             *  |         |  | in beam|    of Search
+     *                 |  Closer |                      *  |  Closer |__|________|       |
+     *                 |   Item  |                      *  |  Item   |  |________|       |
+     *                 |_________|                      *  |_________|  focused  |       |
+     *                                                  *            |    Item   |
+     *          <---- Direction of Search ---           *            |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         ______________________________           *             ____________
+     *        |  focused  |  | Item in Beam |           *  __________|  focused  |
+     *        |    Item   |__|______________|           *  |  Closer |           |
+     *        |______________|___                       *  |   Item  |   ________|        |
+     *                |  Closer |                       *  |         |__|________|    Direction
+     *                |   Item  |                       *  |_________|  |        |    of Search
+     *                |_________|                       *               |  Item  |        |
+     *                                                  *               | in beam|        V
+     *           ---- Direction of Search --->          *               |________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis14() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 30, 20)
+                    FocusableBox(itemInBeam, 0, 0, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 30, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 30, 20)
+                    FocusableBox(itemInBeam, 20, 0, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 20, 30, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                      _______________             *
+     *     ________________|___  focused  |             *              _________         ^
+     *    |  Item in Beam  |  |   Item    |             *    _________|  Item  |         |
+     *    |________________|__|___________|             *   |         | in beam|      Direction
+     *                 |  Closer |                      *   |  Closer |________|__    of Search
+     *                 |   Item  |                      *   |  Item   |________|  |      |
+     *                 |_________|                      *   |_________|  focused  |
+     *                                                  *             |    Item   |
+     *          <---- Direction of Search ---           *             |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         _______________                          *              ____________
+     *        |  focused   __|_______________           *    _________|  focused  |
+     *        |    Item   |  | Item in Beam |           *   |         |    Item   |
+     *        |___________|__|______________|           *   |  Closer |_________  |
+     *                |  Closer |                       *   |   Item  |________|__|       |
+     *                |   Item  |                       *   |_________|        |      Direction
+     *                |_________|                       *             |  Item  |      of Search
+     *                                                  *             | in beam|          |
+     *          ---- Direction of Search --->           *             |________|          V
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis15() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 30, 20)
+                    FocusableBox(itemInBeam, 0, 10, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 20, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 20, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 20, 30, 20)
+                    FocusableBox(itemInBeam, 20, 10, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 20, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 20, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                      _______________             *               _________
+     *      _______________|___           |             *              |  Item  |
+     *     | Item in Beam  |  |  focused  |             *   __________ | in beam|         ^
+     *     |_______________|__|    Item   |             *  |  Closer |_|________|__       |
+     *                  ___|______________|             *  |  Item   | |________| |   Direction
+     *                 |  Closer |                      *  |_________|   focused  |   of Search
+     *                 |   Item  |                      *            |    Item    |       |
+     *                 |_________|                      *            |____________|       |
+     *                                                  *
+     *       <---- Direction of Search ---              *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         _______________                          *             _____________
+     *        |            __|________________          *            |  focused   |
+     *        |  focused  |  | Item in Beam  |          *   _________|    Item    |       |
+     *        |    Item   |__|_______________|          *  |  Closer |  _________ |   Direction
+     *        |______________|___                       *  |   Item  |_|________|_|   of Search
+     *                |  Closer |                       *  |_________| |  Item  |         |
+     *                |   Item  |                       *              | in beam|         V
+     *                |_________|                       *              |________|
+     *                                                  *
+     *         ---- Direction of Search --->            *
+     */
+    @LargeTest
+    @Test
+    fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis16() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 20, 0, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 30, 20)
+                    FocusableBox(itemInBeam, 0, 10, 30, 10)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 20, 20, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 30)
+                    FocusableBox(itemInBeam, 30, 0, 10, 30)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 30, 20)
+                    FocusableBox(itemInBeam, 20, 10, 30, 10)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 20, 0, 30, 30, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 0, 10)
+                    FocusableBox(itemInBeam, 30, 20, 10, 30)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *     __________                                   *         ____________       __________
+     *    |  Closer |                                   *        |    Item   |      |  Closer |
+     *    |   Item  |                                   *        |  in beam  |      |   Item  |
+     *    |_________|                                   *        |___________|      |_________|
+     *     __________         ____________              *
+     *    |  Item   |        |  focused  |              *
+     *    | in beam |        |    Item   |              *                               ^
+     *    |_________|        |___________|              *         ____________          |
+     *                                                  *        |  focused  |      Direction
+     *                                                  *        |    Item   |      of Search
+     *          <---- Direction of Search ---           *        |___________|          |
+     *                                                  *                               |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                             __________           *
+     *                            |  Closer |           *
+     *                            |   Item  |           *         ____________          |
+     *                            |_________|           *        |  focused  |      Direction
+     *         ____________        __________           *        |    Item   |      of Search
+     *        |  focused  |       |  Item   |           *        |___________|          |
+     *        |    Item   |       | in beam |           *                               V
+     *        |___________|       |_________|           *
+     *                                                  *         ____________       __________
+     *          ---- Direction of Search --->           *        |    Item   |      |  Closer |
+     *                                                  *        |  in beam  |      |   Item  |
+     *                                                  *        |___________|      |_________|
+     */
+    @MediumTest
+    @Test
+    fun inBeamWinsOverOtherItemWithSameMajorAxisDistance1() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 30, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *                                                  *      __________       ____________
+     *     __________         ____________              *     |  Closer |      |    Item   |
+     *    |  Item   |        |  focused  |              *     |   Item  |      |  in beam  |
+     *    | in beam |        |    Item   |              *     |_________|      |___________|
+     *    |_________|        |___________|              *
+     *     __________                                   *
+     *    |  Closer |                                   *         ^
+     *    |   Item  |                                   *         |             ____________
+     *    |_________|                                   *     Direction        |  focused  |
+     *                                                  *     of Search        |    Item   |
+     *          <---- Direction of Search ---           *         |            |___________|
+     *                                                  *         |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *     ____________        __________               *          |            ____________
+     *    |  focused  |       |  Item   |               *      Direction       |  focused  |
+     *    |    Item   |       | in beam |               *      of Search       |    Item   |
+     *    |___________|       |_________|               *          |           |___________|
+     *                         __________               *          V
+     *                        |  Closer |               *
+     *                        |   Item  |               *       __________      ____________
+     *                        |_________|               *      |  Closer |     |    Item   |
+     *                                                  *      |   Item  |     |  in beam  |
+     *          ---- Direction of Search --->           *      |_________|     |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverOtherItemWithSameMajorAxisDistance2() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 30, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *     _____________                                *         ____________       __________
+     *    |   Closer   |                                *        |    Item   |      |         |
+     *    |    Item    |                                *        |  in beam  |      |  Closer |
+     *    |____________|                                *        |___________|      |   Item  |
+     *     __________         ____________              *                           |_________|
+     *    |  Item   |        |  focused  |              *
+     *    | in beam |        |    Item   |              *                               ^
+     *    |_________|        |___________|              *         ____________          |
+     *                                                  *        |  focused  |      Direction
+     *                                                  *        |    Item   |      of Search
+     *          <---- Direction of Search ---           *        |___________|          |
+     *                                                  *                               |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                          _____________           *
+     *                         |   Closer   |           *
+     *                         |    Item    |           *         ____________          |
+     *                         |____________|           *        |  focused  |      Direction
+     *         ____________        __________           *        |    Item   |      of Search
+     *        |  focused  |       |  Item   |           *        |___________|          |
+     *        |    Item   |       | in beam |           *                               V
+     *        |___________|       |_________|           *                            __________
+     *                                                  *         ____________      |         |
+     *          ---- Direction of Search --->           *        |    Item   |      |  Closer |
+     *                                                  *        |  in beam  |      |   Item  |
+     *                                                  *        |___________|      |_________|
+     */
+    @MediumTest
+    @Test
+    fun inBeamWinsOverOtherItemWithSameFarEdge1() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 30, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 30)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 30, 20)
+                    FocusableBox(itemInBeam, 40, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 30)
+                    FocusableBox(itemInBeam, 0, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *     __________         ____________              *      __________       ____________
+     *    |  Item   |        |  focused  |              *     |         |      |    Item   |
+     *    | in beam |        |    Item   |              *     |  Closer |      |  in beam  |
+     *    |_________|        |___________|              *     |   Item  |      |___________|
+     *                                                  *     |_________|
+     *     _____________                                *
+     *    |   Closer   |                                *         ^
+     *    |    Item    |                                *         |             ____________
+     *    |____________|                                *     Direction        |  focused  |
+     *                                                  *     of Search        |    Item   |
+     *         <---- Direction of Search ---            *         |            |___________|
+     *                                                  *         |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *         ____________        __________           *          |            ____________
+     *        |  focused  |       |  Item   |           *      Direction       |  focused  |
+     *        |    Item   |       | in beam |           *      of Search       |    Item   |
+     *        |___________|       |_________|           *          |           |___________|
+     *                          _____________           *          V
+     *                         |   Closer   |           *       __________
+     *                         |    Item    |           *      |         |      ____________
+     *                         |____________|           *      |  Closer |     |    Item   |
+     *                                                  *      |   Item  |     |  in beam  |
+     *        ---- Direction of Search --->             *      |_________|     |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverOtherItemWithSameFarEdge2() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 30, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 0, 20, 30)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 30, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 30)
+                    FocusableBox(itemInBeam, 30, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *         ___________                              *         ____________
+     *        |  Closer  |                              *        |    Item   |       __________
+     *        |   Item   |                              *        |  in beam  |      |  Closer |
+     *        |__________|                              *        |___________|      |   Item  |
+     *     __________         ____________              *                           |_________|
+     *    |  Item   |        |  focused  |              *
+     *    | in beam |        |    Item   |              *                               ^
+     *    |_________|        |___________|              *         ____________          |
+     *                                                  *        |  focused  |      Direction
+     *                                                  *        |    Item   |      of Search
+     *          <---- Direction of Search ---           *        |___________|          |
+     *                                                  *                               |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                         ___________              *
+     *                        |  Closer  |              *
+     *                        |   Item   |              *         ____________          |
+     *                        |__________|              *        |  focused  |      Direction
+     *         ____________        __________           *        |    Item   |      of Search
+     *        |  focused  |       |  Item   |           *        |___________|          |
+     *        |    Item   |       | in beam |           *                               V
+     *        |___________|       |_________|           *                            __________
+     *                                                  *         ____________      |  Closer |
+     *          ---- Direction of Search --->           *        |    Item   |      |   Item  |
+     *                                                  *        |  in beam  |      |_________|
+     *                                                  *        |___________|
+     */
+    @MediumTest
+    @Test
+    fun inBeamWinsOverOtherItemWithFarEdgeGreaterThanInBeamCloserEdge1() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 10, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *     __________         ____________              *                        ____________
+     *    |  Item   |        |  focused  |              *       __________      |    Item   |
+     *    | in beam |        |    Item   |              *      |  Closer |      |  in beam  |
+     *    |_________|        |___________|              *      |   Item  |      |___________|
+     *         ___________                              *      |_________|
+     *        |  Closer  |                              *
+     *        |   Item   |                              *          ^
+     *        |__________|                              *          |             ____________
+     *                                                  *      Direction        |  focused  |
+     *          <---- Direction of Search ---           *      of Search        |    Item   |
+     *                                                  *          |            |___________|
+     *                                                  *          |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *       ____________        __________             *          |             ____________
+     *      |  focused  |       |  Item   |             *      Direction        |  focused  |
+     *      |    Item   |       | in beam |             *      of Search        |    Item   |
+     *      |___________|       |_________|             *          |            |___________|
+     *                       ___________                *          V
+     *                      |  Closer  |                *       __________
+     *                      |   Item   |                *      |  Closer |       ____________
+     *                      |__________|                *      |   Item  |      |    Item   |
+     *                                                  *      |_________|      |  in beam  |
+     *            ---- Direction of Search --->         *                       |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverOtherItemWithFarEdgeGreaterThanInBeamCloserEdge2() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 10, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 10, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(closerItem.value).isFalse()
+            assertThat(focusedItem.value).isFalse()
+            assertThat(itemInBeam.value).isTrue()
+        }
+    }
+
+    /**
+     *               ___________                       *         ____________
+     *              |  Closer  |                       *        |    Item   |
+     *              |   Item   |                       *        |  in beam  |
+     *              |__________|                       *        |___________|       __________
+     *     __________              ____________        *                           |  Closer |
+     *    |  Item   |             |  focused  |        *                           |   Item  |
+     *    | in beam |             |    Item   |        *                           |_________|
+     *    |_________|             |___________|        *                                ^
+     *                                                 *         ____________           |
+     *                                                 *        |  focused  |       Direction
+     *          <---- Direction of Search ---          *        |    Item   |       of Search
+     *                                                 *        |___________|           |
+     *                                                 *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                   ___________                   *         ____________           |
+     *                  |  Closer  |                   *        |  focused  |       Direction
+     *                  |   Item   |                   *        |    Item   |       of Search
+     *                  |__________|                   *        |___________|           |
+     *    ____________              __________         *                                V
+     *   |  focused  |             |  Item   |         *                             _________
+     *   |    Item   |             | in beam |         *                            | Closer |
+     *   |___________|             |_________|         *                            |  Item  |
+     *                                                 *         ____________       |________|
+     *          ---- Direction of Search --->          *        |    Item   |
+     *                                                 *        |  in beam  |
+     *                                                 *        |___________|
+     */
+    @MediumTest
+    @Test
+    fun inBeamWinsOverOtherItemWithFarEdgeEqualToInBeamCloserEdge_forHorizontalSearch1() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 50, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 50, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 50, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 50, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            when (focusDirection) {
+                Left, Right -> {
+                    assertThat(closerItem.value).isFalse()
+                    assertThat(itemInBeam.value).isTrue()
+                }
+                Up, Down -> {
+                    assertThat(closerItem.value).isTrue()
+                    assertThat(itemInBeam.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *     __________              ____________        *                       ____________
+     *    |  Item   |             |  focused  |        *                      |    Item   |
+     *    | in beam |             |    Item   |        *                      |  in beam  |
+     *    |_________|             |___________|        *        __________    |___________|
+     *               ___________                       *       |  Closer |
+     *              |  Closer  |                       *       |   Item  |
+     *              |   Item   |                       *       |_________|
+     *              |__________|                       *            ^
+     *                                                 *            |          ____________
+     *          <---- Direction of Search ---          *        Direction     |  focused  |
+     *                                                 *        of Search     |    Item   |
+     *                                                 *            |         |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *    ____________              __________         *            |          ____________
+     *   |  focused  |             |  Item   |         *        Direction     |  focused  |
+     *   |    Item   |             | in beam |         *        of Search     |    Item   |
+     *   |___________|             |_________|         *            |         |___________|
+     *                   ___________                   *            V
+     *                  |  Closer  |                   *         _________
+     *                  |   Item   |                   *        | Closer |
+     *                  |__________|                   *        |  Item  |
+     *                                                 *        |________|     ____________
+     *          ---- Direction of Search --->          *                      |    Item   |
+     *                                                 *                      |  in beam  |
+     *                                                 *                      |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverOtherItemWithFarEdgeEqualToInBeamCloserEdge_forHorizontalSearch2() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 50, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 50, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 20, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 50, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 50, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            when (focusDirection) {
+                Left, Right -> {
+                    assertThat(closerItem.value).isFalse()
+                    assertThat(itemInBeam.value).isTrue()
+                }
+                Up, Down -> {
+                    assertThat(closerItem.value).isTrue()
+                    assertThat(itemInBeam.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *               ___________                      *         ____________
+     *              |  Closer  |                      *        |    Item   |
+     *              |   Item   |                      *        |  in beam  |
+     *              |__________|                      *        |___________|         _________
+     *     __________           ____________          *                             | Closer |
+     *    |  Item   |          |  focused  |          *                             |  Item  |
+     *    | in beam |          |    Item   |          *         ____________        |________|
+     *    |_________|          |___________|          *        |  focused  |            ^
+     *                                                *        |    Item   |            |
+     *                                                *        |___________|        Direction
+     *          <---- Direction of Search ---         *                             of Search
+     *                                                *                                 |
+     *                                                *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                ___________                     *                                 |
+     *               |  Closer  |                     *                             Direction
+     *               |   Item   |                     *         ____________        of Search
+     *               |__________|                     *        |  focused  |            |
+     *    ____________           __________           *        |    Item   |            V
+     *   |  focused  |          |  Item   |           *        |___________|         _________
+     *   |    Item   |          | in beam |           *                             | Closer |
+     *   |___________|          |_________|           *                             |  Item  |
+     *                                                *         ____________        |________|
+     *          ---- Direction of Search --->         *        |    Item   |
+     *                                                *        |  in beam  |
+     *                                                *        |___________|
+     */
+    @MediumTest
+    @Test
+    fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch1() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 0, 20, 20)
+                    FocusableBox(itemInBeam, 40, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            when (focusDirection) {
+                Left, Right -> {
+                    assertThat(closerItem.value).isFalse()
+                    assertThat(itemInBeam.value).isTrue()
+                }
+                Up, Down -> {
+                    assertThat(closerItem.value).isTrue()
+                    assertThat(itemInBeam.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                  ___________                   *         ____________
+     *                 |  Closer  |                   *        |    Item   |
+     *                 |   Item   |                   *        |  in beam  |
+     *                 |__________|                   *        |___________|
+     *     __________                ____________     *                              _________
+     *    |  Item   |               |  focused  |     *                             | Closer |
+     *    | in beam |               |    Item   |     *                             |  Item  |
+     *    |_________|               |___________|     *                             |________|
+     *                                                *         ____________            ^
+     *                                                *        |  focused  |            |
+     *          <---- Direction of Search ---         *        |    Item   |        Direction
+     *                                                *        |___________|        of Search
+     *                                                *                                 |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                   __________                   *
+     *                  | Closer  |                   *                                 |
+     *                  |  Item   |                   *         ____________        Direction
+     *                  |_________|                   *        |  focused  |        of Search
+     *    ____________               __________       *        |    Item   |            |
+     *   |  focused  |              |  Item   |       *        |___________|            V
+     *   |    Item   |              | in beam |       *                              _________
+     *   |___________|              |_________|       *                             | Closer |
+     *                                                *                             |  Item  |
+     *          ---- Direction of Search --->         *                             |________|
+     *                                                *         ____________
+     *                                                *        |    Item   |
+     *                                                *        |  in beam  |
+     *                                                *        |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch2() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 60, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 60, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            when (focusDirection) {
+                Left, Right -> {
+                    assertThat(closerItem.value).isFalse()
+                    assertThat(itemInBeam.value).isTrue()
+                }
+                Up, Down -> {
+                    assertThat(closerItem.value).isTrue()
+                    assertThat(itemInBeam.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                  ___________                   *         ____________
+     *                 |  Closer  |                   *        |    Item   |
+     *                 |   Item   |                   *        |  in beam  |
+     *                 |__________|                   *        |___________|
+     *     __________              ____________       *                              _________
+     *    |  Item   |             |  focused  |       *                             | Closer |
+     *    | in beam |             |    Item   |       *                             |  Item  |
+     *    |_________|             |___________|       *         ____________        |________|
+     *                                                *        |  focused  |            ^
+     *                                                *        |    Item   |            |
+     *          <---- Direction of Search ---         *        |___________|        Direction
+     *                                                *                             of Search
+     *                                                *                                 |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                __________                      *                                 |
+     *               | Closer  |                      *                             Direction
+     *               |  Item   |                      *         ____________        of Search
+     *               |_________|                      *        |  focused  |            |
+     *    ____________             __________         *        |    Item   |            V
+     *   |  focused  |            |  Item   |         *        |___________|         _________
+     *   |    Item   |            | in beam |         *                             | Closer |
+     *   |___________|            |_________|         *                             |  Item  |
+     *                                                *                             |________|
+     *          ---- Direction of Search --->         *         ____________
+     *                                                *        |    Item   |
+     *                                                *        |  in beam  |
+     *                                                *        |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch3() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 50, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 0, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 50, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 0, 20, 20)
+                    FocusableBox(itemInBeam, 50, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 50, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            when (focusDirection) {
+                Left, Right -> {
+                    assertThat(closerItem.value).isFalse()
+                    assertThat(itemInBeam.value).isTrue()
+                }
+                Up, Down -> {
+                    assertThat(closerItem.value).isTrue()
+                    assertThat(itemInBeam.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *     __________           ____________          *                    ____________
+     *    |  Item   |          |  focused  |          *                   |    Item   |
+     *    | in beam |          |    Item   |          *                   |  in beam  |
+     *    |_________|          |___________|          *     _________     |___________|
+     *               ___________                      *    | Closer |
+     *              |  Closer  |                      *    |  Item  |
+     *              |   Item   |                      *    |________|      ____________
+     *              |__________|                      *        ^          |  focused  |
+     *                                                *        |          |    Item   |
+     *          <---- Direction of Search ---         *    Direction      |___________|
+     *                                                *    of Search
+     *                                                *        |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *    ____________           __________           *        |
+     *   |  focused  |          |  Item   |           *    Direction
+     *   |    Item   |          | in beam |           *    of Search       ____________
+     *   |___________|          |_________|           *        |          |  focused  |
+     *                ___________                     *        V          |    Item   |
+     *               |  Closer  |                     *     _________     |___________|
+     *               |   Item   |                     *    | Closer |
+     *               |__________|                     *    |  Item  |
+     *                                                *    |________|      ____________
+     *        ---- Direction of Search --->           *                   |    Item   |
+     *                                                *                   |  in beam  |
+     *                                                *                   |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch4() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 20, 20, 20)
+                    FocusableBox(itemInBeam, 0, 30, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 40, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 20, 20, 20)
+                    FocusableBox(itemInBeam, 30, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            when (focusDirection) {
+                Left, Right -> {
+                    assertThat(closerItem.value).isFalse()
+                    assertThat(itemInBeam.value).isTrue()
+                }
+                Up, Down -> {
+                    assertThat(closerItem.value).isTrue()
+                    assertThat(itemInBeam.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *     __________                ____________     *                      ____________
+     *    |  Item   |               |  focused  |     *                     |    Item   |
+     *    | in beam |               |    Item   |     *                     |  in beam  |
+     *    |_________|               |___________|     *                     |___________|
+     *                  ___________                   *      _________
+     *                 |  Closer  |                   *     | Closer |
+     *                 |   Item   |                   *     |  Item  |
+     *                 |__________|                   *     |________|
+     *                                                *         ^            ____________
+     *         <---- Direction of Search ---          *         |           |  focused  |
+     *                                                *     Direction       |    Item   |
+     *                                                *     of Search       |___________|
+     *                                                *         |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *    ____________               __________       *
+     *   |  focused  |              |  Item   |       *         |
+     *   |    Item   |              | in beam |       *     Direction        ____________
+     *   |___________|              |_________|       *     of Search       |  focused  |
+     *                   __________                   *         |           |    Item   |
+     *                  | Closer  |                   *         V           |___________|
+     *                  |  Item   |                   *      _________
+     *                  |_________|                   *     | Closer |
+     *                                                *     |  Item  |
+     *          ---- Direction of Search --->         *     |________|
+     *                                                *                      ____________
+     *                                                *                     |    Item   |
+     *                                                *                     |  in beam  |
+     *                                                *                     |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch5() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 60, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 60, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            when (focusDirection) {
+                Left, Right -> {
+                    assertThat(closerItem.value).isFalse()
+                    assertThat(itemInBeam.value).isTrue()
+                }
+                Up, Down -> {
+                    assertThat(closerItem.value).isTrue()
+                    assertThat(itemInBeam.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *     __________              ____________       *                      ____________
+     *    |  Item   |             |  focused  |       *                     |    Item   |
+     *    | in beam |             |    Item   |       *                     |  in beam  |
+     *    |_________|             |___________|       *                     |___________|
+     *                  ___________                   *      _________
+     *                 |  Closer  |                   *     | Closer |
+     *                 |   Item   |                   *     |  Item  |
+     *                 |__________|                   *     |________|       ____________
+     *                                                *         ^           |  focused  |
+     *           <---- Direction of Search ---        *         |           |    Item   |
+     *                                                *     Direction       |___________|
+     *                                                *     of Search
+     *                                                *         |
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *    ____________             __________         *         |
+     *   |  focused  |            |  Item   |         *     Direction
+     *   |    Item   |            | in beam |         *     of Search        ____________
+     *   |___________|            |_________|         *         |           |  focused  |
+     *                __________                      *         V           |    Item   |
+     *               | Closer  |                      *      _________      |___________|
+     *               |  Item   |                      *     | Closer |
+     *               |_________|                      *     |  Item  |
+     *                                                *     |________|
+     *                                                *                      ____________
+     *       ---- Direction of Search --->            *                     |    Item   |
+     *                                                *                     |  in beam  |
+     *                                                *                     |___________|
+     */
+    @LargeTest
+    @Test
+    fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch6() {
+        // Arrange.
+        val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 50, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 30, 30, 20, 20)
+                    FocusableBox(itemInBeam, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 50, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 30, 20, 20)
+                    FocusableBox(itemInBeam, 30, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 20, 30, 20, 20)
+                    FocusableBox(itemInBeam, 50, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+                    FocusableBox(closerItem, 0, 20, 20, 20)
+                    FocusableBox(itemInBeam, 30, 50, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            when (focusDirection) {
+                Left, Right -> {
+                    assertThat(closerItem.value).isFalse()
+                    assertThat(itemInBeam.value).isTrue()
+                }
+                Up, Down -> {
+                    assertThat(closerItem.value).isTrue()
+                    assertThat(itemInBeam.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ____________   ____________   ____________  *    ____________
+     *   |  In Beam  |  |  In Beam  |  |  focused  |  *   |  In Beam  |
+     *   |  Farther  |  |   Closer  |  |    Item   |  *   |  Farther  |
+     *   |___________|  |___________|  |___________|  *   |___________|        ^
+     *                                                *    ____________        |
+     *         <---- Direction of Search ---          *   |  In Beam  |    Direction
+     *                                                *   |   Closer  |    of Search
+     *                                                *   |___________|        |
+     *                                                *    ____________        |
+     *                                                *   |  focused  |
+     *                                                *   |    Item   |
+     *                                                *   |___________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *    ____________   ____________   ____________  *    ____________
+     *   |  focused  |  |  In Beam  |  |  In Beam  |  *   |  focused  |
+     *   |    Item   |  |   Closer  |  |  Farther  |  *   |    Item   |
+     *   |___________|  |___________|  |___________|  *   |___________|        |
+     *                                                *    ____________        |
+     *        ---- Direction of Search --->           *   |  In Beam  |    Direction
+     *                                                *   |   Closer  |    of Search
+     *                                                *   |___________|        |
+     *                                                *    ____________        v
+     *                                                *   |  In Beam  |
+     *                                                *   |  Farther  |
+     *                                                *   |___________|
+     */
+    @MediumTest
+    @Test
+    fun closerItemWinsWhenThereAreMultipleItemsInBeam1() {
+        // Arrange.
+        val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 0, 20, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 0, 20, 20)
+                    FocusableBox(inBeamFarther, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 60, 20, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 0, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 0, 20, 20)
+                    FocusableBox(inBeamFarther, 60, 0, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 0, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 0, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(inBeamCloser.value).isTrue()
+            assertThat(inBeamFarther.value).isFalse()
+        }
+    }
+
+    /**
+     *                   ____________                 *
+     *                  |           |   ____________  *
+     *                  |  In Beam  |  |           |  *       ___________
+     *    ____________  |   Closer  |  |           |  *      | In Beam  |
+     *   |  In Beam  |  |___________|  |  focused  |  *      | Farther  |
+     *   |  Farther  |                 |    Item   |  *      |__________|               ^
+     *   |___________|                 |           |  *            _____________        |
+     *                                 |___________|  *           |  In Beam   |    Direction
+     *                                                *           |   Closer   |    of Search
+     *         <---- Direction of Search ---          *           |____________|        |
+     *                                                *   __________________            |
+     *                                                *  |     focused     |
+     *                                                *  |      Item       |
+     *                                                *  |_________________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *    ____________                                *   __________________
+     *   |           |                                *  |     focused     |
+     *   |           |                  ____________  *  |      Item       |
+     *   |  focused  |                 |  In Beam  |  *  |_________________|            |
+     *   |    Item   |   ____________  |  Farther  |  *            _____________        |
+     *   |           |  |           |  |___________|  *           |   In Beam  |    Direction
+     *   |           |  |  In Beam  |                 *           |    Closer  |    of Search
+     *   |___________|  |   Closer  |                 *           |____________|        |
+     *                  |___________|                 *       ___________               v
+     *                                                *      |  In Beam |
+     *        ---- Direction of Search --->           *      |  Farther |
+     *                                                *      |__________|
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsWhenThereAreMultipleItemsInBeam2() {
+        // Arrange.
+        val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 10, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 0, 20, 30)
+                    FocusableBox(inBeamFarther, 0, 20, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 60, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 20, 30, 30, 20)
+                    FocusableBox(inBeamFarther, 10, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 0, 20, 30)
+                    FocusableBox(inBeamFarther, 60, 10, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 20, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 10, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(inBeamCloser.value).isTrue()
+            assertThat(inBeamFarther.value).isFalse()
+        }
+    }
+
+    /**
+     *                   ____________   ____________  *        ___________
+     *    ____________  |  In Beam  |  |           |  *       | In Beam  |
+     *   |  In Beam  |  |   Closer  |  |  focused  |  *       | Farther  |
+     *   |  Farther  |  |___________|  |    Item   |  *       |__________|           ^
+     *   |___________|                 |           |  *          _____________       |
+     *                                 |___________|  *         |   In Beam  |   Direction
+     *                                                *         |    Closer  |   of Search
+     *         <---- Direction of Search ---          *         |____________|       |
+     *                                                *    ___________________       |
+     *                                                *   |      focused     |
+     *                                                *   |        Item      |
+     *                                                *   |__________________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                *    ___________________
+     *    ____________                                *   |      focused     |
+     *   |           |                  ____________  *   |        Item      |
+     *   |  focused  |   ____________  |  In Beam  |  *   |__________________|       |
+     *   |    Item   |  |  In Beam  |  |  Farther  |  *          _____________       |
+     *   |           |  |   Closer  |  |___________|  *         |  In Beam   |   Direction
+     *   |___________|  |___________|                 *         |   Closer   |   of Search
+     *                                                *         |____________|       |
+     *        ---- Direction of Search --->           *        ___________           v
+     *                                                *       |  In Beam |
+     *                                                *       |  Farther |
+     *                                                *       |__________|
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsWhenThereAreMultipleItemsInBeam3() {
+        // Arrange.
+        val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 0, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 0, 20, 20)
+                    FocusableBox(inBeamFarther, 0, 10, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 60, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 20, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 10, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 20, 20, 20)
+                    FocusableBox(inBeamFarther, 60, 10, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 20, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 10, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(inBeamCloser.value).isTrue()
+            assertThat(inBeamFarther.value).isFalse()
+        }
+    }
+
+    /**
+     *                                  ____________  *       ____________
+     *    ____________   ____________  |           |  *      |  In Beam  |
+     *   |  In Beam  |  |  In Beam  |  |  focused  |  *      |  Farther  |
+     *   |  Farther  |  |   Closer  |  |    Item   |  *      |___________|        ^
+     *   |___________|  |___________|  |           |  *       ____________        |
+     *                                 |___________|  *      |  In Beam  |    Direction
+     *                                                *      |   Closer  |    of Search
+     *         <---- Direction of Search ---          *      |___________|        |
+     *                                                *     ________________      |
+     *                                                *    |    focused    |
+     *                                                *    |      Item     |
+     *                                                *    |_______________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                *     ________________
+     *    ____________                                *    |    focused    |
+     *   |           |   ____________   ____________  *    |      Item     |
+     *   |  focused  |  |  In Beam  |  |  In Beam  |  *    |_______________|      |
+     *   |    Item   |  |   Closer  |  |  Farther  |  *       ____________        |
+     *   |           |  |___________|  |___________|  *      |  In Beam  |    Direction
+     *   |___________|                                *      |   Closer  |    of Search
+     *                                                *      |___________|        |
+     *        ---- Direction of Search --->           *       ____________        v
+     *                                                *      |  In Beam  |
+     *                                                *      |  Farther  |
+     *                                                *      |___________|
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsWhenThereAreMultipleItemsInBeam4() {
+        // Arrange.
+        val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 0, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 10, 20, 20)
+                    FocusableBox(inBeamFarther, 0, 10, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 60, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 10, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 10, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 10, 20, 20)
+                    FocusableBox(inBeamFarther, 60, 10, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 10, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 10, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(inBeamCloser.value).isTrue()
+            assertThat(inBeamFarther.value).isFalse()
+        }
+    }
+
+    /**
+     *                                  ____________  *
+     *                                 }           |  *            ___________
+     *    ____________                 |           |  *           | In Beam  |
+     *   |  In Beam  |   ____________  |  focused  |  *           | Farther  |
+     *   |  Farther  |  |  In Beam  |  |    Item   |  *           |__________|        ^
+     *   |___________|  |   Closer  |  |           |  *      ____________             |
+     *                  |___________|  |___________|  *     |  In Beam  |          Direction
+     *                                                *     |   Closer  |          of Search
+     *         <---- Direction of Search ---          *     |___________|             |
+     *                                                *      ____________________     |
+     *                                                *     |      focused      |
+     *                                                *     |        Item       |
+     *                                                *     |___________________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                                *      ____________________
+     *    ____________   ____________                 *     |      focused      |
+     *   |           |  |  In Beam  |   ____________  *     |        Item       |
+     *   |  focused  |  |   Closer  |  |  In Beam  |  *     |___________________|      |
+     *   |    Item   |  |___________|  |  Farther  |  *      ____________              |
+     *   |           |                 |___________|  *     |  In Beam  |          Direction
+     *   |           |                                *     |   Closer  |          of Search
+     *   |___________|                                *     |___________|              |
+     *                                                *           ___________          v
+     *          ---- Direction of Search --->         *          |  In Beam |
+     *                                                *          |  Farther |
+     *                                                *          |__________|
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsWhenThereAreMultipleItemsInBeam5() {
+        // Arrange.
+        val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 0, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 20, 20, 20)
+                    FocusableBox(inBeamFarther, 0, 10, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 0, 60, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 0, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 10, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 20, 20, 20)
+                    FocusableBox(inBeamFarther, 60, 10, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 0, 30, 20, 20)
+                    FocusableBox(inBeamFarther, 10, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(inBeamCloser.value).isTrue()
+            assertThat(inBeamFarther.value).isFalse()
+        }
+    }
+
+    /**
+     *                                  ____________  *
+     *                                 |           |  *          ___________
+     *    ____________                 |           |  *         | In Beam  |
+     *   |  In Beam  |                 |  focused  |  *         | Farther  |
+     *   |  Farther  |   ____________  |    Item   |  *         |__________|           ^
+     *   |___________|  |           |  |           |  *   ____________                 |
+     *                  |  In Beam  |  |           |  *  |  In Beam  |             Direction
+     *                  |   Closer  |  |___________|  *  |   Closer  |             of Search
+     *                  |___________|                 *  |___________|                 |
+     *                                                *      ___________________       |
+     *                                                *     |     focused      |
+     *         <---- Direction of Search ---          *     |       Item       |
+     *                                                *     |__________________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                   ____________                 *      ___________________
+     *    ____________  |           |                 *     |     focused      |
+     *   |           |  |  In Beam  |   ____________  *     |       Item       |
+     *   |  focused  |  |   Closer  |  |  In Beam  |  *     |__________________|       |
+     *   |    Item   |  |___________|  |  Farther  |  *   ____________                 |
+     *   |           |                 |___________|  *  |  In Beam  |             Direction
+     *   |           |                                *  |   Closer  |             of Search
+     *   |___________|                                *  |___________|                 |
+     *                                                *          ___________           v
+     *                                                *         |  In Beam |
+     *        ---- Direction of Search --->           *         |  Farther |
+     *                                                *         |__________|
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsWhenThereAreMultipleItemsInBeam6() {
+        // Arrange.
+        val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 0, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 20, 20, 30)
+                    FocusableBox(inBeamFarther, 0, 10, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 10, 60, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 0, 30, 30, 20)
+                    FocusableBox(inBeamFarther, 20, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 10, 20, 40, initialFocus)
+                    FocusableBox(inBeamCloser, 30, 0, 20, 30)
+                    FocusableBox(inBeamFarther, 60, 20, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 10, 0, 40, 20, initialFocus)
+                    FocusableBox(inBeamCloser, 0, 30, 30, 20)
+                    FocusableBox(inBeamFarther, 20, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(inBeamCloser.value).isTrue()
+            assertThat(inBeamFarther.value).isFalse()
+        }
+    }
+
+    /**
+     *                  ____________                 *              ____________            ^
+     *                 |           |                 *             |           |            |
+     *   ____________  |   Closer  |                 *             |  Farther  |        Direction
+     *  |           |  |___________|                 *             |___________|        of Search
+     *  |  Farther  |                                *        ____________                  |
+     *  |___________|                                *       |           |                  |
+     *                                 ____________  *       |   Closer  |
+     *                                |  focused  |  *       |___________|
+     *                                |    Item   |  *                                 ____________
+     *                                |___________|  *                                |  focused  |
+     *                                               *                                |    Item   |
+     *        <---- Direction of Search ---          *                                |___________|
+     *                                               *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                               *   ____________
+     *        ---- Direction of Search --->          *  |  focused  |
+     *   ____________                                *  |    Item   |
+     *  |  focused  |                                *  |___________|
+     *  |    Item   |                                *                          ____________
+     *  |___________|                                *                         |           |
+     *                                 ____________  *      |                  |   Closer  |
+     *                                |           |  *      |                  |___________|
+     *                  ____________  |  Farther  |  *   Direction        ____________
+     *                 |           |  |___________|  *   of Search       |           |
+     *                 |   Closer  |                 *      |            |  Farther  |
+     *                 |___________|                 *      v            |___________|
+     */
+    @MediumTest
+    @Test
+    fun closerItemWinsForItemsOutsideBeam1() {
+        // Arrange.
+        val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 40, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 0, 20, 20)
+                    FocusableBox(farther, 0, 10, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 40, 60, 20, 20, initialFocus)
+                    FocusableBox(closer, 0, 30, 20, 20)
+                    FocusableBox(farther, 10, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 40, 20, 20)
+                    FocusableBox(farther, 60, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 40, 30, 20, 20)
+                    FocusableBox(farther, 30, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(closer.value).isTrue()
+            assertThat(farther.value).isFalse()
+        }
+    }
+
+    /**
+     *                                               *              ____________            ^
+     *                                               *             |           |            |
+     *   ____________   ____________                 *             |  Farther  |        Direction
+     *  |           |  |           |                 *             |___________|        of Search
+     *  |  Farther  |  |   Closer  |                 *              ____________            |
+     *  |___________|  |___________|                 *             |           |            |
+     *                                 ____________  *             |   Closer  |
+     *                                |  focused  |  *             |___________|
+     *                                |    Item   |  *                                 ____________
+     *                                |___________|  *                                |  focused  |
+     *                                               *                                |    Item   |
+     *        <---- Direction of Search ---          *                                |___________|
+     *                                               *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                               *   ____________
+     *        ---- Direction of Search --->          *  |  focused  |
+     *   ____________                                *  |    Item   |
+     *  |  focused  |                                *  |___________|
+     *  |    Item   |                                *                    ____________
+     *  |___________|                                *                   |           |
+     *                  ____________   ____________  *      |            |   Closer  |
+     *                 |           |  |           |  *      |            |___________|
+     *                 |   Closer  |  |  Farther  |  *   Direction        ____________
+     *                 |___________|  |___________|  *   of Search       |           |
+     *                                               *      |            |  Farther  |
+     *                                               *      v            |___________|
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsForItemsOutsideBeam2() {
+        // Arrange.
+        val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 30, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 0, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 30, 60, 20, 20, initialFocus)
+                    FocusableBox(closer, 0, 30, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 60, 30, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 30, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(closer.value).isTrue()
+            assertThat(farther.value).isFalse()
+        }
+    }
+
+    /**
+     *   ____________                                *        ____________                  ^
+     *  |           |                                *       |           |                  |
+     *  |  Farther  |   ____________                 *       |  Farther  |              Direction
+     *  |___________|  |           |                 *       |___________|              of Search
+     *                 |   Closer  |                 *               ____________            |
+     *                 |___________|                 *              |           |            |
+     *                                 ____________  *              |   Closer  |
+     *                                |  focused  |  *              |___________|
+     *                                |    Item   |  *                                 ____________
+     *                                |___________|  *                                |  focused  |
+     *                                               *                                |    Item   |
+     *        <---- Direction of Search ---          *                                |___________|
+     *                                               *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                               *   ____________
+     *        ---- Direction of Search --->          *  |  focused  |
+     *   ____________                                *  |    Item   |
+     *  |  focused  |                                *  |___________|
+     *  |    Item   |                                *                  ____________
+     *  |___________|                                *                 |           |
+     *                  ____________                 *      |          |   Closer  |
+     *                 |           |                 *      |          |___________|
+     *                 |   Closer  |   ____________  *   Direction             ____________
+     *                 |___________|  |           |  *   of Search            |           |
+     *                                |  Farther  |  *      |                 |  Farther  |
+     *                                |___________|  *      v                 |___________|
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsForItemsOutsideBeam3() {
+        // Arrange.
+        val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 60, 40, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 10, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 40, 60, 20, 20, initialFocus)
+                    FocusableBox(closer, 10, 30, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 60, 40, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 40, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(closer.value).isTrue()
+            assertThat(farther.value).isFalse()
+        }
+    }
+
+    /**
+     *   ____________
+     *  |           |
+     *  |  Farther  |                                     ^
+     *  |___________|                                     |
+     *                  ____________                  Direction
+     *                 |           |                  of Search
+     *                 |   Closer  |                      |
+     *                 |___________|                      |
+     *                                 ____________
+     *                                |  focused  |
+     *                                |    Item   |
+     *                                |___________|
+     *
+     *        <---- Direction of Search ---
+     *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *
+     *        ---- Direction of Search --->
+     *   ____________
+     *  |  focused  |
+     *  |    Item   |
+     *  |___________|
+     *                  ____________                      |
+     *                 |           |                      |
+     *                 |   Closer  |                   Direction
+     *                 |___________|                   of Search
+     *                                 ____________       |
+     *                                |           |       v
+     *                                |  Farther  |
+     *                                |___________|
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsForItemsOutsideBeam4() {
+        // Arrange.
+        val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left, Up -> {
+                    FocusableBox(focusedItem, 60, 60, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Right, Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 60, 60, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(closer.value).isTrue()
+            assertThat(farther.value).isFalse()
+        }
+    }
+
+    /**
+     *          ____________                         *
+     *         |           |                         *
+     *         |  Farther  |                         *                                      ^
+     *         |___________|                         *                                      |
+     *                ____________                   *  ____________                    Direction
+     *               |           |                   * |           |                    of Search
+     *               |   Closer  |                   * |  Farther  |   ____________         |
+     *               |___________|                   * |___________|  |           |         |
+     *                                 ____________  *                |   Closer  |
+     *                                |  focused  |  *                |___________|
+     *                                |    Item   |  *                                 ____________
+     *                                |___________|  *                                |  focused  |
+     *                                               *                                |    Item   |
+     *        <---- Direction of Search ---          *                                |___________|
+     *                                               *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                               *   ____________
+     *        ---- Direction of Search --->          *  |  focused  |
+     *   ____________                                *  |    Item   |
+     *  |  focused  |                                *  |___________|
+     *  |    Item   |                                *                  ____________
+     *  |___________|                                *                 |           |
+     *                   ____________                *      |          |   Closer  |    ____________
+     *                  |           |                *      |          |___________|   |           |
+     *                  |   Closer  |                *   Direction                     |  Farther  |
+     *                  |___________|                *   of Search                     |___________|
+     *                           ____________        *      |
+     *                          |           |        *      v
+     *                          |  Farther  |        *
+     *                          |___________|        *
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsForItemsOutsideBeam5() {
+        // Arrange.
+        val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 60, 20, 20, initialFocus)
+                    FocusableBox(closer, 10, 30, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 60, 40, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 10, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 40, 60, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 60, 40, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(closer.value).isTrue()
+            assertThat(farther.value).isFalse()
+        }
+    }
+
+    /**
+     *                ____________                   *
+     *               |           |                   *
+     *               |  Farther  |                   *                                      ^
+     *               |___________|                   *                                      |
+     *                ____________                   *                                  Direction
+     *               |           |                   *                                  of Search
+     *               |   Closer  |                   *  ____________   ____________         |
+     *               |___________|                   * |           |  |           |         |
+     *                                 ____________  * |  Farther  |  |   Closer  |
+     *                                |  focused  |  * |___________|  |___________|
+     *                                |    Item   |  *                                 ____________
+     *                                |___________|  *                                |  focused  |
+     *                                               *                                |    Item   |
+     *        <---- Direction of Search ---          *                                |___________|
+     *                                               *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                               *   ____________
+     *        ---- Direction of Search --->          *  |  focused  |
+     *   ____________                                *  |    Item   |
+     *  |  focused  |                                *  |___________|
+     *  |    Item   |                                *                  ____________    ____________
+     *  |___________|                                *                 |           |   |           |
+     *                   ____________                *      |          |   Closer  |   |  Farther  |
+     *                  |           |                *      |          |___________|   |___________|
+     *                  |   Closer  |                *   Direction
+     *                  |___________|                *   of Search
+     *                   ____________                *      |
+     *                  |           |                *      v
+     *                  |  Farther  |                *
+     *                  |___________|                *
+     */
+    @LargeTest
+    @Test
+    fun closerItemWinsForItemsOutsideBeam6() {
+        // Arrange.
+        val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 30, 60, 20, 20, initialFocus)
+                    FocusableBox(closer, 0, 30, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 60, 30, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 0, 20, 20)
+                    FocusableBox(farther, 0, 0, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 30, 60, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 30, 20, 20)
+                    FocusableBox(farther, 60, 30, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(closer.value).isTrue()
+            assertThat(farther.value).isFalse()
+        }
+    }
+
+    /**
+     *                ____________                   *
+     *               |           |                   *
+     *               |  Farther  |                   *                                      ^
+     *               |___________|                   *                                      |
+     *           ____________                        *                 ____________     Direction
+     *          |           |                        *                |           |     of Search
+     *          |   Closer  |                        *  ____________  |   Closer  |         |
+     *          |___________|                        * |           |  |___________|         |
+     *                                 ____________  * |  Farther  |
+     *                                |  focused  |  * |___________|
+     *                                |    Item   |  *                                 ____________
+     *                                |___________|  *                                |  focused  |
+     *                                               *                                |    Item   |
+     *        <---- Direction of Search ---          *                                |___________|
+     *                                               *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *                                               *  ____________
+     *        ---- Direction of Search --->          * |  focused  |
+     *   ____________                                * |    Item   |
+     *  |  focused  |                                * |___________|
+     *  |    Item   |                                *                                  ____________
+     *  |___________|                                *                                 |           |
+     *                            ____________       *      |           ____________   |  Farther  |
+     *                           |           |       *      |          |           |   |___________|
+     *                           |   Closer  |       *   Direction     |   Closer  |
+     *                           |___________|       *   of Search     |___________|
+     *                      ____________             *      |
+     *                     |           |             *      v
+     *                     |  Farther  |             *
+     *                     |___________|             *
+     */
+    @MediumTest
+    @Test
+    fun fartherItemWinsWhenTheMinorAxisDistanceIsMuchSmaller() {
+        // Arrange.
+        val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(focusedItem, 40, 60, 20, 20, initialFocus)
+                    FocusableBox(closer, 0, 30, 20, 20)
+                    FocusableBox(farther, 10, 0, 20, 20)
+                }
+                Up -> {
+                    FocusableBox(focusedItem, 60, 40, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 0, 20, 20)
+                    FocusableBox(farther, 0, 10, 20, 20)
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 40, 30, 20, 20)
+                    FocusableBox(farther, 30, 60, 20, 20)
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+                    FocusableBox(closer, 30, 40, 20, 20)
+                    FocusableBox(farther, 60, 30, 20, 20)
+                }
+                else -> error(invalid)
+            }
+        }
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(closer.value).isFalse()
+            assertThat(farther.value).isTrue()
+        }
+    }
+
+    private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
+        setContent {
+            focusManager = LocalFocusManager.current
+            composable()
+        }
+        rule.runOnIdle { initialFocus.requestFocus() }
+    }
+}
+
+@Composable
+private fun FocusableBox(
+    isFocused: MutableState<Boolean>,
+    x: Int,
+    y: Int,
+    width: Int,
+    height: Int,
+    focusRequester: FocusRequester? = null,
+    content: @Composable () -> Unit = {}
+) {
+    Layout(
+        content = content,
+        modifier = Modifier
+            .offset { IntOffset(x, y) }
+            .focusRequester(focusRequester ?: FocusRequester())
+            .onFocusChanged { isFocused.value = it.isFocused }
+            .focusModifier(),
+        measurePolicy = remember(width, height) {
+            MeasurePolicy { measurables, _ ->
+                val constraint = Constraints(width, width, height, height)
+                layout(width, height) {
+                    measurables.forEach {
+                        it.measure(constraint).place(0, 0)
+                    }
+                }
+            }
+        }
+    )
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
new file mode 100644
index 0000000..04b5a7f
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
@@ -0,0 +1,4645 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection.Down
+import androidx.compose.ui.focus.FocusDirection.Left
+import androidx.compose.ui.focus.FocusDirection.Right
+import androidx.compose.ui.focus.FocusDirection.Up
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val invalid = "Not applicable to a 2D focus search."
+
+@RunWith(Parameterized::class)
+class TwoDimensionalFocusTraversalTwoItemsTest(private val focusDirection: FocusDirection) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    private lateinit var focusManager: FocusManager
+    private val initialFocus: FocusRequester = FocusRequester()
+    private val focusedItem = mutableStateOf(false)
+    private val candidate = mutableStateOf(false)
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "direction={0}")
+        fun initParameters() = listOf(Left, Right, Up, Down)
+    }
+
+    /**
+     *                       ____________
+     *                      | candidate |
+     *                      |___________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @MediumTest
+    @Test
+    fun nonOverlappingCandidate1() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 20, 30, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                         ____________
+     *                        | candidate |
+     *                        |___________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate2() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 20, 30, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                           ____________
+     *                          | candidate |
+     *                          |___________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate3() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 20, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 20, 30, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |   candidate   |
+     *                      |_______________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate4() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 20, 10, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ___________________
+     *                      |     candidate    |
+     *                      |__________________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate5() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 30, 10)
+            FocusableBox(focusedItem, 0, 20, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                               ________________
+     *                              |   candidate   |
+     *                              |_______________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate6() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 0, 20, 10)
+            FocusableBox(focusedItem, 0, 20, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up, Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                       ________________
+     *                                      |   candidate   |
+     *                                      |_______________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate7() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 20, 0, 20, 10)
+            FocusableBox(focusedItem, 0, 20, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up, Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                          ________________
+     *                                         |   candidate   |
+     *                                         |_______________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate8() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 30, 0, 20, 10)
+            FocusableBox(focusedItem, 0, 20, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up, Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                          ________________
+     *                                         |   candidate   |
+     *                       ________________  |_______________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate9() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 30, 0, 20, 10)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up, Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                          ________________
+     *                       ________________  |   candidate   |
+     *                      |  focusedItem  |  |_______________|
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate10() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 30, 0, 20, 10)
+            FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up, Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                          ________________
+     *                       ________________  |               |
+     *                      |  focusedItem  |  |   candidate   |
+     *                      |_______________|  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate11() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 30, 0, 20, 20)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                          ________________
+     *                       ________________  |               |
+     *                      |  focusedItem  |  |   candidate   |
+     *                      |_______________|  |               |
+     *                                         |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate12() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+            FocusableBox(candidate, 30, 0, 20, 30)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________   ________________
+     *                      |               |  |   candidate   |
+     *                      |  focusedItem  |  |_______________|
+     *                      |               |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate13() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 30, 0, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |               |   ________________
+     *                      |  focusedItem  |  |   candidate   |
+     *                      |               |  |_______________|
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate14() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+            FocusableBox(candidate, 30, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |               |
+     *                      |  focusedItem  |   ________________
+     *                      |               |  |   candidate   |
+     *                      |_______________|  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate15() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 30, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________   ________________
+     *                      |  focusedItem  |  |   candidate   |
+     *                      |_______________|  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate16() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 30, 0, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________   ________________
+     *                      |  focusedItem  |  |               |
+     *                      |_______________|  |   candidate   |
+     *                                         |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate17() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 30, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |   ________________
+     *                      |_______________|  |   candidate   |
+     *                                         |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate18() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 30, 5, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|   ________________
+     *                                         |   candidate   |
+     *                                         |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate19() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 30, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                                          ________________
+     *                                         |   candidate   |
+     *                                         |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate20() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 30, 20, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                                       ________________
+     *                                      |   candidate   |
+     *                                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate21() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 20, 20, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                               ________________
+     *                              |   candidate   |
+     *                              |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate22() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 20, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                       _____________________
+     *                      |      candidate     |
+     *                      |____________________|
+     */
+    @MediumTest
+    @Test
+    fun nonOverlappingCandidate23() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 0, 20, 30, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                       ________________
+     *                      |   candidate   |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate24() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 0, 20, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                         ______________
+     *                        |  candidate  |
+     *                        |_____________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate25() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 30, 10, initialFocus)
+            FocusableBox(candidate, 10, 20, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                         ____________
+     *                        | candidate |
+     *                        |___________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate26() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 30, 10, initialFocus)
+            FocusableBox(candidate, 10, 20, 10, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                       ____________
+     *                      | candidate |
+     *                      |___________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate27() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 30, 10, initialFocus)
+            FocusableBox(candidate, 0, 20, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                  _____________________
+     *                 |      candidate     |
+     *                 |____________________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate28() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 0, 20, 30, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Left, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                ________________
+     *               |   candidate   |
+     *               |_______________|
+     */
+    @MediumTest
+    @Test
+    fun nonOverlappingCandidate29() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 20, 20, 10)
+            FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *       ________________
+     *      |   candidate   |
+     *      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate30() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 20, 20, 10)
+            FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *    ________________
+     *   |   candidate   |
+     *   |_______________|
+     */
+    @MediumTest
+    @Test
+    fun nonOverlappingCandidate31() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 20, 20, 10)
+            FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *    ________________  |_______________|
+     *   |   candidate   |
+     *   |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate32() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 10, 20, 10)
+            FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *    ________________  |  focusedItem  |
+     *   |   candidate   |  |_______________|
+     *   |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate33() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 5, 20, 10)
+            FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ________________    ________________
+     *  |               |   |  focusedItem  |
+     *  |   candidate   |   |_______________|
+     *  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate34() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 15)
+            FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ________________   ________________
+     *  |   candidate   |  |  focusedItem  |
+     *  |_______________|  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate35() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ________________    ________________
+     *  |   candidate   |   |               |
+     *  |_______________|   |  focusedItem  |
+     *                      |               |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate36() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *   ________________   |               |
+     *  |   candidate   |   |  focusedItem  |
+     *  |_______________|   |               |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate37() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 10, 20, 10)
+            FocusableBox(focusedItem, 30, 0, 20, 30, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |               |
+     *   ________________   |  focusedItem  |
+     *  |   candidate   |   |               |
+     *  |_______________|   |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate38() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 10, 20, 10)
+            FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ________________
+     *  |               |    ________________
+     *  |   candidate   |   |  focusedItem  |
+     *  |               |   |_______________|
+     *  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate39() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 30)
+            FocusableBox(focusedItem, 30, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ________________
+     *   |               |   ________________
+     *   |   candidate   |  |  focusedItem  |
+     *   |_______________|  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate40() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 20)
+            FocusableBox(focusedItem, 30, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Up, Down, Right -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ________________
+     *   |   candidate   |   ________________
+     *   |_______________|  |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate41() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 30, 5, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ________________
+     *   |   candidate   |
+     *   |_______________|   ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate42() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 30, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ________________
+     *   |   candidate   |
+     *   |_______________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate43() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 30, 20, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *       ________________
+     *      |   candidate   |
+     *      |_______________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate44() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 20, 20, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *               ________________
+     *              |   candidate   |
+     *              |_______________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate45() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 10, 20, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                    ___________________
+     *                   |     candidate    |
+     *                   |__________________|
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun nonOverlappingCandidate46() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 30, 10)
+            FocusableBox(focusedItem, 10, 20, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ____________
+     *                      | candidate |
+     *                      |___________|____
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary1() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                         ____________
+     *                        | candidate |
+     *                       _|___________|__
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @MediumTest
+    @Test
+    fun candidateWithCommonBoundary2() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 5, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                           ____________
+     *                          | candidate |
+     *                       ___|___________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary3() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |   candidate   |
+     *                      |_______________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary4() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ___________________
+     *                      |     candidate    |
+     *                      |__________________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary5() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 30, 10)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                               ________________
+     *                              |   candidate   |
+     *                       _______|_______________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary6() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 0, 20, 10)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                       ________________
+     *                                      |   candidate   |
+     *                       _______________|_______________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary7() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 20, 0, 20, 10)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                       ________________
+     *                       _______________|   candidate   |
+     *                      |  focusedItem  |_______________|
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary8() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+            FocusableBox(candidate, 20, 0, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                       ________________
+     *                       _______________|               |
+     *                      |  focusedItem  |   candidate   |
+     *                      |_______________|_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary9() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+            FocusableBox(candidate, 20, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                       ________________
+     *                       _______________|               |
+     *                      |  focusedItem  |   candidate   |
+     *                      |_______________|               |
+     *                                      |_______________|
+     */
+    @MediumTest
+    @Test
+    fun candidateWithCommonBoundary10() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+            FocusableBox(candidate, 20, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________________________
+     *                      |               |   candidate   |
+     *                      |  focusedItem  |_______________|
+     *                      |               |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary11() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 20, 0, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |               |________________
+     *                      |  focusedItem  |   candidate   |
+     *                      |               |_______________|
+     *                      |_______________|
+     */
+    @MediumTest
+    @Test
+    fun candidateWithCommonBoundary12() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 20, 5, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |               |
+     *                      |  focusedItem  |________________
+     *                      |               |   candidate   |
+     *                      |_______________|_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary13() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 20, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________________________
+     *                      |  focusedItem  |   candidate   |
+     *                      |_______________|_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary14() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 20, 0, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________________________
+     *                      |  focusedItem  |               |
+     *                      |_______________|   candidate   |
+     *                                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary15() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 20, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |________________
+     *                      |_______________|   candidate   |
+     *                                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary16() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 20, 5, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|________________
+     *                                      |   candidate   |
+     *                                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary17() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 20, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|________
+     *                              |   candidate   |
+     *                              |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary18() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|_____
+     *                      |      candidate     |
+     *                      |____________________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary19() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 0, 10, 30, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                      |   candidate   |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary20() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 0, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                        |  candidate  |
+     *                        |_____________|     *
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary21() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 10, 10, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                        | candidate |
+     *                        |___________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary22() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 5, 10, 10, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                      | candidate |
+     *                      |___________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary23() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 0, 10, 10, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                  ____|_______________|
+     *                 |      candidate     |
+     *                 |____________________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary24() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 10, 0, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                ______|_______________|
+     *               |   candidate   |
+     *               |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary25() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 0, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *       _______________|_______________|
+     *      |   candidate   |
+     *      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary26() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 0, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                    ________________
+     *    _______________|  focusedItem  |
+     *   |   candidate   |_______________|
+     *   |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary27() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 5, 20, 10)
+            FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ________________________________
+     *  |               |  focusedItem  |
+     *  |   candidate   |_______________|
+     *  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary28() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 20)
+            FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ________________________________
+     *  |   candidate   |  focusedItem  |
+     *  |_______________|_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary29() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ________________________________
+     *  |   candidate   |               |
+     *  |_______________|  focusedItem  |
+     *                  |               |
+     *                  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary30() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                   ________________
+     *   _______________|               |
+     *  |   candidate   |  focusedItem  |
+     *  |_______________|               |
+     *                  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary31() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 5, 20, 10)
+            FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                   ________________
+     *                  |               |
+     *   _______________|  focusedItem  |
+     *  |   candidate   |               |
+     *  |_______________|_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary32() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 10, 20, 10)
+            FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ________________
+     *  |               |________________
+     *  |   candidate   |  focusedItem  |
+     *  |               |_______________|
+     *  |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary33() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 20)
+            FocusableBox(focusedItem, 20, 5, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ________________
+     *   |               |________________
+     *   |   candidate   |  focusedItem  |
+     *   |_______________|_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary34() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 20)
+            FocusableBox(focusedItem, 20, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ________________
+     *   |   candidate   |________________
+     *   |_______________|  focusedItem  |
+     *                   |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary35() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 20, 5, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ________________
+     *   |   candidate   |
+     *   |_______________|________________
+     *                   |  focusedItem  |
+     *                   |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary36() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 20, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *               ________________
+     *              |   candidate   |
+     *              |_______________|________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary37() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 10, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                    ___________________
+     *                   |     candidate    |
+     *                   |__________________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithCommonBoundary38() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ____________
+     *                      | candidate |
+     *                      |___________|____
+     *                      |___________|   |
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate1() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 10, 15)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                         ____________
+     *                        | candidate |
+     *                       _|___________|__
+     *                      | |___________| |
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @MediumTest
+    @Test
+    fun overlappingCandidate2() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 5, 0, 10, 15)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                           ____________
+     *                          | candidate |
+     *                       ___|___________|
+     *                      |   |___________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate3() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 0, 10, 15)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |   candidate   |
+     *                      |_______________|
+     *                      |_______________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate4() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 15)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ___________________
+     *                      |     candidate    |
+     *                      |________________  |
+     *                      |_______________|__|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate5() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 25, 15)
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                       ________________
+     *                       _______________|__  candidate   |
+     *                      |  focusedItem  |__|_____________|
+     *                      |__________________|
+     */
+    @MediumTest
+    @Test
+    fun overlappingCandidate6() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 0, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                       __________________
+     *                       _______________|___              |
+     *                      |  focusedItem  |  |  candidate   |
+     *                      |_______________|__|______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate7() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                                       _________________
+     *                       _______________|___             |
+     *                      |  focusedItem  |  | candidate   |
+     *                      |_______________|__|             |
+     *                                      |________________|
+     */
+    @MediumTest
+    @Test
+    fun overlappingCandidate8() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       __________________________________
+     *                      |               |  |  candidate   |
+     *                      |  focusedItem  |__|______________|
+     *                      |                  |
+     *                      |__________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate9() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 10, 0, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ___________________
+     *                      |                __|________________
+     *                      |  focusedItem  |  |   candidate   |
+     *                      |               |__|_______________|
+     *                      |__________________|
+     */
+    @MediumTest
+    @Test
+    fun overlappingCandidate10() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 10, 5, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ___________________
+     *                      |                  |
+     *                      |  focusedItem   __|________________
+     *                      |               |  |   candidate   |
+     *                      |_______________|__|_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate11() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 10, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ___________________________________
+     *                      |  focusedItem  |  |   candidate   |
+     *                      |_______________|__|_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate12() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 0, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ___________________________________
+     *                      |  focusedItem  |  |               |
+     *                      |_______________|__|   candidate   |
+     *                                      |__________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate13() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ___________________
+     *                      |  focusedItem   __|________________
+     *                      |_______________|__|   candidate   |
+     *                                      |__________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate14() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+            FocusableBox(candidate, 10, 5, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Right, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|_____
+     *                      |_______________|    |
+     *                      |      candidate     |
+     *                      |____________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate15() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 15, initialFocus)
+            FocusableBox(candidate, 0, 10, 30, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |_______________|
+     *                      |_______________|
+     *                      |   candidate   |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate16() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 15, initialFocus)
+            FocusableBox(candidate, 0, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |  _____________|
+     *                      |_|_____________|
+     *                        |  candidate  |
+     *                        |_____________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate17() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+            FocusableBox(candidate, 10, 10, 10, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |  ____________ |
+     *                      |_|___________|_|
+     *                        | candidate |
+     *                        |___________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate18() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 15, initialFocus)
+            FocusableBox(candidate, 5, 10, 10, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                      |____________   |
+     *                      |___________|___|
+     *                      | candidate |
+     *                      |___________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate19() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 20, 15, initialFocus)
+            FocusableBox(candidate, 0, 10, 10, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                       ________________
+     *                      |  focusedItem  |
+     *                  ____|_______________|
+     *                 |    |_______________|
+     *                 |      candidate     |
+     *                 |____________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate20() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 10, 0, 10, 15, initialFocus)
+            FocusableBox(candidate, 0, 10, 20, 10)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                    ___________________
+     *    _______________|___  focusedItem  |
+     *   |   candidate   |__|_______________|
+     *   |__________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate21() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 5, 20, 10)
+            FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Down -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ___________________________________
+     *  |               |  |  focusedItem  |
+     *  |   candidate   |__|_______________|
+     *  |__________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate22() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 20)
+            FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ___________________________________
+     *  |   candidate   |  |  focusedItem  |
+     *  |_______________|__|_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate23() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ___________________________________
+     *  |   candidate   |  |               |
+     *  |_______________|__|  focusedItem  |
+     *                  |                  |
+     *                  |__________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate24() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                   ___________________
+     *   _______________|___               |
+     *  |   candidate   |  |  focusedItem  |
+     *  |_______________|__|               |
+     *                  |__________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate25() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 5, 20, 10)
+            FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                   ___________________
+     *                  |                  |
+     *   _______________|___  focusedItem  |
+     *  |   candidate   |  |               |
+     *  |_______________|__}_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate26() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 10, 20, 10)
+            FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *   ___________________
+     *  |                __|_________________
+     *  |   candidate   |  |   focusedItem  |
+     *  |               |__|________________|
+     *  |__________________|
+     */
+    @MediumTest
+    @Test
+    fun overlappingCandidate27() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 20)
+            FocusableBox(focusedItem, 10, 5, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ___________________
+     *   |                __|_________________
+     *   |   candidate   |  |   focusedItem  |
+     *   |_______________|__|________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate28() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 20)
+            FocusableBox(focusedItem, 10, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Up, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *    ___________________
+     *   |   candidate    __|________________
+     *   |_______________|__|  focusedItem  |
+     *                   |__________________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate29() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 10, 5, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Left, Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *                    ___________________
+     *                   |     candidate    |
+     *                   |   _______________|
+     *                   |__|_______________|
+     *                      |  focusedItem  |
+     *                      |_______________|
+     */
+    @LargeTest
+    @Test
+    fun overlappingCandidate30() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 20, 10)
+            FocusableBox(focusedItem, 10, 5, 10, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            when (focusDirection) {
+                Up -> {
+                    assertThat(focusedItem.value).isFalse()
+                    assertThat(candidate.value).isTrue()
+                }
+                Left, Right, Down -> {
+                    assertThat(focusedItem.value).isTrue()
+                    assertThat(candidate.value).isFalse()
+                }
+                else -> error(invalid)
+            }
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |  candidate  |               |
+     *       |_____________|               |
+     *       |               focusedItem   |
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem1() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |       |  candidate  |       |
+     *       |       |_____________|       |
+     *       |         focusedItem         |
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem2() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 5, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |             |   candidate   |
+     *       |             |_______________|
+     *       |  focusedItem                |
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem3() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 0, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |               ______________|
+     *       | focusedItem  |   candidate  |
+     *       |              |______________|
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem4() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 5, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |  focusedItem                |
+     *       |               ______________|
+     *       |              |   candidate  |
+     *       |______________|______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem5() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 10, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |          focusedItem        |
+     *       |        _______________      |
+     *       |       |   candidate  |      |
+     *       |_______|______________|______|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem6() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 5, 10, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |                focusedItem  |
+     *       |______________               |
+     *       |  candidate  |               |
+     *       |_____________|_______________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem7() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 10, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |______________               |
+     *       |  candidate  |  focusedItem  |
+     *       |_____________|               |
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem8() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 0, 5, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |  focusedItem                |
+     *       |       ______________        |
+     *       |      |  candidate  |        |
+     *       |      |_____________|        |
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun candidateWithinBoundsOfFocusedItem9() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 5, 5, 10, 10)
+            FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |  focusedItem  |             |
+     *       |_______________|             |
+     *       |                 candidate   |
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate1() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 0, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |      |  focusedItem  |      |
+     *       |      |_______________|      |
+     *       |          candidate          |
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate2() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 5, 0, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |             |  focusedItem  |
+     *       |             |_______________|
+     *       |  candidate                  |
+     *       |_____________________________|
+     */
+    @MediumTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate3() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 10, 0, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |              _______________|
+     *       |  candidate  |  focusedItem  |
+     *       |             |_______________|
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate4() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 10, 5, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |   candidate                 |
+     *       |             ________________|
+     *       |            |   focusedItem  |
+     *       |____________|________________|
+     */
+    @LargeTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate5() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |           candidate         |
+     *       |        _______________      |
+     *       |       |  focusedItem |      |
+     *       |_______|______________|______|
+     */
+    @LargeTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate6() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 5, 10, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |                 candidate   |
+     *       |_______________              |
+     *       |  focusedItem |              |
+     *       |______________|______________|
+     */
+    @LargeTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate7() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 10, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |________________             |
+     *       |  focusedItem  |  candidate  |
+     *       |_______________|             |
+     *       |_____________________________|
+     */
+    @LargeTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate8() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 0, 5, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *        ------------------------------
+     *       |   candidate                 |
+     *       |       _______________       |
+     *       |      |  focusedItem |       |
+     *       |      |______________|       |
+     *       |_____________________________|
+     */
+    @MediumTest
+    @Test
+    fun focusedItemWithinBoundsOfCandidate9() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(focusedItem, 5, 5, 10, 10, initialFocus)
+            FocusableBox(candidate, 0, 0, 20, 20)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    /**
+     *                        ______________________   *        _________________
+     *    _________________  |            ________ |   *       |  next sibling  |           ^
+     *   |  next sibling  |  |  current  | child | |   *       |________________|           |
+     *   |________________|  |  focus    |_______| |   *      ______________________    Direction
+     *                       |_____________________|   *     |            ________ |    of Search
+     *                                                 *     |  current  | child | |        |
+     *          <---- Direction of Search ---          *     |  focus    |_______| |        |
+     *                                                 *     |_____________________|
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     *    ______________________                       *      ______________________
+     *   |            ________ |   _________________   *     |            ________ |        |
+     *   |  current  | child | |  |  next sibling  |   *     |  current  | child | |        |
+     *   |  focus    |_______| |  |________________|   *     |  focus    |_______| |    Direction
+     *   |_____________________|                       *     |_____________________|    of Search
+     *                                                 *        _________________           |
+     *          ---- Direction of Search --->          *       |  next sibling  |           |
+     *                                                 *       |________________|           v
+     */
+    @OptIn(ExperimentalComposeUiApi::class)
+    @MediumTest
+    @Test
+    fun picksSiblingAndNotChild() {
+        // Arrange.
+        val (focusedItem, child, nextSibling) = List(3) { mutableStateOf(false) }
+        rule.setContentForTest {
+            when (focusDirection) {
+                Left -> {
+                    FocusableBox(nextSibling, 0, 10, 10, 10)
+                    FocusableBox(focusedItem, 20, 0, 30, 30, initialFocus) {
+                        FocusableBox(child, 10, 10, 10, 10)
+                    }
+                }
+                Right -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus) {
+                        FocusableBox(child, 10, 10, 10, 10)
+                    }
+                    FocusableBox(nextSibling, 40, 10, 10, 10)
+                }
+                Up -> {
+                    FocusableBox(nextSibling, 10, 0, 10, 10)
+                    FocusableBox(focusedItem, 0, 20, 30, 30, initialFocus) {
+                        FocusableBox(child, 10, 10, 10, 10)
+                    }
+                }
+                Down -> {
+                    FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus) {
+                        FocusableBox(child, 10, 10, 10, 10)
+                    }
+                    FocusableBox(nextSibling, 10, 40, 10, 10)
+                }
+                else -> error(invalid)
+            }
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isFalse()
+            assertThat(child.value).isFalse()
+            assertThat(nextSibling.value).isTrue()
+        }
+    }
+
+    @MediumTest
+    @Test
+    fun sameBoundsForFocusedItemAndCandidate() {
+        // Arrange.
+        rule.setContentForTest {
+            FocusableBox(candidate, 10, 10, 20, 10)
+            FocusableBox(focusedItem, 10, 10, 20, 10, initialFocus)
+        }
+
+        // Act.
+        focusManager.moveFocus(focusDirection)
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusedItem.value).isTrue()
+            assertThat(candidate.value).isFalse()
+        }
+    }
+
+    private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
+        setContent {
+            focusManager = LocalFocusManager.current
+            composable()
+        }
+        rule.runOnIdle { initialFocus.requestFocus() }
+    }
+}
+
+@Composable
+private fun FocusableBox(
+    isFocused: MutableState<Boolean>,
+    x: Int,
+    y: Int,
+    width: Int,
+    height: Int,
+    focusRequester: FocusRequester? = null,
+    content: @Composable () -> Unit = {}
+) {
+    Layout(
+        content = content,
+        modifier = Modifier
+            .offset { IntOffset(x, y) }
+            .focusRequester(focusRequester ?: FocusRequester())
+            .onFocusChanged { isFocused.value = it.isFocused }
+            .focusModifier(),
+        measurePolicy = remember(width, height) {
+            MeasurePolicy { measurables, _ ->
+                val constraint = Constraints(width, width, height, height)
+                layout(width, height) {
+                    measurables.forEach {
+                        it.measure(constraint).place(0, 0)
+                    }
+                }
+            }
+        }
+    )
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
index 71259f9..629dae5 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
@@ -631,4 +631,4 @@
         return findRootView(parent)
     }
     return view
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
index 2600605..1c82675 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
@@ -24,7 +24,6 @@
 import android.view.MotionEvent.ACTION_POINTER_UP
 import android.view.MotionEvent.ACTION_UP
 import android.view.MotionEvent.TOOL_TYPE_UNKNOWN
-import androidx.activity.ComponentActivity
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -35,6 +34,7 @@
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.ValueElement
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.unit.IntSize
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -50,7 +50,7 @@
 @RunWith(AndroidJUnit4::class)
 class PointerInteropFilterTest {
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
+    val rule = createAndroidComposeRule<TestActivity>()
 
     private lateinit var pointerInteropFilter: PointerInteropFilter
     private val dispatchedMotionEvents = mutableListOf<MotionEvent>()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
index 04da750..81b70cd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
@@ -16,14 +16,27 @@
 
 package androidx.compose.ui.layout
 
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.Constraints
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import kotlin.math.min
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@SmallTest
+@MediumTest
 @RunWith(AndroidJUnit4::class)
 class AlignmentLineTest {
+    @get:Rule
+    val rule = createAndroidComposeRule<ComponentActivity>()
+
     @Test
     fun queryingLinesOfUnmeasuredChild() {
         val root = root {
@@ -39,4 +52,78 @@
         createDelegate(root)
         assertMeasuredAndLaidOut(root)
     }
+
+    @Test
+    fun alignmentLinesPositionInCooperation_whenModifierDisobeys() {
+        val hLine = HorizontalAlignmentLine(::min)
+        val vLine = VerticalAlignmentLine(::min)
+        val hLinePosition = 50
+        val vLinePosition = 150
+        val constrainedSize = 100
+        val actualSize = 200
+        rule.setContent {
+            val contentWithAlignmentLines = @Composable {
+                Box(Modifier.requiredSize(with(rule.density) { actualSize.toDp() })) {
+                    Layout({}, Modifier) { _, _ ->
+                        layout(0, 0, mapOf(hLine to hLinePosition, vLine to vLinePosition)) {}
+                    }
+                }
+            }
+            Layout(contentWithAlignmentLines) { measurables, _ ->
+                val placeable = measurables.first().measure(
+                    Constraints(maxWidth = constrainedSize, maxHeight = constrainedSize)
+                )
+                val obtainedHLinePosition = placeable[hLine]
+                val obtainedVLinePosition = placeable[vLine]
+                assertEquals(
+                    hLinePosition - (actualSize - constrainedSize) / 2,
+                    obtainedHLinePosition
+                )
+                assertEquals(
+                    vLinePosition - (actualSize - constrainedSize) / 2,
+                    obtainedVLinePosition
+                )
+                layout(0, 0) {}
+            }
+        }
+        rule.waitForIdle()
+    }
+
+    @Test
+    fun alignmentLinesPositionInCooperation_whenLayoutDisobeys() {
+        val hLine = HorizontalAlignmentLine(::min)
+        val vLine = VerticalAlignmentLine(::min)
+        val hLinePosition = 50
+        val vLinePosition = 150
+        val constrainedSize = 100
+        val actualSize = 200
+        rule.setContent {
+            val contentWithAlignmentLines = @Composable {
+                Layout({}, Modifier) { _, _ ->
+                    layout(
+                        actualSize,
+                        actualSize,
+                        mapOf(hLine to hLinePosition, vLine to vLinePosition)
+                    ) {}
+                }
+            }
+            Layout(contentWithAlignmentLines) { measurables, _ ->
+                val placeable = measurables.first().measure(
+                    Constraints(maxWidth = constrainedSize, maxHeight = constrainedSize)
+                )
+                val obtainedHLinePosition = placeable[hLine]
+                val obtainedVLinePosition = placeable[vLine]
+                assertEquals(
+                    hLinePosition - (actualSize - constrainedSize) / 2,
+                    obtainedHLinePosition
+                )
+                assertEquals(
+                    vLinePosition - (actualSize - constrainedSize) / 2,
+                    obtainedVLinePosition
+                )
+                layout(0, 0) {}
+            }
+        }
+        rule.waitForIdle()
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
index 2554407..3082620 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
@@ -20,7 +20,6 @@
 import android.view.ViewGroup
 import android.widget.LinearLayout
 import android.widget.ScrollView
-import androidx.activity.ComponentActivity
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
@@ -45,6 +44,7 @@
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.unit.Constraints
@@ -69,7 +69,7 @@
 class OnGloballyPositionedTest {
 
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
+    val rule = createAndroidComposeRule<TestActivity>()
 
     @Test
     fun handlesChildrenNodeMoveCorrectly() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index 595bcce..22e4869 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -18,13 +18,12 @@
 
 import android.os.Build
 import android.widget.FrameLayout
-import androidx.activity.ComponentActivity
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.background
@@ -36,6 +35,7 @@
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
@@ -65,7 +65,7 @@
 class SubcomposeLayoutTest {
 
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
+    val rule = createAndroidComposeRule<TestActivity>()
     @get:Rule
     val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
index 50a283b..0c989fd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
@@ -24,6 +24,7 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.runtime.MonotonicFrameClock
 import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.test.setViewLayerTypeForApi28
 import androidx.core.view.doOnLayout
 import androidx.test.ext.junit.rules.activityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -40,6 +41,7 @@
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertSame
 import org.junit.Assert.assertTrue
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -50,6 +52,11 @@
     @get:Rule
     val rule = activityScenarioRule<AppCompatActivity>()
 
+    @Before
+    fun setup() {
+        setViewLayerTypeForApi28()
+    }
+
     @Test
     fun currentThreadIsMainOnMainThread() = runBlocking(Dispatchers.Main) {
         assertSame(AndroidUiDispatcher.Main, AndroidUiDispatcher.CurrentThread)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
index ef211d1..46dbe8b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
@@ -16,10 +16,15 @@
 
 package androidx.compose.ui.platform
 
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.SideEffect
 import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
@@ -29,6 +34,7 @@
 import androidx.test.filters.LargeTest
 import com.nhaarman.mockitokotlin2.inOrder
 import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
 import com.nhaarman.mockitokotlin2.times
 import com.nhaarman.mockitokotlin2.verify
 import org.junit.Rule
@@ -43,7 +49,37 @@
     @get:Rule
     val rule = createComposeRule()
 
-    @ExperimentalComposeUiApi
+    @Test
+    fun whenButtonClicked_performsHide_realisticAppTestCase() {
+        // code under test
+        @Composable
+        fun TestComposable() {
+            val softwareKeyboardController = LocalSoftwareKeyboardController.current
+            // Box instead of Button in this file for module dependency reasons
+            Box(Modifier.clickable { softwareKeyboardController?.hide() }) {
+                BasicText("Click Me")
+            }
+        }
+
+        // arrange
+        val mockSoftwareKeyboardController: SoftwareKeyboardController = mock()
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalSoftwareKeyboardController provides mockSoftwareKeyboardController
+            ) {
+                TestComposable()
+            }
+        }
+
+        // act
+        rule.onNodeWithText("Click Me").performClick()
+
+        // assert
+        rule.runOnIdle {
+            verify(mockSoftwareKeyboardController).hide()
+        }
+    }
+
     @Test
     fun localSoftwareKeybardController_delegatesTo_textInputService() {
         val platformTextInputService = mock<PlatformTextInputService>()
@@ -55,7 +91,7 @@
             ) {
                 val controller = LocalSoftwareKeyboardController.current
                 SideEffect {
-                    controller?.hideSoftwareKeyboard()
+                    controller?.hide()
                 }
             }
         }
@@ -84,8 +120,8 @@
         rule.onNodeWithText("string").performClick()
 
         rule.runOnIdle {
-            controller?.hideSoftwareKeyboard()
-            controller?.showSoftwareKeyboard()
+            controller?.hide()
+            controller?.show()
             inOrder(platformTextInputService) {
                 verify(platformTextInputService).showSoftwareKeyboard() // focus
                 verify(platformTextInputService).hideSoftwareKeyboard() // explicit call
@@ -100,7 +136,29 @@
         rule.setContent {
             keyboardController = LocalSoftwareKeyboardController.current
         }
-        keyboardController!!.showSoftwareKeyboard()
-        keyboardController!!.hideSoftwareKeyboard()
+        keyboardController!!.show()
+        keyboardController!!.hide()
+    }
+
+    @Test
+    fun showAndHide_noOp_whenProvidedMock() {
+        val mockSoftwareKeyboardController: SoftwareKeyboardController = mock()
+        val platformTextInputService = mock<PlatformTextInputService>()
+        val textInputService = TextInputService(platformTextInputService)
+        var controller: SoftwareKeyboardController? = null
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalSoftwareKeyboardController provides mockSoftwareKeyboardController,
+                LocalTextInputService provides textInputService
+            ) {
+                controller = LocalSoftwareKeyboardController.current
+            }
+        }
+        rule.runOnIdle {
+            controller?.show()
+            controller?.hide()
+        }
+        verify(platformTextInputService, never()).hideSoftwareKeyboard()
+        verify(platformTextInputService, never()).showSoftwareKeyboard()
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
index 646f377..fe77ad6 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
@@ -28,7 +28,6 @@
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -46,7 +45,7 @@
      * Test that a Recomposer that doesn't shut down with the activity doesn't inadvertently
      * keep a reference to the Activity
      */
-    @OptIn(ExperimentalCoroutinesApi::class, InternalComposeUiApi::class)
+    @OptIn(InternalComposeUiApi::class)
     @Test
     @LargeTest
     fun activityGarbageCollected() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt
index c6da754..f7351f0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt
@@ -15,16 +15,22 @@
  */
 package androidx.compose.ui.test
 
+import android.os.Build
 import android.view.KeyEvent
 import androidx.activity.ComponentActivity
+import androidx.compose.ui.platform.ViewLayer
 import java.util.concurrent.CountDownLatch
 
-class TestActivity : ComponentActivity() {
+open class TestActivity : ComponentActivity() {
 
     var receivedKeyEvent: KeyEvent? = null
 
     var hasFocusLatch = CountDownLatch(1)
 
+    init {
+        setViewLayerTypeForApi28()
+    }
+
     override fun onWindowFocusChanged(hasFocus: Boolean) {
         super.onWindowFocusChanged(hasFocus)
         if (hasFocus) {
@@ -37,3 +43,15 @@
         return super.onKeyDown(keyCode, event)
     }
 }
+
+/**
+ * We have a ViewLayer that doesn't use reflection that won't be activated on
+ * any Google devices, so we must trigger it directly. Here, we use it on all P
+ * devices. The normal ViewLayer is used on L devices. RenderNodeLayer is used
+ * on all other devices.
+ */
+internal fun setViewLayerTypeForApi28() {
+    if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
+        ViewLayer.shouldUseDispatchDraw = true
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 8646e5d..db0b1eb 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -27,7 +27,6 @@
 import android.widget.FrameLayout
 import android.widget.RelativeLayout
 import android.widget.TextView
-import androidx.activity.ComponentActivity
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -50,6 +49,7 @@
 import androidx.compose.ui.platform.findViewTreeCompositionContext
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.R
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -79,7 +79,7 @@
 @RunWith(AndroidJUnit4::class)
 class AndroidViewTest {
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
+    val rule = createAndroidComposeRule<TestActivity>()
 
     @Test
     fun androidViewWithConstructor() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
index fba2332..d849a82 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
@@ -19,8 +19,8 @@
 import android.view.KeyEvent
 import android.view.View
 import android.widget.EditText
-import androidx.activity.ComponentActivity
 import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -33,7 +33,7 @@
 @RunWith(AndroidJUnit4::class)
 class EditTextInteropTest {
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
+    val rule = createAndroidComposeRule<TestActivity>()
 
     @Test
     fun hardwareKeyInEmbeddedView() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
index b799060..d37dd10 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
@@ -19,13 +19,13 @@
 import android.os.Bundle
 import android.view.View
 import android.view.WindowManager
-import androidx.activity.ComponentActivity
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.node.Owner
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
@@ -67,7 +67,7 @@
     }
 }
 
-internal class ActivityWithFlagSecure : ComponentActivity() {
+internal class ActivityWithFlagSecure : TestActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
index 3f678ca..ffd35e4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -41,7 +42,7 @@
 class PositionInWindowTest {
 
     @get:Rule
-    val rule = createAndroidComposeRule<ComponentActivity>()
+    val rule = createAndroidComposeRule<TestActivity>()
 
     lateinit var activity: ComponentActivity
 
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 844c3aa..232a9a6 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -89,7 +89,9 @@
 import androidx.compose.ui.node.OwnerSnapshotObserver
 import androidx.compose.ui.node.RootForTest
 import androidx.compose.ui.semantics.SemanticsModifierCore
+import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.semantics.outerSemantics
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.input.PlatformTextInputService
 import androidx.compose.ui.text.input.TextInputService
@@ -100,7 +102,10 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.util.trace
 import androidx.compose.ui.viewinterop.AndroidViewHolder
+import androidx.core.view.AccessibilityDelegateCompat
 import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewTreeLifecycleOwner
@@ -224,7 +229,7 @@
     override var showLayoutBounds = false
 
     private var _androidViewsHandler: AndroidViewsHandler? = null
-    private val androidViewsHandler: AndroidViewsHandler
+    internal val androidViewsHandler: AndroidViewsHandler
         get() {
             if (_androidViewsHandler == null) {
                 _androidViewsHandler = AndroidViewsHandler(context)
@@ -232,9 +237,7 @@
             }
             return _androidViewsHandler!!
         }
-    private val viewLayersContainer by lazy(LazyThreadSafetyMode.NONE) {
-        ViewLayerContainer(context).also { addView(it) }
-    }
+    private var viewLayersContainer: DrawChildContainer? = null
 
     // The constraints being used by the last onMeasure. It is set to null in onLayout. It allows
     // us to detect the case when the View was measured twice with different constraints within
@@ -399,6 +402,30 @@
     fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
         androidViewsHandler.holderToLayoutNode[view] = layoutNode
         androidViewsHandler.addView(view)
+        androidViewsHandler.layoutNodeToHolder[layoutNode] = view
+        // Fetching AccessibilityNodeInfo from a View which is not set to
+        // IMPORTANT_FOR_ACCESSIBILITY_YES will return null.
+        ViewCompat.setImportantForAccessibility(
+            view,
+            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
+        )
+        val thisView = this
+        ViewCompat.setAccessibilityDelegate(
+            view,
+            object : AccessibilityDelegateCompat() {
+                override fun onInitializeAccessibilityNodeInfo(
+                    host: View?,
+                    info: AccessibilityNodeInfoCompat?
+                ) {
+                    super.onInitializeAccessibilityNodeInfo(host, info)
+                    var parentId = SemanticsNode(layoutNode.outerSemantics!!, true).parent!!.id
+                    if (parentId == semanticsOwner.rootSemanticsNode.id) {
+                        parentId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+                    }
+                    info!!.setParent(thisView, parentId)
+                }
+            }
+        )
     }
 
     /**
@@ -408,6 +435,13 @@
     fun removeAndroidView(view: AndroidViewHolder) {
         androidViewsHandler.removeView(view)
         androidViewsHandler.holderToLayoutNode.remove(view)
+        androidViewsHandler.layoutNodeToHolder.remove(
+            androidViewsHandler.holderToLayoutNode[view]
+        )
+        ViewCompat.setImportantForAccessibility(
+            view,
+            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+        )
     }
 
     /**
@@ -547,12 +581,20 @@
                 isRenderNodeCompatible = false
             }
         }
-        return ViewLayer(
-            this,
-            viewLayersContainer,
-            drawBlock,
-            invalidateParentLayer
-        )
+        if (viewLayersContainer == null) {
+            if (!ViewLayer.hasRetrievedMethod) {
+                // Test to see if updateDisplayList() can be called. If this fails then
+                // ViewLayer.shouldUseDispatchDraw will be true.
+                ViewLayer.updateDisplayList(View(context))
+            }
+            if (ViewLayer.shouldUseDispatchDraw) {
+                viewLayersContainer = DrawChildContainer(context)
+            } else {
+                viewLayersContainer = ViewLayerContainer(context)
+            }
+            addView(viewLayersContainer)
+        }
+        return ViewLayer(this, viewLayersContainer!!, drawBlock, invalidateParentLayer)
     }
 
     override fun onSemanticsChange() {
@@ -577,6 +619,7 @@
             invalidateLayers(root)
         }
         measureAndLayout()
+
         // we don't have to observe here because the root has a layer modifier
         // that will observe all children. The AndroidComposeView has only the
         // root, so it doesn't have to invalidate itself based on model changes.
@@ -589,6 +632,17 @@
             }
             dirtyLayers.clear()
         }
+
+        if (ViewLayer.shouldUseDispatchDraw) {
+            // We must update the display list of all children using dispatchDraw()
+            // instead of updateDisplayList(). But since we don't want to actually draw
+            // the contents, we will clip out everything from the canvas.
+            val saveCount = canvas.save()
+            canvas.clipRect(0f, 0f, 0f, 0f)
+
+            super.dispatchDraw(canvas)
+            canvas.restoreToCount(saveCount)
+        }
     }
 
     /**
@@ -791,6 +845,63 @@
         return accessibilityDelegate.dispatchHoverEvent(event)
     }
 
+    private fun findViewByAccessibilityIdRootedAtCurrentView(
+        accessibilityId: Int,
+        currentView: View
+    ): View? {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            val getAccessibilityViewIdMethod = View::class.java
+                .getDeclaredMethod("getAccessibilityViewId")
+            getAccessibilityViewIdMethod.isAccessible = true
+            if (getAccessibilityViewIdMethod.invoke(currentView) == accessibilityId) {
+                return currentView
+            }
+            if (currentView is ViewGroup) {
+                for (i in 0 until currentView.childCount) {
+                    val foundView = findViewByAccessibilityIdRootedAtCurrentView(
+                        accessibilityId,
+                        currentView.getChildAt(i)
+                    )
+                    if (foundView != null) {
+                        return foundView
+                    }
+                }
+            }
+        }
+        return null
+    }
+
+    /**
+     * This overrides an @hide method in ViewGroup. Because of the @hide, the override keyword
+     * cannot be used, but the override works anyway because the ViewGroup method is not final.
+     * In Android P and earlier, the call path is
+     * AccessibilityInteractionController#findViewByAccessibilityId ->
+     * View#findViewByAccessibilityId -> ViewGroup#findViewByAccessibilityIdTraversal. In Android
+     * Q and later, AccessibilityInteractionController#findViewByAccessibilityId uses
+     * AccessibilityNodeIdManager and findViewByAccessibilityIdTraversal is only used by autofill.
+     */
+    public fun findViewByAccessibilityIdTraversal(accessibilityId: Int): View? {
+        try {
+            // AccessibilityInteractionController#findViewByAccessibilityId doesn't call this
+            // method in Android Q and later. Ideally, we should only define this method in
+            // Android P and earlier, but since we don't have a way to do so, we can simply
+            // invoke the hidden parent method after Android P. If in new android, the hidden method
+            // ViewGroup#findViewByAccessibilityIdTraversal signature is changed or removed, we can
+            // simply return null here because there will be no call to this method.
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+                val findViewByAccessibilityIdTraversalMethod = View::class.java
+                    .getDeclaredMethod("findViewByAccessibilityIdTraversal", Int::class.java)
+                findViewByAccessibilityIdTraversalMethod.isAccessible = true
+                return findViewByAccessibilityIdTraversalMethod.invoke(this, accessibilityId) as?
+                    View
+            } else {
+                return findViewByAccessibilityIdRootedAtCurrentView(accessibilityId, this)
+            }
+        } catch (e: NoSuchMethodException) {
+            return null
+        }
+    }
+
     override val isLifecycleInResumedState: Boolean
         get() = viewTreeOwners?.lifecycleOwner
             ?.lifecycle?.currentState == Lifecycle.State.RESUMED
@@ -958,4 +1069,4 @@
         // not to add the default focus highlight to the whole compose view
         view.defaultFocusHighlightEnabled = defaultFocusHighlightEnabled
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index a556129..cc8cdbe 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -154,7 +154,7 @@
     }
 
     /** Virtual view id for the currently hovered logical item. */
-    private var hoveredVirtualViewId = InvalidId
+    internal var hoveredVirtualViewId = InvalidId
     private val accessibilityManager: AccessibilityManager =
         view.context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
     internal var accessibilityForceEnabledForTesting = false
@@ -296,7 +296,12 @@
 
         semanticsNode.children.fastForEach { child ->
             if (currentSemanticsNodes.contains(child.id)) {
-                info.addChild(view, child.id)
+                val holder = view.androidViewsHandler.layoutNodeToHolder[child.layoutNode]
+                if (holder != null) {
+                    info.addChild(holder)
+                } else {
+                    info.addChild(view, child.id)
+                }
             }
         }
 
@@ -1203,16 +1208,36 @@
 
         when (event.action) {
             MotionEvent.ACTION_HOVER_MOVE, MotionEvent.ACTION_HOVER_ENTER -> {
-                val virtualViewId: Int = getVirtualViewAt(event.getX(), event.getY())
+                val rootNode = view.semanticsOwner.rootSemanticsNode
+                val node = findSemanticsNodeAt(event.x, event.y, rootNode)
+                var virtualViewId = InvalidId
+                if (node != null) {
+                    val hoveredView =
+                        view.androidViewsHandler.layoutNodeToHolder[node.layoutNode]
+                    if (hoveredView == null) {
+                        virtualViewId = semanticsNodeIdToAccessibilityVirtualNodeId(node.id)
+                    }
+                }
+                // The android views could be view groups, so the event must be dispatched to the
+                // views. Android ViewGroup.java will take care of synthesizing hover enter/exit
+                // actions from hover moves.
+                // Note that this should be before calling "updateHoveredVirtualView" so that in
+                // the corner case of overlapped nodes, the final hover enter event is sent from
+                // the node/view that we want to focus.
+                val handled = view.androidViewsHandler.dispatchGenericMotionEvent(event)
                 updateHoveredVirtualView(virtualViewId)
-                return (virtualViewId != InvalidId)
+                return if (virtualViewId == InvalidId) handled else true
             }
             MotionEvent.ACTION_HOVER_EXIT -> {
-                if (hoveredVirtualViewId != InvalidId) {
-                    updateHoveredVirtualView(InvalidId)
-                    return true
+                return when {
+                    hoveredVirtualViewId != InvalidId -> {
+                        updateHoveredVirtualView(InvalidId)
+                        true
+                    }
+                    else -> {
+                        view.androidViewsHandler.dispatchGenericMotionEvent(event)
+                    }
                 }
-                return false
             }
             else -> {
                 return false
@@ -1220,36 +1245,27 @@
         }
     }
 
+    // TODO(b/151729467): compose accessibility findSemanticsNodeAt needs to be more efficient
+    /**
+     * Find the semantics node at the specified location. The location is relative to the root.
+     */
     @VisibleForTesting
-    internal fun getVirtualViewAt(x: Float, y: Float): Int {
-        val node = view.semanticsOwner.rootSemanticsNode
-        val id = findVirtualViewAt(
-            x + node.boundsInWindow.left,
-            y + node.boundsInWindow.top, node
-        )
-        if (id == node.id) {
-            return AccessibilityNodeProviderCompat.HOST_VIEW_ID
-        }
-        return id
-    }
-
-    // TODO(b/151729467): compose accessibility getVirtualViewAt needs to be more efficient
-    private fun findVirtualViewAt(x: Float, y: Float, node: SemanticsNode): Int {
+    internal fun findSemanticsNodeAt(x: Float, y: Float, node: SemanticsNode): SemanticsNode? {
         val children = node.children
         for (i in children.size - 1 downTo 0) {
-            val id = findVirtualViewAt(x, y, children[i])
-            if (id != InvalidId) {
-                return id
+            val target = findSemanticsNodeAt(x, y, children[i])
+            if (target != null) {
+                return target
             }
         }
 
-        if (node.boundsInWindow.left < x && node.boundsInWindow.right > x && node
-            .boundsInWindow.top < y && node.boundsInWindow.bottom > y
+        if (node.boundsInRoot.left < x && node.boundsInRoot.right > x &&
+            node.boundsInRoot.top < y && node.boundsInRoot.bottom > y
         ) {
-            return node.id
+            return node
         }
 
-        return InvalidId
+        return null
     }
 
     /**
@@ -1389,6 +1405,10 @@
         if (!layoutNode.isAttached) {
             return
         }
+        // Android Views will send proper events themselves.
+        if (view.androidViewsHandler.layoutNodeToHolder.contains(layoutNode)) {
+            return
+        }
         // When we finally send the event, make sure it is an accessibility-focusable node.
         var semanticsWrapper = layoutNode.outerSemantics
             ?: layoutNode.findClosestParentNode { it.outerSemantics != null }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
index e72b4a2..5be4139 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
@@ -19,8 +19,6 @@
 import android.os.Looper
 import android.view.Choreographer
 import androidx.compose.runtime.MonotonicFrameClock
-import androidx.compose.ui.platform.AndroidUiDispatcher.Companion.CurrentThread
-import androidx.compose.ui.platform.AndroidUiDispatcher.Companion.Main
 import androidx.core.os.HandlerCompat
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
@@ -40,7 +38,6 @@
 // not marked as async will adversely affect dispatch behavior but not to the point of
 // incorrectness; more operations would be deferred to the choreographer frame as racing handler
 // messages would wait behind a frame barrier.
-@OptIn(ExperimentalStdlibApi::class)
 class AndroidUiDispatcher private constructor(
     val choreographer: Choreographer,
     private val handler: android.os.Handler
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
index 30b40a3..ace7c55 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
@@ -38,6 +38,7 @@
     }
 
     val holderToLayoutNode = hashMapOf<AndroidViewHolder, LayoutNode>()
+    val layoutNodeToHolder = hashMapOf<LayoutNode, AndroidViewHolder>()
 
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         // Layout will be handled by component nodes.
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index 1844344..13238a3 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -40,7 +40,7 @@
  */
 internal class ViewLayer(
     val ownerView: AndroidComposeView,
-    val container: ViewLayerContainer,
+    val container: DrawChildContainer,
     val drawBlock: (Canvas) -> Unit,
     val invalidateParentLayer: () -> Unit
 ) : View(ownerView.context), OwnedLayer {
@@ -50,7 +50,8 @@
     private var clipBoundsCache: android.graphics.Rect? = null
     private val manualClipPath: Path? get() =
         if (!clipToOutline) null else outlineResolver.clipPath
-    private var isInvalidated = false
+    var isInvalidated = false
+        private set
     private var drawnWithZ = false
     private val canvasHolder = CanvasHolder()
 
@@ -221,13 +222,15 @@
     }
 
     override fun destroy() {
-        container.removeView(this)
+        container.postOnAnimation {
+            container.removeView(this)
+        }
         ownerView.dirtyLayers -= this
         ownerView.requestClearInvalidObservations()
     }
 
     override fun updateDisplayList() {
-        if (isInvalidated) {
+        if (isInvalidated && !shouldUseDispatchDraw) {
             updateDisplayList(this)
             isInvalidated = false
         }
@@ -252,40 +255,48 @@
         }
         private var updateDisplayListIfDirtyMethod: Method? = null
         private var recreateDisplayList: Field? = null
-        private var hasRetrievedMethod = false
+        var hasRetrievedMethod = false
+            private set
+
+        var shouldUseDispatchDraw = false
+            internal set // internal so that tests can use it.
 
         fun updateDisplayList(view: View) {
-            if (!hasRetrievedMethod) {
-                hasRetrievedMethod = true
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
-                    updateDisplayListIfDirtyMethod =
-                        View::class.java.getDeclaredMethod("updateDisplayListIfDirty")
-                    recreateDisplayList =
-                        View::class.java.getDeclaredField("mRecreateDisplayList")
-                } else {
-                    val getDeclaredMethod = Class::class.java.getDeclaredMethod(
-                        "getDeclaredMethod",
-                        String::class.java,
-                        arrayOf<Class<*>>()::class.java
-                    )
-                    updateDisplayListIfDirtyMethod = getDeclaredMethod.invoke(
-                        View::class.java,
-                        "updateDisplayListIfDirty", emptyArray<Class<*>>()
-                    ) as Method?
-                    val getDeclaredField = Class::class.java.getDeclaredMethod(
-                        "getDeclaredField",
-                        String::class.java
-                    )
-                    recreateDisplayList = getDeclaredField.invoke(
-                        View::class.java,
-                        "mRecreateDisplayList"
-                    ) as Field?
+            try {
+                if (!hasRetrievedMethod) {
+                    hasRetrievedMethod = true
+                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+                        updateDisplayListIfDirtyMethod =
+                            View::class.java.getDeclaredMethod("updateDisplayListIfDirty")
+                        recreateDisplayList =
+                            View::class.java.getDeclaredField("mRecreateDisplayList")
+                    } else {
+                        val getDeclaredMethod = Class::class.java.getDeclaredMethod(
+                            "getDeclaredMethod",
+                            String::class.java,
+                            arrayOf<Class<*>>()::class.java
+                        )
+                        updateDisplayListIfDirtyMethod = getDeclaredMethod.invoke(
+                            View::class.java,
+                            "updateDisplayListIfDirty", emptyArray<Class<*>>()
+                        ) as Method?
+                        val getDeclaredField = Class::class.java.getDeclaredMethod(
+                            "getDeclaredField",
+                            String::class.java
+                        )
+                        recreateDisplayList = getDeclaredField.invoke(
+                            View::class.java,
+                            "mRecreateDisplayList"
+                        ) as Field?
+                    }
+                    updateDisplayListIfDirtyMethod?.isAccessible = true
+                    recreateDisplayList?.isAccessible = true
                 }
-                updateDisplayListIfDirtyMethod?.isAccessible = true
-                recreateDisplayList?.isAccessible = true
+                recreateDisplayList?.setBoolean(view, true)
+                updateDisplayListIfDirtyMethod?.invoke(view)
+            } catch (_: Throwable) {
+                shouldUseDispatchDraw = true
             }
-            recreateDisplayList?.setBoolean(view, true)
-            updateDisplayListIfDirtyMethod?.invoke(view)
         }
     }
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
index 967bc00..4a3f22c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
@@ -25,21 +25,7 @@
 /**
  * The container we will use for [ViewLayer]s.
  */
-internal class ViewLayerContainer(context: Context) : ViewGroup(context) {
-
-    init {
-        clipChildren = false
-    }
-
-    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
-        // we don't layout our children
-    }
-
-    // we change visibility for this method so ViewLayer can use it for drawing
-    internal fun drawChild(canvas: Canvas, view: View, drawingTime: Long) {
-        super.drawChild(canvas.nativeCanvas, view, drawingTime)
-    }
-
+internal class ViewLayerContainer(context: Context) : DrawChildContainer(context) {
     override fun dispatchDraw(canvas: android.graphics.Canvas) {
         // we draw our children as part of AndroidComposeView.dispatchDraw
     }
@@ -49,5 +35,47 @@
      * the display lists.
      * We override hidden protected method from ViewGroup
      */
-    protected fun dispatchGetDisplayList() {}
+    protected fun dispatchGetDisplayList() {
+    }
+}
+
+/**
+ * The container we will use for [ViewLayer]s when [ViewLayer.shouldUseDispatchDraw] is true.
+ */
+internal open class DrawChildContainer(context: Context) : ViewGroup(context) {
+    init {
+        clipChildren = false
+    }
+
+    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+        // we don't layout our children
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        // we don't measure our children
+        setMeasuredDimension(0, 0)
+    }
+
+    override fun dispatchDraw(canvas: android.graphics.Canvas) {
+        // We must updateDisplayListIfDirty for all invalidated Views.
+
+        // We only want to call super.dispatchDraw() if there is an invalidated layer
+        var doDispatch = false
+        for (i in 0 until childCount) {
+            val child = getChildAt(i) as ViewLayer
+            if (child.isInvalidated) {
+                doDispatch = true
+                break
+            }
+        }
+
+        if (doDispatch) {
+            super.dispatchDraw(canvas)
+        }
+    }
+
+    // we change visibility for this method so ViewLayer can use it for drawing
+    internal fun drawChild(canvas: Canvas, view: View, drawingTime: Long) {
+        super.drawChild(canvas.nativeCanvas, view, drawingTime)
+    }
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
index e0d7619..ccd2990 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
@@ -29,7 +29,6 @@
 import androidx.lifecycle.ViewTreeLifecycleOwner
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.launch
@@ -204,7 +203,6 @@
         }
     }
 
-@OptIn(ExperimentalCoroutinesApi::class)
 private fun View.createLifecycleAwareViewTreeRecomposer(): Recomposer {
     val currentThreadContext = AndroidUiDispatcher.CurrentThread
     val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ColorResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ColorResources.android.kt
index 9a6108e..d56ac0c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ColorResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ColorResources.android.kt
@@ -19,6 +19,7 @@
 import android.os.Build
 import androidx.annotation.ColorRes
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 
@@ -29,6 +30,7 @@
  * @return the color associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun colorResource(@ColorRes id: Int): Color {
     val context = LocalContext.current
     return if (Build.VERSION.SDK_INT >= 23) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/FontResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/FontResources.android.kt
index f7cd48c..bc0a8aa 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/FontResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/FontResources.android.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import androidx.annotation.GuardedBy
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.font.Typeface
 import androidx.compose.ui.text.font.FontFamily
@@ -41,6 +42,7 @@
  * @return the decoded image data associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun fontResource(fontFamily: FontFamily): Typeface {
     return fontResourceFromContext(LocalContext.current, fontFamily)
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PrimitiveResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PrimitiveResources.android.kt
index 6c2fb81..f10c3d9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PrimitiveResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PrimitiveResources.android.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.DimenRes
 import androidx.annotation.IntegerRes
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.Dp
@@ -32,6 +33,7 @@
  * @return the integer associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun integerResource(@IntegerRes id: Int): Int {
     val context = LocalContext.current
     return context.resources.getInteger(id)
@@ -44,6 +46,7 @@
  * @return the integer array associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun integerArrayResource(@ArrayRes id: Int): IntArray {
     val context = LocalContext.current
     return context.resources.getIntArray(id)
@@ -56,6 +59,7 @@
  * @return the boolean associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun booleanResource(@BoolRes id: Int): Boolean {
     val context = LocalContext.current
     return context.resources.getBoolean(id)
@@ -68,6 +72,7 @@
  * @return the dimension value associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun dimensionResource(@DimenRes id: Int): Dp {
     val context = LocalContext.current
     val density = LocalDensity.current
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/StringResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/StringResources.android.kt
index 70e41c5..189ee64 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/StringResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/StringResources.android.kt
@@ -20,6 +20,7 @@
 import androidx.annotation.ArrayRes
 import androidx.annotation.StringRes
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 
@@ -30,6 +31,7 @@
  * @return the string data associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun stringResource(@StringRes id: Int): String {
     val resources = resources()
     return resources.getString(id)
@@ -43,6 +45,7 @@
  * @return the string data associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun stringResource(@StringRes id: Int, vararg formatArgs: Any): String {
     val resources = resources()
     return resources.getString(id, *formatArgs)
@@ -55,6 +58,7 @@
  * @return the string data associated with the resource
  */
 @Composable
+@ReadOnlyComposable
 fun stringArrayResource(@ArrayRes id: Int): Array<String> {
     val resources = resources()
     return resources.getStringArray(id)
@@ -65,6 +69,7 @@
  * gets updated.
  */
 @Composable
+@ReadOnlyComposable
 private fun resources(): Resources {
     LocalConfiguration.current
     return LocalContext.current.resources
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index a101327b..f88c289 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -28,7 +28,6 @@
 import android.view.inputmethod.InputMethodManager
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.text.TextRange
-import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.channels.Channel
 import kotlin.math.roundToInt
 
@@ -158,7 +157,6 @@
         showKeyboardChannel.offer(false)
     }
 
-    @OptIn(FlowPreview::class)
     suspend fun keyboardVisibilityEventLoop() {
         // TODO(b/180071033): Allow for more IMPLICIT flag to be passed.
         for (showKeyboard in showKeyboardChannel) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index 8f3aff5..7c2b07e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -32,6 +32,7 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.LayoutDirection
 
 /**
@@ -65,7 +66,11 @@
     update: (T) -> Unit = NoOpUpdate
 ) {
     val context = LocalContext.current
-    val materialized = currentComposer.materialize(modifier)
+    // Create a semantics node for accessibility. Semantics modifier is composed and need to be
+    // materialized. So it can't be added in AndroidViewHolder when assigning modifier to layout
+    // node, which is after the materialize call.
+    val modifierWithSemantics = modifier.semantics(true) {}
+    val materialized = currentComposer.materialize(modifierWithSemantics)
     val density = LocalDensity.current
     val layoutDirection = LocalLayoutDirection.current
     val parentReference = rememberCompositionContext()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 0112b21..7baab58 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -184,7 +184,7 @@
      */
     val layoutNode: LayoutNode = run {
         // TODO(soboleva): add layout direction here?
-        // TODO(popam): forward pointer input, accessibility, focus
+        // TODO(popam): forward pointer input and focus
         // Prepare layout node that proxies measure and layout passes to the View.
         val layoutNode = LayoutNode()
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
index 892199b..ba53f0f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
@@ -31,6 +31,8 @@
  * Creates a [GraphicsLayerModifier] that draws the shadow. The [elevation] defines the visual
  * depth of the physical object. The physical object has a shape specified by [shape].
  *
+ * If the passed [shape] is concave the shadow will not be drawn on Android versions less than 10.
+ *
  * Note that [elevation] is only affecting the shadow size and doesn't change the drawing order.
  * Use [zIndex] modifier if you want to draw the elements with larger [elevation] after all the
  * elements with a smaller one.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
index 9d55d7f..adf2c51 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
@@ -65,16 +65,18 @@
 
     // If no custom focus traversal order is specified, perform a search for the appropriate item
     // to move focus to.
-    return when (focusDirection) {
-        Next, Previous -> {
-            // TODO(b/170155659): Perform one dimensional focus search.
-            false
-        }
-        Left, Right, Up, Down -> {
-            // TODO(b/170155926): Perform two dimensional focus search.
-            false
-        }
+    val nextNode = when (focusDirection) {
+        Next, Previous -> null // TODO(b/170155659): Perform one dimensional focus search.
+        Left, Right, Up, Down -> twoDimensionalFocusSearch(focusDirection)
     }
+
+    // If we found a potential next item, call requestFocus() to move focus to it.
+    if (nextNode != null) {
+        nextNode.requestFocus(propagateFocus = false)
+        return true
+    }
+
+    return false
 }
 
 internal fun ModifiedFocusNode.findActiveFocusNode(): ModifiedFocusNode? {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
new file mode 100644
index 0000000..3cfb4f8
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.ui.focus.FocusDirection.Down
+import androidx.compose.ui.focus.FocusDirection.Left
+import androidx.compose.ui.focus.FocusDirection.Right
+import androidx.compose.ui.focus.FocusDirection.Up
+import androidx.compose.ui.focus.FocusState.Active
+import androidx.compose.ui.focus.FocusState.ActiveParent
+import androidx.compose.ui.focus.FocusState.Captured
+import androidx.compose.ui.focus.FocusState.Disabled
+import androidx.compose.ui.focus.FocusState.Inactive
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.node.ModifiedFocusNode
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.absoluteValue
+import kotlin.math.max
+
+private const val invalidFocusDirection = "This function should only be used for 2-D focus search"
+
+/**
+ *  Perform a search among the immediate children of this [node][ModifiedFocusNode] in the
+ *  specified [direction][FocusDirection] and return the node that is to be focused next. If one
+ *  of the children is currently focused, we start from that point and search in the specified
+ *  [direction][FocusDirection]. If none of the children are currently focused, we pick the
+ *  top-left or bottom right based on the specified [direction][FocusDirection].
+ */
+internal fun ModifiedFocusNode.twoDimensionalFocusSearch(
+    direction: FocusDirection
+): ModifiedFocusNode? {
+    return when (focusState) {
+        Inactive -> this
+        Disabled -> null
+        ActiveParent -> {
+            val focusedChild = focusedChild
+            checkNotNull(focusedChild) { "ActiveParent must have a focusedChild" }
+
+            // If this node contains the focused child, pick one of the other children.
+            // Otherwise, this is an intermediate parent. Continue searching among the children of
+            // the child that has focus.
+            if (focusedChild.focusState.isFocused) {
+                focusableChildren().findBestCandidate(focusedChild.focusRect(), direction)
+            } else {
+                focusedChild.twoDimensionalFocusSearch(direction)
+            }
+        }
+        Active, Captured -> {
+            // The 2-D focus search starts form the root. If we reached here, it means that there
+            // was no intermediate node that was ActiveParent. This is an initial focus scenario.
+            // We need to search among this node's children to find the best focus candidate.
+            val focusableChildren = focusableChildren()
+
+            // If there are aren't multiple children to choose from, return the first child.
+            if (focusableChildren.size <= 1) {
+                return focusableChildren.firstOrNull()
+            }
+
+            // To start the search, we pick one of the four corners of this node as the initially
+            // focused rectangle.
+            val initialFocusRect = when (direction) {
+                Right, Down -> focusRect().topLeft()
+                Left, Up -> focusRect().bottomRight()
+                else -> error(invalidFocusDirection)
+            }
+            focusableChildren.findBestCandidate(initialFocusRect, direction)
+        }
+    }
+}
+
+// Iterate through this list of focus nodes and find best candidate in the specified direction.
+// TODO(b/182319711): For Left/Right focus moves, Consider finding the first candidate in the beam
+//  and then only comparing candidates in the beam. If nothing is in the beam, then consider all
+//  valid candidates.
+private fun List<ModifiedFocusNode>.findBestCandidate(
+    focusRect: Rect,
+    direction: FocusDirection
+): ModifiedFocusNode? {
+    // Pick an impossible rectangle as the initial best candidate Rect.
+    var bestCandidate = when (direction) {
+        Left -> focusRect.translate(focusRect.width + 1, 0f)
+        Right -> focusRect.translate(-(focusRect.width + 1), 0f)
+        Up -> focusRect.translate(0f, focusRect.height + 1)
+        Down -> focusRect.translate(0f, -(focusRect.height + 1))
+        else -> error(invalidFocusDirection)
+    }
+
+    var searchResult: ModifiedFocusNode? = null
+    fastForEach { candidateNode ->
+        val candidateRect = candidateNode.focusRect()
+        if (isBetterCandidate(candidateRect, bestCandidate, focusRect, direction)) {
+            bestCandidate = candidateRect
+            searchResult = candidateNode
+        }
+    }
+    return searchResult
+}
+
+// Is this Rect a better candidate than currentCandidateRect for a focus search in a particular
+// direction from a source rect? This is the core routine that determines the order of focus
+// searching.
+private fun isBetterCandidate(
+    proposedCandidate: Rect,
+    currentCandidate: Rect,
+    focusedRect: Rect,
+    direction: FocusDirection
+): Boolean {
+
+    // Is this Rect a candidate for the next focus given the direction? This checks whether the
+    // rect is at least partially to the direction of (e.g left of) from source. Includes an edge
+    // case for an empty rect (which is used in some cases when searching from a point on the
+    // screen).
+    fun Rect.isCandidate() = when (direction) {
+        Left -> (focusedRect.right > right || focusedRect.left >= right) && focusedRect.left > left
+        Right -> (focusedRect.left < left || focusedRect.right <= left) && focusedRect.right < right
+        Up -> (focusedRect.bottom > bottom || focusedRect.top >= bottom) && focusedRect.top > top
+        Down -> (focusedRect.top < top || focusedRect.bottom <= top) && focusedRect.bottom < bottom
+        else -> error(invalidFocusDirection)
+    }
+
+    // The distance from the edge furthest in the given direction of source to the edge nearest
+    // in the given direction of dest. If the dest is not in the direction from source, return 0.
+    fun Rect.majorAxisDistance(): Float {
+        val majorAxisDistance = when (direction) {
+            Left -> focusedRect.left - right
+            Right -> left - focusedRect.right
+            Up -> focusedRect.top - bottom
+            Down -> top - focusedRect.bottom
+            else -> error(invalidFocusDirection)
+        }
+        return max(0.0f, majorAxisDistance)
+    }
+
+    // Find the distance on the minor axis w.r.t the direction to the nearest edge of the
+    // destination rectangle.
+    fun Rect.minorAxisDistance() = when (direction) {
+        // the distance between the center verticals
+        Left, Right -> (focusedRect.top + focusedRect.height / 2) - (top + height / 2)
+        // the distance between the center horizontals
+        Up, Down -> (focusedRect.left + focusedRect.width / 2) - (left + width / 2)
+        else -> error(invalidFocusDirection)
+    }
+
+    // Fudge-factor opportunity: how to calculate distance given major and minor axis distances.
+    // Warning: This fudge factor is finely tuned, run all focus tests if you dare tweak it.
+    fun weightedDistance(candidate: Rect): Long {
+        val majorAxisDistance = candidate.majorAxisDistance().absoluteValue.toLong()
+        val minorAxisDistance = candidate.minorAxisDistance().absoluteValue.toLong()
+        return 13 * majorAxisDistance * majorAxisDistance + minorAxisDistance * minorAxisDistance
+    }
+
+    return when {
+        // to be a better candidate, need to at least be a candidate in the first place.
+        !proposedCandidate.isCandidate() -> false
+
+        // If the currentCandidate is not a candidate, proposedCandidate is better.
+        !currentCandidate.isCandidate() -> true
+
+        // if proposedCandidate is better by beam, it wins.
+        beamBeats(focusedRect, proposedCandidate, currentCandidate, direction) -> true
+
+        // if currentCandidate is better, then the proposedCandidate can't be.
+        beamBeats(focusedRect, currentCandidate, proposedCandidate, direction) -> false
+
+        else -> weightedDistance(proposedCandidate) < weightedDistance(currentCandidate)
+    }
+}
+
+/**
+ * A rectangle may be a better candidate by virtue of being exclusively in the beam of the source
+ * rect.
+ * @return Whether rect1 is a better candidate than rect2 by virtue of it being in the source's
+ * beam.
+ */
+private fun beamBeats(source: Rect, rect1: Rect, rect2: Rect, direction: FocusDirection): Boolean {
+    // Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap?
+    fun Rect.inSourceBeam() = when (direction) {
+        Left, Right -> this.bottom > source.top && this.top < source.bottom
+        Up, Down -> this.right > source.left && this.left < source.right
+        else -> error(invalidFocusDirection)
+    }
+
+    // Whether the rect is in the direction of search.
+    fun Rect.isInDirectionOfSearch() = when (direction) {
+        Left -> source.left >= right
+        Right -> source.right <= left
+        Up -> source.top >= bottom
+        Down -> source.bottom <= top
+        else -> error(invalidFocusDirection)
+    }
+
+    // The distance from the edge furthest in the given direction of source to the edge nearest
+    // in the given direction of dest. If the dest is not in the direction from source, return 0.
+    fun Rect.majorAxisDistance(): Float {
+        val majorAxisDistance = when (direction) {
+            Left -> source.left - right
+            Right -> left - source.right
+            Up -> source.top - bottom
+            Down -> top - source.bottom
+            else -> error(invalidFocusDirection)
+        }
+        return max(0.0f, majorAxisDistance)
+    }
+
+    // The distance along the major axis w.r.t the direction from the edge of source to the far
+    // edge of dest. If the dest is not in the direction from source, return 1 (to break ties
+    // with Rect.majorAxisDistance).
+    fun Rect.majorAxisDistanceToFarEdge(): Float {
+        val majorAxisDistance = when (direction) {
+            Left -> source.left - left
+            Right -> right - source.right
+            Up -> source.top - top
+            Down -> bottom - source.bottom
+            else -> error(invalidFocusDirection)
+        }
+        return max(1.0f, majorAxisDistance)
+    }
+
+    return when {
+        // if rect1 isn't exclusively in the src beam, it doesn't win
+        rect2.inSourceBeam() || !rect1.inSourceBeam() -> false
+
+        // We know rect1 is in the beam, and rect2 is not. If Rect2 is not in the direction of
+        // search, rect1 wins (since rect2 could be reached by going in another direction).
+        !rect2.isInDirectionOfSearch() -> true
+
+        // for horizontal directions, being exclusively in beam always wins
+        direction == Left || direction == Right -> true
+
+        // for vertical directions, beams only beat up to a point:
+        // now, as long as rect2 isn't completely closer, rect1 wins
+        // e.g for direction down, completely closer means for rect2's top
+        // edge to be closer to the source's top edge than rect1's bottom edge.
+        else -> rect1.majorAxisDistance() < rect2.majorAxisDistanceToFarEdge()
+    }
+}
+
+private fun Rect.topLeft() = Rect(left, top, left, top)
+private fun Rect.bottomRight() = Rect(right, bottom, right, bottom)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
index 626f9d8..fe604ab 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
@@ -37,6 +37,9 @@
  * rotation ([rotationX], [rotationY], [rotationZ]), opacity ([alpha]), shadow
  * ([shadowElevation], [shape]), and clipping ([clip], [shape]).
  *
+ * Note that if you provide a non-zero [shadowElevation] and if the passed [shape] is concave the
+ * shadow will not be drawn on Android versions less than 10.
+ *
  * If the layer parameters are backed by a [androidx.compose.runtime.State] or an animated value
  * prefer an overload with a lambda block on [GraphicsLayerScope] as reading a state inside the block
  * will only cause the layer properties update without triggering recomposition and relayout.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
index 4285fad..b81db67 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
@@ -62,6 +62,9 @@
      * Sets the elevation for the shadow in pixels. With the [shadowElevation] > 0f and
      * [shape] set, a shadow is produced. Default value is `0` and the value must not be
      * negative.
+     *
+     * Note that if you provide a non-zero [shadowElevation] and if the passed [shape] is concave the
+     * shadow will not be drawn on Android versions less than 10.
      */
     /*@setparam:FloatRange(from = 0.0)*/
     var shadowElevation: Float
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/AnimatedImageVector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/AnimatedImageVector.kt
index 9d13b37..bb6f2a9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/AnimatedImageVector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/AnimatedImageVector.kt
@@ -31,14 +31,14 @@
  *
  * @param imageVector The [ImageVector] to be animated. This is represented with the
  * `android:drawable` parameter of an `<animated-vector>` element.
- * @param targets The list of [AnimatedVectorTarget]s that specify animations for each of the
- * elements in the drawable. This is represented with `<target>` elements in `<animated-vector>`.
- * This list is expected to be *immutable*.
  */
 @ExperimentalComposeUiApi
 @Immutable
 class AnimatedImageVector internal constructor(
     val imageVector: ImageVector,
+    // The list of [AnimatedVectorTarget]s that specify animations for each of the elements in the
+    // drawable. This is represented with `<target>` elements in `<animated-vector>`. This list is
+    // expected to be *immutable*.
     internal val targets: List<AnimatedVectorTarget>
 ) {
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/AlignmentLine.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/AlignmentLine.kt
index 4640ac6..593831d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/AlignmentLine.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/AlignmentLine.kt
@@ -49,8 +49,6 @@
  *
  * @see VerticalAlignmentLine
  * @see HorizontalAlignmentLine
- *
- * @param merger Defines the position of an alignment line inherited from more than one child.
  */
 @Immutable
 sealed class AlignmentLine(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
index ebbc0b2..f4d9abd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
@@ -97,7 +97,7 @@
     /**
      * The constraints used for the measurement made to obtain this [Placeable].
      */
-    protected var measurementConstraints: Constraints = Constraints()
+    protected var measurementConstraints: Constraints = DefaultConstraints
 
     /**
      * The offset to be added to an apparent position assigned to this [Placeable] to make it real.
@@ -330,3 +330,5 @@
  * Block on [GraphicsLayerScope] which applies the default layer parameters.
  */
 private val DefaultLayerBlock: GraphicsLayerScope.() -> Unit = {}
+
+private val DefaultConstraints = Constraints()
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
index 9e961e0..387da83 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
@@ -80,7 +80,7 @@
         }
     }
 
-    override fun get(alignmentLine: AlignmentLine): Int = wrapped[alignmentLine]
+    override fun calculateAlignmentLine(alignmentLine: AlignmentLine) = wrapped[alignmentLine]
 
     override fun placeAt(
         position: IntOffset,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index f6a580b..a8b4970 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -108,7 +108,7 @@
         layoutNode.onNodePlaced()
     }
 
-    override operator fun get(alignmentLine: AlignmentLine): Int {
+    override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
         return layoutNode.calculateAlignmentLines()[alignmentLine] ?: AlignmentLine.Unspecified
     }
 
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 ea36814..d153075 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
@@ -31,7 +31,6 @@
 import androidx.compose.ui.input.pointer.PointerInputFilter
 import androidx.compose.ui.input.pointer.PointerInputModifier
 import androidx.compose.ui.layout.AlignmentLine
-import androidx.compose.ui.layout.HorizontalAlignmentLine
 import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.LayoutCoordinates
@@ -48,7 +47,6 @@
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.Remeasurement
 import androidx.compose.ui.layout.RemeasurementModifier
-import androidx.compose.ui.layout.merge
 import androidx.compose.ui.node.LayoutNode.LayoutState.LayingOut
 import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring
 import androidx.compose.ui.node.LayoutNode.LayoutState.NeedsRelayout
@@ -62,7 +60,6 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
-import kotlin.math.roundToInt
 
 /**
  * Enable to log changes to the LayoutNode tree.  This logging is quite chatty.
@@ -102,17 +99,20 @@
 
     // the list of nodes where the virtual children are unfolded (their children are represented
     // as our direct children)
-    private val _unfoldedChildren = mutableVectorOf<LayoutNode>()
+    private var _unfoldedChildren: MutableVector<LayoutNode>? = null
 
     private fun recreateUnfoldedChildrenIfDirty() {
         if (unfoldedVirtualChildrenListDirty) {
             unfoldedVirtualChildrenListDirty = false
-            _unfoldedChildren.clear()
+            val unfoldedChildren = _unfoldedChildren ?: mutableVectorOf<LayoutNode>().also {
+                _unfoldedChildren = it
+            }
+            unfoldedChildren.clear()
             _foldedChildren.forEach {
                 if (it.isVirtual) {
-                    _unfoldedChildren.addAll(it._children)
+                    unfoldedChildren.addAll(it._children)
                 } else {
-                    _unfoldedChildren.add(it)
+                    unfoldedChildren.add(it)
                 }
             }
         }
@@ -135,7 +135,7 @@
             _foldedChildren
         } else {
             recreateUnfoldedChildrenIfDirty()
-            _unfoldedChildren
+            _unfoldedChildren!!
         }
 
     /**
@@ -491,12 +491,13 @@
     /**
      * The alignment lines of this layout, inherited + intrinsic
      */
-    internal val alignmentLines: MutableMap<AlignmentLine, Int> = hashMapOf()
+    internal var alignmentLines: LayoutNodeAlignmentLines? = null
+        private set
 
     /**
      * The alignment lines provided by this layout at the last measurement
      */
-    internal val providedAlignmentLines: MutableMap<AlignmentLine, Int> = hashMapOf()
+    internal var providedAlignmentLines: Map<AlignmentLine, Int> = emptyMap()
 
     internal val mDrawScope: LayoutNodeDrawScope = sharedDrawScope
 
@@ -558,8 +559,6 @@
 
     internal var alignmentUsageByParent = UsageByParent.NotUsed
 
-    private val previousAlignmentLines = mutableMapOf<AlignmentLine, Int>()
-
     @Deprecated("Temporary API to support ConstraintLayout prototyping.")
     internal var canMultiMeasure: Boolean = false
 
@@ -638,13 +637,17 @@
                 owner!!.onSemanticsChange()
             }
             val addedCallback = hasNewPositioningCallback()
-            onPositionedCallbacks.clear()
+            onPositionedCallbacks?.clear()
 
             // Create a new chain of LayoutNodeWrappers, reusing existing ones from wrappers
             // when possible.
             val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
                 var wrapper = toWrap
                 if (mod is OnGloballyPositionedModifier) {
+                    val onPositionedCallbacks = onPositionedCallbacks
+                        ?: mutableVectorOf<OnGloballyPositionedModifier>().also {
+                            onPositionedCallbacks = it
+                        }
                     onPositionedCallbacks += mod
                 }
                 if (mod is RemeasurementModifier) {
@@ -763,7 +766,7 @@
     /**
      * List of all OnPositioned callbacks in the modifier chain.
      */
-    private val onPositionedCallbacks = mutableVectorOf<OnGloballyPositionedModifier>()
+    private var onPositionedCallbacks: MutableVector<OnGloballyPositionedModifier>? = null
 
     /**
      * Flag used by [OnPositionedDispatcher] to identify LayoutNodes that have already
@@ -812,30 +815,11 @@
     }
 
     /**
-     * Returns the alignment line value for a given alignment line without affecting whether
-     * the flag for whether the alignment line was read.
-     */
-    internal fun getAlignmentLine(alignmentLine: AlignmentLine): Int? {
-        val linePos = alignmentLines[alignmentLine] ?: return null
-        var pos = Offset(linePos.toFloat(), linePos.toFloat())
-        var wrapper = innerLayoutNodeWrapper
-        while (wrapper != outerLayoutNodeWrapper) {
-            pos = wrapper.toParentPosition(pos)
-            wrapper = wrapper.wrappedBy!!
-        }
-        pos = wrapper.toParentPosition(pos)
-        return if (alignmentLine is HorizontalAlignmentLine) {
-            pos.y.roundToInt()
-        } else {
-            pos.x.roundToInt()
-        }
-    }
-
-    /**
      * Return true if there is a new [OnGloballyPositionedModifier] assigned to this Layout.
      */
     private fun hasNewPositioningCallback(): Boolean {
-        return modifier.foldOut(false) { mod, hasNewCallback ->
+        val onPositionedCallbacks = onPositionedCallbacks
+        return onPositionedCallbacks != null && modifier.foldOut(false) { mod, hasNewCallback ->
             hasNewCallback ||
                 (mod is OnGloballyPositionedModifier && mod !in onPositionedCallbacks)
         }
@@ -930,28 +914,10 @@
             alignmentLinesCalculatedDuringLastLayout = false
             if (alignmentLinesRequired) {
                 alignmentLinesCalculatedDuringLastLayout = true
-                previousAlignmentLines.clear()
-                previousAlignmentLines.putAll(alignmentLines)
-                alignmentLines.clear()
-                _children.forEach { child ->
-                    if (!child.isPlaced) return@forEach
-                    child.alignmentLines.keys.forEach { childLine ->
-                        val linePositionInContainer = child.getAlignmentLine(childLine)!!
-                        // If the line was already provided by a previous child, merge the values.
-                        alignmentLines[childLine] = if (childLine in alignmentLines) {
-                            childLine.merge(
-                                alignmentLines.getValue(childLine),
-                                linePositionInContainer
-                            )
-                        } else {
-                            linePositionInContainer
-                        }
-                    }
+                val alignments = alignmentLines ?: LayoutNodeAlignmentLines(this).also {
+                    alignmentLines = it
                 }
-                alignmentLines += providedAlignmentLines
-                if (previousAlignmentLines != alignmentLines) {
-                    onAlignmentsChanged()
-                }
+                alignments.recalculate()
             }
             layoutState = Ready
         }
@@ -1055,13 +1021,12 @@
             layoutState = endState
         }
         isCalculatingAlignmentLines = false
-        return alignmentLines
+        return alignmentLines?.getLastCalculation() ?: emptyMap()
     }
 
     internal fun handleMeasureResult(measureResult: MeasureResult) {
         innerLayoutNodeWrapper.measureResult = measureResult
-        this.providedAlignmentLines.clear()
-        this.providedAlignmentLines += measureResult.alignmentLines
+        providedAlignmentLines = measureResult.alignmentLines
     }
 
     /**
@@ -1093,7 +1058,7 @@
         if (!isPlaced) {
             return // it hasn't been placed, so don't make a call
         }
-        onPositionedCallbacks.forEach { it.onGloballyPositioned(coordinates) }
+        onPositionedCallbacks?.forEach { it.onGloballyPositioned(coordinates) }
     }
 
     /**
@@ -1185,8 +1150,17 @@
         }
 
         modifier.foldIn(Unit) { _, mod ->
-            val wrapper = wrapperCache.firstOrNull { it.modifier === mod }
-            wrapper?.toBeReusedForSameModifier = true
+            var wrapper = wrapperCache.lastOrNull {
+                it.modifier === mod && !it.toBeReusedForSameModifier
+            }
+            // we want to walk up the chain up all LayoutNodeWrappers for the same modifier
+            while (wrapper != null) {
+                wrapper.toBeReusedForSameModifier = true
+                wrapper = if (wrapper.isChained)
+                    wrapper.wrappedBy as? DelegatingLayoutNodeWrapper<*>
+                else
+                    null
+            }
         }
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
new file mode 100644
index 0000000..fba763a
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.HorizontalAlignmentLine
+import androidx.compose.ui.layout.merge
+import kotlin.math.roundToInt
+
+internal class LayoutNodeAlignmentLines(
+    private val layoutNode: LayoutNode
+) {
+    /**
+     * The alignment lines of this layout, inherited + intrinsic
+     */
+    private val alignmentLines: MutableMap<AlignmentLine, Int> = hashMapOf()
+
+    private val previousAlignmentLines = mutableMapOf<AlignmentLine, Int>()
+
+    fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLines
+
+    fun recalculate() {
+        previousAlignmentLines.clear()
+        previousAlignmentLines.putAll(alignmentLines)
+        alignmentLines.clear()
+        layoutNode._children.forEach { child ->
+            val childAlignments = child.alignmentLines
+            if (!child.isPlaced || childAlignments == null) return@forEach
+            childAlignments.alignmentLines.keys.forEach { childLine ->
+                val linePositionInContainer = childAlignments.getAlignmentLine(childLine)!!
+                // If the line was already provided by a previous child, merge the values.
+                alignmentLines[childLine] = if (childLine in alignmentLines) {
+                    childLine.merge(
+                        alignmentLines.getValue(childLine),
+                        linePositionInContainer
+                    )
+                } else {
+                    linePositionInContainer
+                }
+            }
+        }
+        alignmentLines += layoutNode.providedAlignmentLines
+        if (previousAlignmentLines != alignmentLines) {
+            layoutNode.onAlignmentsChanged()
+        }
+    }
+
+    /**
+     * Returns the alignment line value for a given alignment line without affecting whether
+     * the flag for whether the alignment line was read.
+     */
+    private fun getAlignmentLine(alignmentLine: AlignmentLine): Int? {
+        val linePos = alignmentLines[alignmentLine] ?: return null
+        var pos = Offset(linePos.toFloat(), linePos.toFloat())
+        var wrapper = layoutNode.innerLayoutNodeWrapper
+        while (wrapper != layoutNode.outerLayoutNodeWrapper) {
+            pos = wrapper.toParentPosition(pos)
+            wrapper = wrapper.wrappedBy!!
+        }
+        pos = wrapper.toParentPosition(pos)
+        return if (alignmentLine is HorizontalAlignmentLine) {
+            pos.y.roundToInt()
+        } else {
+            pos.x.roundToInt()
+        }
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index fa5252f..9958594 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -31,11 +31,13 @@
 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
 import androidx.compose.ui.input.nestedscroll.NestedScrollDelegatingWrapper
 import androidx.compose.ui.input.pointer.PointerInputFilter
+import androidx.compose.ui.layout.AlignmentLine
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.VerticalAlignmentLine
 import androidx.compose.ui.layout.findRoot
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.unit.Constraints
@@ -164,6 +166,18 @@
         return result
     }
 
+    abstract fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int
+
+    final override fun get(alignmentLine: AlignmentLine): Int {
+        val measuredPosition = calculateAlignmentLine(alignmentLine)
+        if (measuredPosition == AlignmentLine.Unspecified) return AlignmentLine.Unspecified
+        return measuredPosition + if (alignmentLine is VerticalAlignmentLine) {
+            apparentToRealOffset.x
+        } else {
+            apparentToRealOffset.y
+        }
+    }
+
     /**
      * Places the modified child.
      */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
index 18c4ca3..9004a6a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.focus.findFocusableChildren
 import androidx.compose.ui.focus.searchChildrenForFocusNode
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.util.fastForEach
 
 internal class ModifiedFocusNode(
@@ -45,22 +46,16 @@
             sendOnFocusEvent(value)
         }
 
-    // TODO(b/175900268): Add API to allow a parent to extends the bounds of the focus Modifier.
-    //  For now we just use the bounds of this node.
-    val focusRect: Rect
-        get() = Rect(
-            left = position.x.toFloat(),
-            top = position.y.toFloat(),
-            right = size.width.toFloat(),
-            bottom = size.height.toFloat()
-        )
-
     var focusedChild: ModifiedFocusNode?
         get() = modifier.focusedChild
         set(value) {
             modifier.focusedChild = value
         }
 
+    // TODO(b/175900268): Add API to allow a parent to extends the bounds of the focus Modifier.
+    //  For now we just use the bounds of this node.
+    fun focusRect(): Rect = boundsInRoot()
+
     // TODO(b/152051577): Measure the performance of focusableChildren.
     //  Consider caching the children.
     fun focusableChildren(): List<ModifiedFocusNode> {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
index 614d548..bcf5547 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
@@ -59,7 +59,7 @@
             measureScope.maxIntrinsicHeight(wrapped, width)
         }
 
-    override operator fun get(alignmentLine: AlignmentLine): Int {
+    override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
         if (measureResult.alignmentLines.containsKey(alignmentLine)) {
             return measureResult.alignmentLines[alignmentLine] ?: AlignmentLine.Unspecified
         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
index 4f2e9c5..eadbc1c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
@@ -38,7 +38,7 @@
     }
     private var lastPosition: IntOffset = IntOffset.Zero
     private var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
-    private val lastProvidedAlignmentLines = mutableMapOf<AlignmentLine, Int>()
+    private var lastProvidedAlignmentLines: MutableMap<AlignmentLine, Int>? = null
     private var lastZIndex: Float = 0f
 
     /**
@@ -90,16 +90,12 @@
             measuredOnce = true
             layoutNode.layoutState = LayoutState.Measuring
             measurementConstraints = constraints
-            lastProvidedAlignmentLines.clear()
-            lastProvidedAlignmentLines.putAll(layoutNode.providedAlignmentLines)
             val outerWrapperPreviousMeasuredSize = outerWrapper.size
             owner.snapshotObserver.observeMeasureSnapshotReads(layoutNode) {
                 outerWrapper.measure(constraints)
             }
             layoutNode.layoutState = LayoutState.NeedsRelayout
-            if (layoutNode.providedAlignmentLines != lastProvidedAlignmentLines) {
-                layoutNode.onAlignmentsChanged()
-            }
+            notifyAlignmentChanges()
             val sizeChanged = outerWrapper.size != outerWrapperPreviousMeasuredSize ||
                 outerWrapper.width != width ||
                 outerWrapper.height != height
@@ -110,6 +106,26 @@
         return false
     }
 
+    private fun notifyAlignmentChanges() {
+        // optimized to only create a lastProvidedAlignmentLines when we do have non empty map
+        if (layoutNode.providedAlignmentLines.isNotEmpty()) {
+            val previous = lastProvidedAlignmentLines ?: mutableMapOf<AlignmentLine, Int>().also {
+                lastProvidedAlignmentLines = it
+            }
+            if (layoutNode.providedAlignmentLines != previous) {
+                previous.clear()
+                previous.putAll(layoutNode.providedAlignmentLines)
+                layoutNode.onAlignmentsChanged()
+            }
+        } else {
+            val previous = lastProvidedAlignmentLines
+            if (previous != null && previous.isNotEmpty()) {
+                previous.clear()
+                layoutNode.onAlignmentsChanged()
+            }
+        }
+    }
+
     // We are setting our measuredSize to match the coerced outerWrapper size, to prevent
     // double offseting for layout cooperation. However, this means that here we need
     // to override these getters to make the measured values correct in Measured.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController.kt
index 312783f..dbcc400 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.platform
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ProvidedValue
+import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.text.input.TextInputService
@@ -24,31 +26,51 @@
 @ExperimentalComposeUiApi
 public object LocalSoftwareKeyboardController {
 
+    private val LocalSoftwareKeyboardController =
+        compositionLocalOf<SoftwareKeyboardController?> { null }
+
     /**
-     * Return a [SoftwareKeyboardController] that delegates to the current [LocalTextInputService].
+     * Return a [SoftwareKeyboardController] that can control the current software keyboard.
      *
-     * Returns null if there is no [LocalTextInputService] and the software keyboard cannot be
-     * controlled.
+     * If it is not provided, the default implementation will delegate to [LocalTextInputService].
+     *
+     * Returns null if the software keyboard cannot be controlled.
      */
     @ExperimentalComposeUiApi
     public val current: SoftwareKeyboardController?
         @Composable get() {
-            val textInputService = LocalTextInputService.current ?: return null
-            return remember(textInputService) {
-                DelegatingSotwareKeyboardController(textInputService)
-            }
+            return LocalSoftwareKeyboardController.current ?: delegatingController()
         }
+
+    @Composable
+    private fun delegatingController(): SoftwareKeyboardController? {
+        val textInputService = LocalTextInputService.current ?: return null
+        return remember(textInputService) {
+            DelegatingSoftwareKeyboardController(textInputService)
+        }
+    }
+
+    /**
+     * Set the key [LocalSoftwareKeyboardController] in [CompositionLocalProvider].
+     */
+    public infix fun provides(
+        softwareKeyboardController: SoftwareKeyboardController
+    ): ProvidedValue<SoftwareKeyboardController?> {
+        return LocalSoftwareKeyboardController.provides(softwareKeyboardController)
+    }
 }
 
 @ExperimentalComposeUiApi
-private class DelegatingSotwareKeyboardController(
-    val textInputService: TextInputService?
+private class DelegatingSoftwareKeyboardController(
+    val textInputService: TextInputService
 ) : SoftwareKeyboardController {
-    override fun showSoftwareKeyboard() {
-        textInputService?.showSoftwareKeyboard()
+    override fun show() {
+        @Suppress("DEPRECATION")
+        textInputService.showSoftwareKeyboard()
     }
 
-    override fun hideSoftwareKeyboard() {
-        textInputService?.hideSoftwareKeyboard()
+    override fun hide() {
+        @Suppress("DEPRECATION")
+        textInputService.hideSoftwareKeyboard()
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/SoftwareKeyboardController.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/SoftwareKeyboardController.kt
index 5150781..339684b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/SoftwareKeyboardController.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/SoftwareKeyboardController.kt
@@ -16,12 +16,14 @@
 
 package androidx.compose.ui.platform
 
+import androidx.compose.runtime.Stable
 import androidx.compose.ui.ExperimentalComposeUiApi
 
 /**
  * Provide software keyboard control.
  */
 @ExperimentalComposeUiApi
+@Stable
 interface SoftwareKeyboardController {
     /**
      * Request that the system show a software keyboard.
@@ -37,13 +39,22 @@
      *
      * @sample androidx.compose.ui.samples.SoftwareKeyboardControllerSample
      *
-     * You do not need to call this function unless you also call [hideSoftwareKeyboard], as the
+     * You do not need to call this function unless you also call [hide], as the
      * keyboard is automatically shown and hidden by focus events in the BasicTextField.
      *
      * Calling this function is considered a side-effect and should not be called directly from
      * recomposition.
      */
-    fun showSoftwareKeyboard()
+    fun show()
+
+    /**
+     * @see show
+     */
+    @Deprecated(
+        "Use show instead.",
+        ReplaceWith("show()")
+    )
+    fun showSoftwareKeyboard() = show()
 
     /**
      * Hide the software keyboard.
@@ -56,5 +67,14 @@
      * Calling this function is considered a side-effect and should not be called directly from
      * recomposition.
      */
-    fun hideSoftwareKeyboard()
+    fun hide()
+
+    /**
+     * @see hide
+     */
+    @Deprecated(
+        "Use hide instead.",
+        ReplaceWith("hide()")
+    )
+    fun hideSoftwareKeyboard() = hide()
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.desktop.kt
index dcfd076..874e013 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.desktop.kt
@@ -116,12 +116,14 @@
             defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
             addWindowListener(object : WindowAdapter() {
                 override fun windowClosing(event: WindowEvent) {
-                    if (defaultCloseOperation != WindowConstants.DO_NOTHING_ON_CLOSE) {
-                        onDispose?.invoke()
-                        onDismiss?.invoke()
-                        events.invokeOnClose()
-                        AppManager.removeWindow(parent)
+                    if (!isClosed) {
                         isClosed = true
+                        if (defaultCloseOperation != WindowConstants.DO_NOTHING_ON_CLOSE) {
+                            onDispose?.invoke()
+                            onDismiss?.invoke()
+                            events.invokeOnClose()
+                            AppManager.removeWindow(parent)
+                        }
                     }
                 }
                 override fun windowIconified(event: WindowEvent) {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
index 25b5672..92cfd5c 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
@@ -161,6 +161,17 @@
                     event
                 )
             }
+
+            override fun mouseEntered(event: MouseEvent) = events.post {
+                owners.onMouseEntered(
+                    (event.x * density.density).toInt(),
+                    (event.y * density.density).toInt()
+                )
+            }
+
+            override fun mouseExited(event: MouseEvent) = events.post {
+                owners.onMouseExited()
+            }
         })
         wrapped.addMouseMotionListener(object : MouseMotionAdapter() {
             override fun mouseDragged(event: MouseEvent) = events.post {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
index 08be312..90c25cf 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
@@ -18,6 +18,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionContext
 import org.jetbrains.skiko.ClipComponent
+import org.jetbrains.skiko.GraphicsApi
 import java.awt.Component
 import javax.swing.JFrame
 import javax.swing.JLayeredPane
@@ -28,6 +29,7 @@
  * @param parent The parent AppFrame that wraps the ComposeWindow.
  */
 class ComposeWindow(val parent: AppFrame) : JFrame() {
+    private var isDisposed = false
     internal val layer = ComposeLayer()
     private val pane = object : JLayeredPane() {
         override fun setBounds(x: Int, y: Int, width: Int, height: Int) {
@@ -76,7 +78,10 @@
     }
 
     override fun dispose() {
-        layer.dispose()
+        if (!isDisposed) {
+            layer.dispose()
+            isDisposed = true
+        }
         super.dispose()
     }
 
@@ -86,4 +91,18 @@
             layer.component.requestFocus()
         }
     }
+
+    /**
+     * Retrieve underlying platform-specific operating system handle for the window where ComposeWindow is rendered.
+     * Currently returns HWND on Windows, Drawable on X11 and 0 on macOS.
+     */
+    val windowHandle: Long
+        get() = layer.component.windowHandle
+
+    /**
+     * Returns low level rendering API used for rendering in this ComposeWindow. API is automatically selected based on
+     * operating system, graphical hardware and `SKIKO_RENDER_API` environment variable.
+     */
+    val renderApi: GraphicsApi
+        get() = layer.component.renderApi
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
index b496be2..02572ed 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
@@ -358,4 +358,34 @@
         oldMoveFilters = newMoveFilters.filterIsInstance<PointerMoveEventFilter>()
         newMoveFilters = mutableListOf()
     }
+
+    internal fun onPointerEnter(position: Offset) {
+        var onEnterConsumed = false
+        // TODO: do we actually need that?
+        measureAndLayout()
+        root.hitTest(position, newMoveFilters)
+        for (
+            filter in newMoveFilters
+                .asReversed()
+                .asSequence()
+                .filterIsInstance<PointerMoveEventFilter>()
+        ) {
+            if (!onEnterConsumed) {
+                onEnterConsumed = filter.onEnterHandler()
+            }
+        }
+        oldMoveFilters = newMoveFilters.filterIsInstance<PointerMoveEventFilter>()
+        newMoveFilters = mutableListOf()
+    }
+
+    internal fun onPointerExit() {
+        var onExitConsumed = false
+        for (filter in oldMoveFilters.asReversed()) {
+            if (!onExitConsumed) {
+                onExitConsumed = filter.onExitHandler()
+            }
+        }
+        oldMoveFilters = listOf()
+        newMoveFilters = mutableListOf()
+    }
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
index 2ab89f4..31b9743 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
@@ -27,7 +27,6 @@
 import androidx.compose.ui.input.pointer.PointerType
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
 import org.jetbrains.skija.Canvas
 import java.awt.event.InputMethodEvent
@@ -39,7 +38,6 @@
     error("CompositionLocal DesktopOwnersAmbient not provided")
 }
 
-@OptIn(ExperimentalCoroutinesApi::class)
 internal class DesktopOwners(
     coroutineScope: CoroutineScope,
     component: DesktopComponent = DummyDesktopComponent,
@@ -81,8 +79,6 @@
     internal val platformInputService: DesktopPlatformInput = DesktopPlatformInput(component)
 
     init {
-        // TODO(demin): Experimental API (CoroutineStart.UNDISPATCHED).
-        //  Decide what to do before release (copy paste or use different approach).
         coroutineScope.launch(coroutineContext, start = CoroutineStart.UNDISPATCHED) {
             recomposer.runRecomposeAndApplyChanges()
         }
@@ -153,6 +149,15 @@
         lastOwner?.onPointerMove(position)
     }
 
+    fun onMouseEntered(x: Int, y: Int) {
+        val position = Offset(x.toFloat(), y.toFloat())
+        lastOwner?.onPointerEnter(position)
+    }
+
+    fun onMouseExited() {
+        lastOwner?.onPointerExit()
+    }
+
     private fun consumeKeyEvent(event: KeyEvent) {
         list.lastOrNull()?.sendKeyEvent(ComposeKeyEvent(event))
     }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
index 9cc2205..a4c5c67 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
@@ -85,11 +85,9 @@
     }
 
     override fun showSoftwareKeyboard() {
-        println("DesktopPlatformInput.showSoftwareKeyboard")
     }
 
     override fun hideSoftwareKeyboard() {
-        println("DesktopPlatformInput.hideSoftwareKeyboard")
     }
 
     override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
@@ -109,7 +107,6 @@
     ) {
         // Which OSes and which input method could produce such events? We need to have some
         // specific cases in mind before implementing this
-        println("DesktopInputComponent.inputMethodCaretPositionChanged")
     }
 
     internal fun replaceInputMethodText(event: InputMethodEvent) {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
index 28603b8..fb2f66b 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
@@ -121,4 +121,25 @@
     fun onMouseScroll(x: Int, y: Int, event: MouseScrollEvent) {
         owners.onMouseScroll(x, y, event)
     }
+
+    /**
+     * Process mouse move event
+     */
+    fun onMouseMoved(x: Int, y: Int) {
+        owners.onMouseMoved(x, y)
+    }
+
+    /**
+     * Process mouse enter event
+     */
+    fun onMouseEntered(x: Int, y: Int) {
+        owners.onMouseEntered(x, y)
+    }
+
+    /**
+     * Process mouse exit event
+     */
+    fun onMouseExited() {
+        owners.onMouseExited()
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseHoverFilterTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseHoverFilterTest.kt
new file mode 100644
index 0000000..0793543
--- /dev/null
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseHoverFilterTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.input.mouse
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerMoveFilter
+import androidx.compose.ui.platform.TestComposeWindow
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MouseHoverFilterTest {
+    private val window = TestComposeWindow(width = 100, height = 100, density = Density(2f))
+
+    @Test
+    fun `inside window`() {
+        var moveCount = 0
+        var enterCount = 0
+        var exitCount = 0
+
+        window.setContent {
+            Box(
+                modifier = Modifier
+                    .pointerMoveFilter(
+                        onMove = {
+                            moveCount++
+                            false
+                        },
+                        onEnter = {
+                            enterCount++
+                            false
+                        },
+                        onExit = {
+                            exitCount++
+                            false
+                        }
+                    )
+                    .size(10.dp, 20.dp)
+            )
+        }
+
+        window.onMouseMoved(
+            x = 10,
+            y = 20
+        )
+        assertThat(enterCount).isEqualTo(1)
+        assertThat(exitCount).isEqualTo(0)
+        assertThat(moveCount).isEqualTo(1)
+
+        window.onMouseMoved(
+            x = 10,
+            y = 15
+        )
+        assertThat(enterCount).isEqualTo(1)
+        assertThat(exitCount).isEqualTo(0)
+        assertThat(moveCount).isEqualTo(2)
+
+        window.onMouseMoved(
+            x = 30,
+            y = 30
+        )
+        assertThat(enterCount).isEqualTo(1)
+        assertThat(exitCount).isEqualTo(1)
+        assertThat(moveCount).isEqualTo(2)
+    }
+
+    @Test
+    fun `window enter`() {
+        var moveCount = 0
+        var enterCount = 0
+        var exitCount = 0
+
+        window.setContent {
+            Box(
+                modifier = Modifier
+                    .pointerMoveFilter(
+                        onMove = {
+                            moveCount++
+                            false
+                        },
+                        onEnter = {
+                            enterCount++
+                            false
+                        },
+                        onExit = {
+                            exitCount++
+                            false
+                        }
+                    )
+                    .size(10.dp, 20.dp)
+            )
+        }
+
+        window.onMouseEntered(
+            x = 10,
+            y = 20
+        )
+        assertThat(enterCount).isEqualTo(1)
+        assertThat(exitCount).isEqualTo(0)
+        assertThat(moveCount).isEqualTo(0)
+
+        window.onMouseExited()
+        assertThat(enterCount).isEqualTo(1)
+        assertThat(exitCount).isEqualTo(1)
+        assertThat(moveCount).isEqualTo(0)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
index aba2aab..802806b 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
@@ -48,14 +48,11 @@
 import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
 import androidx.compose.ui.unit.dp
 import com.google.common.truth.Truth
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Assert.assertFalse
+import org.junit.Assume.assumeTrue
 import org.junit.Rule
 import org.junit.Test
 
-@OptIn(
-    ExperimentalCoroutinesApi::class
-)
 class DesktopOwnerTest {
     @get:Rule
     val screenshotRule = DesktopScreenshotTestRule("ui/ui-desktop/ui")
@@ -283,6 +280,8 @@
         height = 40,
         platform = DesktopPlatform.Windows // scrolling behave differently on different platforms
     ) {
+        // Disabled for now, as LazyColumn behaves slightly differently than test.
+        assumeTrue(false)
         var height by mutableStateOf(10.dp)
         setContent {
             Box(Modifier.padding(10.dp)) {
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
index 66e3e5e..434aec0 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
@@ -23,7 +23,6 @@
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.swing.Swing
@@ -33,7 +32,6 @@
 import org.jetbrains.skiko.FrameDispatcher
 import kotlin.coroutines.CoroutineContext
 
-@OptIn(ExperimentalCoroutinesApi::class)
 internal fun renderingTest(
     width: Int,
     height: Int,
diff --git a/compose/ui/ui/src/proguard-rules.pro b/compose/ui/ui/src/proguard-rules.pro
index da6e00d..45df3a1 100644
--- a/compose/ui/ui/src/proguard-rules.pro
+++ b/compose/ui/ui/src/proguard-rules.pro
@@ -17,3 +17,7 @@
 # R8 to complain about them not being there during optimization.
 -dontwarn android.view.RenderNode
 -dontwarn android.view.DisplayListCanvas
+
+-keepclassmember class androidx.compose.ui.platform.ViewLayerContainer {
+    protected void dispatchGetDisplayList();
+}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 5b0bf3d..41517fc 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -1681,6 +1681,33 @@
         assertEquals(2, owner.layoutChangeCount)
     }
 
+    @Test
+    fun reuseModifiersThatImplementMultipleModifierInterfaces() {
+        val drawAndLayoutModifier: Modifier = object : DrawModifier, LayoutModifier {
+            override fun MeasureScope.measure(
+                measurable: Measurable,
+                constraints: Constraints
+            ): MeasureResult {
+                val placeable = measurable.measure(constraints)
+                return layout(placeable.width, placeable.height) {
+                    placeable.placeRelative(IntOffset.Zero)
+                }
+            }
+            override fun ContentDrawScope.draw() {
+                drawContent()
+            }
+        }
+        val a = Modifier.then(EmptyLayoutModifier()).then(drawAndLayoutModifier)
+        val b = Modifier.then(EmptyLayoutModifier()).then(drawAndLayoutModifier)
+        val node = LayoutNode(20, 20, 100, 100)
+        val owner = MockOwner()
+        node.attach(owner)
+        node.modifier = a
+        assertEquals(3, node.getModifierInfo().size)
+        node.modifier = b
+        assertEquals(3, node.getModifierInfo().size)
+    }
+
     private fun createSimpleLayout(): Triple<LayoutNode, LayoutNode, LayoutNode> {
         val layoutNode = ZeroSizedLayoutNode()
         val child1 = ZeroSizedLayoutNode()
@@ -1696,6 +1723,18 @@
         PointerInputModifier
 }
 
+private class EmptyLayoutModifier : LayoutModifier {
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val placeable = measurable.measure(constraints)
+        return layout(placeable.width, placeable.height) {
+            placeable.placeRelative(IntOffset.Zero)
+        }
+    }
+}
+
 @OptIn(InternalCoreApi::class)
 private class MockOwner(
     val position: IntOffset = IntOffset.Zero,
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index cc7820c..ba29bf6 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -33,7 +33,7 @@
 
 dependencies {
     api(KOTLIN_STDLIB)
-    api(project(":core:core"))
+    api("androidx.core:core:1.6.0-alpha01")
 
     implementation("com.google.firebase:firebase-appindexing:19.2.0")
 
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
index 1caa816..dc42557 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
@@ -22,7 +22,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.only;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -36,6 +38,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.firebase.appindexing.Action;
@@ -77,6 +81,7 @@
     @SmallTest
     public void onShortcutUpdated_publicIntent_savesToAppIndex() throws Exception {
         ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+        when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
 
         Intent intent = Intent.parseUri("http://www.google.com", 0);
         ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
@@ -104,8 +109,61 @@
 
     @Test
     @SmallTest
+    public void onShortcutAdded_updateSuccess_reportUsage() throws Exception {
+        ArgumentCaptor<Action> actionCaptor = ArgumentCaptor.forClass(Action.class);
+        Task<Void> result = Tasks.forResult(null);
+        when(mFirebaseAppIndex.update(any())).thenReturn(result);
+
+        Intent intent = Intent.parseUri("http://www.google.com", 0);
+        ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
+                .setShortLabel("short label")
+                .setLongLabel("long label")
+                .setIntent(intent)
+                .setIcon(IconCompat.createWithContentUri("content://abc"))
+                .build();
+
+        mShortcutInfoChangeListener.onShortcutAdded(Collections.singletonList(shortcut));
+
+        // Sleep to make sure the asynchronous call finishes. Since the method is mocked this
+        // should be really quick.
+        Thread.sleep(100);
+        verify(mFirebaseUserActions).end(actionCaptor.capture());
+
+        Action action = actionCaptor.getValue();
+        Action expectedAction = new Action.Builder(Action.Builder.VIEW_ACTION)
+                .setObject("", ShortcutUtils.getIndexableUrl(mContext, "publicIntent"))
+                .setMetadata(new Action.Metadata.Builder().setUpload(false))
+                .build();
+        assertThat(action.toString()).isEqualTo(expectedAction.toString());
+    }
+
+    @Test
+    @SmallTest
+    public void onShortcutAdded_updateError_doNotReportUsage() throws Exception {
+        when(mFirebaseAppIndex.update(any()))
+                .thenReturn(Tasks.forException(new RuntimeException()));
+
+        Intent intent = Intent.parseUri("http://www.google.com", 0);
+        ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
+                .setShortLabel("short label")
+                .setLongLabel("long label")
+                .setIntent(intent)
+                .setIcon(IconCompat.createWithContentUri("content://abc"))
+                .build();
+
+        mShortcutInfoChangeListener.onShortcutAdded(Collections.singletonList(shortcut));
+
+        // Sleep to make sure the asynchronous call finishes. Since the method is mocked this
+        // should be really quick.
+        Thread.sleep(100);
+        verify(mFirebaseUserActions, never()).end(any());
+    }
+
+    @Test
+    @SmallTest
     public void onShortcutUpdated_withCapabilityBinding_savesToAppIndex() throws Exception {
         ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+        when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
 
         Intent intent = Intent.parseUri("http://www.google.com", 0);
         ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
@@ -166,6 +224,7 @@
     @SmallTest
     public void onShortcutUpdated_withCapabilityBindingNoParams_savesToAppIndex() throws Exception {
         ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+        when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
 
         Intent intent = Intent.parseUri("http://www.google.com", 0);
         ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
@@ -196,6 +255,7 @@
     @SmallTest
     public void onShortcutUpdated_privateIntent_savesToAppIndex() throws Exception {
         ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+        when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
 
         String privateIntentUri = "#Intent;component=androidx.core.google.shortcuts.test/androidx"
                 + ".core.google.shortcuts.TrampolineActivity;end";
@@ -224,6 +284,7 @@
     @SmallTest
     public void onShortcutAdded_savesToAppIndex() throws Exception {
         ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+        when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
 
         Intent intent = Intent.parseUri("http://www.google.com", 0);
         ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "intent")
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
index 506441c..7636cd7 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
@@ -24,6 +24,7 @@
 import static androidx.core.google.shortcuts.ShortcutUtils.SHORTCUT_URL_KEY;
 
 import android.content.Context;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -50,6 +51,8 @@
  */
 @RestrictTo(LIBRARY_GROUP)
 public class ShortcutInfoChangeListenerImpl extends ShortcutInfoChangeListener {
+    private static final String TAG = "ShortcutInfoChangeListe";
+
     private final Context mContext;
     private final FirebaseAppIndex mFirebaseAppIndex;
     private final FirebaseUserActions mFirebaseUserActions;
@@ -81,7 +84,7 @@
      */
     @Override
     public void onShortcutAdded(@NonNull List<ShortcutInfoCompat> shortcuts) {
-        mFirebaseAppIndex.update(buildIndexables(shortcuts));
+        updateAndReportUsage(shortcuts);
     }
 
     /**
@@ -91,7 +94,7 @@
      */
     @Override
     public void onShortcutUpdated(@NonNull List<ShortcutInfoCompat> shortcuts) {
-        mFirebaseAppIndex.update(buildIndexables(shortcuts));
+        updateAndReportUsage(shortcuts);
     }
 
     /**
@@ -132,6 +135,25 @@
         mFirebaseAppIndex.removeAll();
     }
 
+    private void updateAndReportUsage(@NonNull List<ShortcutInfoCompat> shortcuts) {
+        mFirebaseAppIndex.update(buildIndexables(shortcuts))
+                .continueWithTask((task) -> {
+                    if (task.isSuccessful()) {
+                        reportUsages(shortcuts);
+                    } else {
+                        Log.e(TAG, "failed to update shortcuts to firebase", task.getException());
+                    }
+                    return null;
+                });
+    }
+
+    private void reportUsages(@NonNull List<ShortcutInfoCompat> shortcuts) {
+        for (ShortcutInfoCompat shortcut : shortcuts) {
+            String url = ShortcutUtils.getIndexableUrl(mContext, shortcut.getId());
+            mFirebaseUserActions.end(buildAction(url));
+        }
+    }
+
     @NonNull
     private Action buildAction(@NonNull String url) {
         // The reported action isn't uploaded to the server.
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index e4767be..4fd4210 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -955,6 +955,7 @@
     ctor protected ContextCompat();
     method public static int checkSelfPermission(android.content.Context, String);
     method public static android.content.Context? createDeviceProtectedStorageContext(android.content.Context);
+    method public static String? getAttributionTag(android.content.Context);
     method public static java.io.File! getCodeCacheDir(android.content.Context);
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
@@ -1619,6 +1620,10 @@
     method @Deprecated public static void setCounter(String, int);
   }
 
+  @RequiresApi(17) public class UserHandleCompat {
+    method public static android.os.UserHandle getUserHandleForUid(int);
+  }
+
   public class UserManagerCompat {
     method public static boolean isUserUnlocked(android.content.Context);
   }
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 862c495..f0121f6 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -955,6 +955,7 @@
     ctor protected ContextCompat();
     method public static int checkSelfPermission(android.content.Context, String);
     method public static android.content.Context? createDeviceProtectedStorageContext(android.content.Context);
+    method public static String? getAttributionTag(android.content.Context);
     method public static java.io.File! getCodeCacheDir(android.content.Context);
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
@@ -1617,6 +1618,10 @@
     method @Deprecated public static void setCounter(String, int);
   }
 
+  @RequiresApi(17) public class UserHandleCompat {
+    method public static android.os.UserHandle getUserHandleForUid(int);
+  }
+
   public class UserManagerCompat {
     method public static boolean isUserUnlocked(android.content.Context);
   }
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index a1037c06..b83a7d3 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1058,6 +1058,7 @@
     ctor protected ContextCompat();
     method public static int checkSelfPermission(android.content.Context, String);
     method public static android.content.Context? createDeviceProtectedStorageContext(android.content.Context);
+    method public static String? getAttributionTag(android.content.Context);
     method public static java.io.File! getCodeCacheDir(android.content.Context);
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
@@ -1938,6 +1939,10 @@
     method @Deprecated public static void setCounter(String, int);
   }
 
+  @RequiresApi(17) public class UserHandleCompat {
+    method public static android.os.UserHandle getUserHandleForUid(int);
+  }
+
   public class UserManagerCompat {
     method public static boolean isUserUnlocked(android.content.Context);
   }
diff --git a/core/core/proguard-rules.pro b/core/core/proguard-rules.pro
index 4efb0d5..47a95b5 100644
--- a/core/core/proguard-rules.pro
+++ b/core/core/proguard-rules.pro
@@ -8,3 +8,6 @@
 -keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.app.NotificationCompat$*$Api*Impl {
   <methods>;
 }
+-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.os.UserHandleCompat$Api*Impl {
+  <methods>;
+}
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index d728332..60e2c56 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -352,6 +352,17 @@
     }
 
     @Test
+    public void testGetAttributionTag() throws Throwable {
+        assertEquals("Unattributed context", null, ContextCompat.getAttributionTag(mContext));
+
+        if (Build.VERSION.SDK_INT >= 30) {
+            // The following test is only expected to pass on v30+ devices
+            Context attributed = mContext.createAttributionContext("test");
+            assertEquals("Attributed context", "test", ContextCompat.getAttributionTag(attributed));
+        }
+    }
+
+    @Test
     public void testGetColor() throws Throwable {
         assertEquals("Unthemed color load", 0xFFFF8090,
                 ContextCompat.getColor(mContext, R.color.text_color));
diff --git a/core/core/src/androidTest/java/androidx/core/os/UserHandleCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/UserHandleCompatTest.java
new file mode 100644
index 0000000..0e38ec4a
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/os/UserHandleCompatTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Process;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SdkSuppress(minSdkVersion = 17)
+@SmallTest
+public class UserHandleCompatTest {
+
+    @Test
+    public void testGetUserHandleForUid() {
+        assertEquals(Process.myUserHandle(), UserHandleCompat.getUserHandleForUid(Process.myUid()));
+    }
+}
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 6b0af66..0497817f3 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -138,11 +138,11 @@
 import androidx.annotation.Nullable;
 import androidx.core.app.ActivityOptionsCompat;
 import androidx.core.os.EnvironmentCompat;
+import androidx.core.os.ExecutorCompat;
 
 import java.io.File;
 import java.util.HashMap;
 import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
 
 /**
  * Helper for accessing features in {@link android.content.Context}.
@@ -163,6 +163,22 @@
     }
 
     /**
+     * <p>Attribution can be used in complex apps to logically separate parts of the app. E.g. a
+     * blogging app might also have a instant messaging app built in. In this case two separate tags
+     * can for used each sub-feature.
+     *
+     * @return the attribution tag this context is for or {@code null} if this is the default.
+     */
+    @Nullable
+    public static String getAttributionTag(@NonNull Context context) {
+        if (Build.VERSION.SDK_INT >= 30) {
+            return context.getAttributionTag();
+        }
+
+        return null;
+    }
+
+    /**
      * Start a set of activities as a synthesized task stack, if able.
      *
      * <p>In API level 11 (Android 3.0/Honeycomb) the recommended conventions for
@@ -663,22 +679,7 @@
         if (Build.VERSION.SDK_INT >= 28) {
             return context.getMainExecutor();
         }
-        return new MainHandlerExecutor(new Handler(context.getMainLooper()));
-    }
-
-    private static class MainHandlerExecutor implements Executor {
-        private final Handler mHandler;
-
-        MainHandlerExecutor(@NonNull Handler handler) {
-            mHandler = handler;
-        }
-
-        @Override
-        public void execute(Runnable command) {
-            if (!mHandler.post(command)) {
-                throw new RejectedExecutionException(mHandler + " is shutting down");
-            }
-        }
+        return ExecutorCompat.create(new Handler(context.getMainLooper()));
     }
 
     /**
diff --git a/core/core/src/main/java/androidx/core/os/UserHandleCompat.java b/core/core/src/main/java/androidx/core/os/UserHandleCompat.java
new file mode 100644
index 0000000..761e388
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/os/UserHandleCompat.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os;
+
+import android.os.Build;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Helper for accessing features in {@link android.os.UserHandle} in a backwards compatible
+ * fashion.
+ */
+@RequiresApi(17)
+public class UserHandleCompat {
+
+    @Nullable
+    private static Method sGetUserIdMethod;
+    @Nullable
+    private static Constructor<UserHandle> sUserHandleConstructor;
+
+    private UserHandleCompat() {
+    }
+
+    /**
+     * Returns the user handle for a given uid.
+     */
+    @NonNull
+    public static UserHandle getUserHandleForUid(int uid) {
+        if (Build.VERSION.SDK_INT >= 24) {
+            return Api24Impl.getUserHandleForUid(uid);
+        } else {
+            try {
+                Integer userId = (Integer) getGetUserIdMethod().invoke(null, uid);
+                return getUserHandleConstructor().newInstance(userId);
+            } catch (NoSuchMethodException e) {
+                Error error = new NoSuchMethodError();
+                error.initCause(e);
+                throw error;
+            } catch (IllegalAccessException e) {
+                Error error = new IllegalAccessError();
+                error.initCause(e);
+                throw error;
+            } catch (InstantiationException e) {
+                Error error = new InstantiationError();
+                error.initCause(e);
+                throw error;
+            } catch (InvocationTargetException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    @RequiresApi(24)
+    private static class Api24Impl {
+
+        private Api24Impl() {
+        }
+
+        @NonNull
+        static UserHandle getUserHandleForUid(int uid) {
+            return UserHandle.getUserHandleForUid(uid);
+        }
+    }
+
+    private static Method getGetUserIdMethod() throws NoSuchMethodException {
+        if (sGetUserIdMethod == null) {
+            sGetUserIdMethod = UserHandle.class.getDeclaredMethod("getUserId", Integer.class);
+            sGetUserIdMethod.setAccessible(true);
+        }
+
+        return sGetUserIdMethod;
+    }
+
+    private static Constructor<UserHandle> getUserHandleConstructor() throws NoSuchMethodException {
+        if (sUserHandleConstructor == null) {
+            sUserHandleConstructor = UserHandle.class.getDeclaredConstructor(Integer.class);
+            sUserHandleConstructor.setAccessible(true);
+        }
+
+        return sUserHandleConstructor;
+    }
+}
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index 81e0c71..68a1b37 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -48,6 +48,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.core.R;
 import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.core.view.InputDeviceCompat;
 import androidx.core.view.NestedScrollingChild3;
@@ -191,7 +192,7 @@
     }
 
     public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
+        this(context, attrs, R.attr.nestedScrollViewStyle);
     }
 
     public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
diff --git a/core/core/src/main/res/values/attrs.xml b/core/core/src/main/res/values/attrs.xml
index ce64ab9..b77c646 100644
--- a/core/core/src/main/res/values/attrs.xml
+++ b/core/core/src/main/res/values/attrs.xml
@@ -15,6 +15,9 @@
   ~ limitations under the License.
   -->
 <resources>
+    <!-- Default style for NestedScrollView. Should be set in your theme. -->
+    <attr name="nestedScrollViewStyle" format="reference" />
+
     <!-- Attributes that are read when parsing a <fontfamily> tag. -->
     <declare-styleable name="FontFamily">
         <!-- The authority of the Font Provider to be used for the request. -->
diff --git a/development/auto-version-updater/README.md b/development/auto-version-updater/README.md
new file mode 100644
index 0000000..f4a7d8d
--- /dev/null
+++ b/development/auto-version-updater/README.md
@@ -0,0 +1,26 @@
+# Auto Version Updater
+
+This script will update versions in LibraryVersions.kt based on Jetpad.
+
+It automatically runs `updateApi` and `repo upload . --cbr --label Presubmit-Ready+1`.
+
+### Using the script
+
+```bash
+./update_versions_for_release.py 1234
+```
+
+Where 1234 is the Jetpad release id.
+
+To use it without creating a commit and uploading a comment, run:
+
+```bash
+./update_versions_for_release.py 1234 --no-commit
+```
+
+### Testing the script
+
+Script test suite
+```bash
+./test_update_versions_for_release.py
+```
\ No newline at end of file
diff --git a/development/auto-version-updater/test_update_versions_for_release.py b/development/auto-version-updater/test_update_versions_for_release.py
new file mode 100755
index 0000000..aa3648a
--- /dev/null
+++ b/development/auto-version-updater/test_update_versions_for_release.py
@@ -0,0 +1,91 @@
+#!/usr/bin/python3
+#
+# 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.
+#
+
+import unittest
+import os
+from update_versions_for_release import *
+
+class TestVersionUpdates(unittest.TestCase):
+
+    def test_increment_version(self):
+        new_version = increment_version("1.0.0-alpha01")
+        self.assertEqual("1.0.0-alpha02", new_version)
+
+        new_version = increment_version("1.1.0-alpha01")
+        self.assertEqual("1.1.0-alpha02", new_version)
+
+        new_version = increment_version("1.0.0-alpha19")
+        self.assertEqual("1.0.0-alpha20", new_version)
+
+        new_version = increment_version("1.0.0-rc01")
+        self.assertEqual("1.1.0-alpha01", new_version)
+
+        new_version = increment_version("1.3.0-beta02")
+        self.assertEqual("1.3.0-beta03", new_version)
+
+        new_version = increment_version("1.0.1")
+        self.assertEqual("1.1.0-alpha01", new_version)
+
+    def test_get_higher_version(self):
+        higher_version = get_higher_version("1.0.0-alpha01", "1.0.0-alpha02")
+        self.assertEqual("1.0.0-alpha02", higher_version)
+
+        higher_version = get_higher_version("1.0.0-alpha02", "1.0.0-alpha01")
+        self.assertEqual("1.0.0-alpha02", higher_version)
+
+        higher_version = get_higher_version("1.0.0-alpha02", "1.0.0-alpha02")
+        self.assertEqual("1.0.0-alpha02", higher_version)
+
+        higher_version = get_higher_version("1.1.0-alpha01", "1.0.0-alpha02")
+        self.assertEqual("1.1.0-alpha01", higher_version)
+
+        higher_version = get_higher_version("1.0.0-rc05", "1.2.0-beta02")
+        self.assertEqual("1.2.0-beta02", higher_version)
+
+        higher_version = get_higher_version("1.3.0-beta01", "1.5.0-beta01")
+        self.assertEqual("1.5.0-beta01", higher_version)
+
+        higher_version = get_higher_version("3.0.0-alpha01", "1.0.0-alpha02")
+        self.assertEqual("3.0.0-alpha01", higher_version)
+
+        higher_version = get_higher_version("1.0.0-beta01", "1.0.0-rc01")
+        self.assertEqual("1.0.0-rc01", higher_version)
+
+        higher_version = get_higher_version("1.4.0-beta01", "1.0.2")
+        self.assertEqual("1.4.0-beta01", higher_version)
+
+        higher_version = get_higher_version("1.4.0-beta01", "1.4.2")
+        self.assertEqual("1.4.2", higher_version)
+
+        higher_version = get_higher_version("1.4.0", "1.4.2")
+        self.assertEqual("1.4.2", higher_version)
+
+    def test_should_update_version_in_library_versions_kt(self):
+        generic_line = "    val CONTENTPAGER = Version(\"1.1.0-alpha01\")"
+        compose_line = "    val COMPOSE = Version(System.getenv(\"COMPOSE_CUSTOM_VERSION\") ?: \"1.0.0-beta04\")"
+        self.assertTrue(should_update_version_in_library_versions_kt(generic_line, "1.1.0-alpha02"))
+        self.assertTrue(should_update_version_in_library_versions_kt(generic_line, "1.3.0-alpha01"))
+        self.assertFalse(should_update_version_in_library_versions_kt(generic_line, "1.0.0-alpha01"))
+
+        self.assertTrue(should_update_version_in_library_versions_kt(compose_line, "1.1.0-alpha02"))
+        self.assertTrue(should_update_version_in_library_versions_kt(compose_line, "1.3.0-alpha01"))
+        self.assertFalse(should_update_version_in_library_versions_kt(compose_line, "1.0.0-alpha01"))
+
+
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/development/auto-version-updater/update_versions_for_release.py b/development/auto-version-updater/update_versions_for_release.py
new file mode 100755
index 0000000..9031d71
--- /dev/null
+++ b/development/auto-version-updater/update_versions_for_release.py
@@ -0,0 +1,323 @@
+#!/usr/bin/python3
+#
+# 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.
+#
+import sys
+import os
+import argparse
+from datetime import date
+import subprocess
+from shutil import rmtree
+from shutil import copyfile
+from distutils.dir_util import copy_tree
+from distutils.dir_util import DistutilsFileError
+
+# Import the JetpadClient from the parent directory
+sys.path.append("..")
+from JetpadClient import *
+
+# cd into directory of script
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+FRAMEWORKS_SUPPORT_FP = os.path.abspath(os.path.join(os.getcwd(), '..', '..'))
+LIBRARY_VERSIONS_REL = './buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt'
+LIBRARY_VERSIONS_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, LIBRARY_VERSIONS_REL)
+
+# Set up input arguments
+parser = argparse.ArgumentParser(
+    description=("""Updates androidx library versions for a given release date.
+        This script takes in a the release date as millisecond since the epoch,
+        which is the unique id for the release in Jetpad.  It queries the
+        Jetpad db, then creates an output json file with the release information.
+        Finally, updates LibraryVersions.kt and runs updateApi."""))
+parser.add_argument(
+    'date',
+    help='Milliseconds since epoch')
+parser.add_argument(
+    '--no-commit', action="store_true",
+    help='If specified, this script will not commit the changes')
+
+def print_e(*args, **kwargs):
+    print(*args, file=sys.stderr, **kwargs)
+
+
+def ask_yes_or_no(question):
+    """Primpts a yes or no question to the user.
+
+    Args:
+        question: the question to asked.
+
+    Returns:
+        boolean representing yes or no.
+    """
+    while(True):
+        reply = str(input(question+' (y/n): ')).lower().strip()
+        if reply:
+            if reply[0] == 'y': return True
+            if reply[0] == 'n': return False
+        print("Please respond with y/n")
+
+
+def run_update_api():
+    """Runs updateApi from the frameworks/support root.
+    """
+    gradle_cmd = "cd " + FRAMEWORKS_SUPPORT_FP + " && ./gradlew updateApi"
+    try:
+        subprocess.check_output(gradle_cmd, stderr=subprocess.STDOUT, shell=True)
+    except subprocess.CalledProcessError:
+        print_e('FAIL: Unable run updateApi with command: %s' % gradle_cmd)
+        return None
+    return True
+
+
+def convert_prerelease_type_to_num(prerelease_type):
+    """" Convert a prerelease suffix type to its numeric equivalent.
+
+    Args:
+        prerelease_type: the androidx SemVer version prerelease suffix.
+
+    Returns:
+        An int representing that suffix.
+    """
+    if prerelease_type == 'alpha':
+        return 0
+    if prerelease_type == 'beta':
+        return 1
+    if prerelease_type == 'rc':
+        return 2
+    # Stable defaults to 9
+    return 9
+
+
+def parse_version(version):
+    """Converts a SemVer androidx version string into a list of ints.
+
+    Accepts a SemVer androidx version string, such as "1.2.0-alpha02" and
+    returns a list of integers representing the version in the following format:
+    [<major>,<minor>,<bugfix>,<prerelease-suffix>,<prerelease-suffix-revision>]
+    For example 1.2.0-alpha02" returns [1,2,0,0,2]
+
+    Args:
+        version: the androidx version string.
+
+    Returns:
+        a list of integers representing the version.
+    """
+    version_elements = version.split('-')[0].split('.')
+    version_list = []
+    for element in version_elements:
+        version_list.append(int(element))
+    # Check if version contains prerelease suffix
+    version_prerelease_suffix = version.split('-')[-1]
+    # Account for suffixes with only 1 suffix number, i.e. "1.1.0-alphaX"
+    version_prerelease_suffix_rev = version_prerelease_suffix[-2:]
+    version_prerelease_suffix_type = version_prerelease_suffix[:-2]
+    if not version_prerelease_suffix_rev.isnumeric():
+        version_prerelease_suffix_rev = version_prerelease_suffix[-1:]
+        version_prerelease_suffix_type = version_prerelease_suffix[:-1]
+    version_list.append(convert_prerelease_type_to_num(version_prerelease_suffix_type))
+    if version.find("-") == -1:
+        # Version contains no prerelease suffix
+        version_list.append(99)
+    else:
+        version_list.append(int(version_prerelease_suffix_rev))
+    return version_list
+
+
+def get_higher_version(version_a, version_b):
+    """Given two androidx SemVer versions, returns the greater one.
+
+    Args:
+        version_a: first version to be compared.
+        version_b: second version to be compared.
+
+    Returns:
+        The greater of version_a and version_b.
+    """
+    version_a_list = parse_version(version_a)
+    version_b_list = parse_version(version_b)
+    for i in range(len(version_a_list)):
+        if version_a_list[i] > version_b_list[i]:
+            return version_a
+        if version_a_list[i] < version_b_list[i]:
+            return version_b
+    return version_a
+
+
+def should_update_version_in_library_versions_kt(line, new_version):
+    """Returns true if the new_version is greater than the version in line.
+
+    Args:
+        line: a line in LibraryVersions.kt file.
+        new_version: the version to check again.
+
+    Returns:
+        True if should update version, false otherwise.
+    """
+    if 'Version(' not in line:
+        return False
+    # Find the first piece with a numeric first character.
+    split_current_line = line.split('"')
+    i = 1
+    while (not split_current_line[i][0].isnumeric() and
+           i < len(split_current_line)):
+        i += 1
+    if i == len(split_current_line):
+        return False
+    version = split_current_line[i]
+    return new_version == get_higher_version(version, new_version)
+
+
+def increment_version(version):
+    """Increments an androidx SemVer version.
+
+    If the version is alpha or beta, the suffix is simply incremented.
+    Otherwise, it chooses the next minor version.
+
+    Args:
+        version: the version to be incremented.
+
+    Returns:
+        The incremented version.
+    """
+    if "alpha" in version or "beta" in version:
+        version_prerelease_suffix = version[-2:]
+        new_version_prerelease_suffix = int(version_prerelease_suffix) + 1
+        new_version = version[:-2] + "%02d" % (new_version_prerelease_suffix,)
+    else:
+        version_minor = version.split(".")[1]
+        new_version_minor = str(int(version_minor) + 1)
+        new_version = version.split(".")[0] + "." + new_version_minor + ".0-alpha01"
+    return new_version
+
+
+def update_versions_in_library_versions_kt(group_id, artifact_id, old_version):
+    """Updates the versions in the LibrarVersions.kt file.
+
+    This will take the old_version and increment it to find the appropriate
+    new version.
+
+    Args:
+        group_id: group_id of the existing library
+        artifact_id: artifact_id of the existing library
+        old_version: old version of the existing library
+
+    Returns:
+        True if the version was updated, false otherwise.
+    """
+    group_id_variable_name = group_id.replace("androidx.","").replace(".","_").upper()
+    artifact_id_variable_name = artifact_id.replace("androidx.","").replace("-","_").upper()
+    new_version = increment_version(old_version)
+    # Special case Compose because it uses the same version variable.
+    if group_id_variable_name.startswith("COMPOSE"):
+        group_id_variable_name = "COMPOSE"
+
+    # Open file for reading and get all lines
+    with open(LIBRARY_VERSIONS_FP, 'r') as f:
+        library_versions_lines = f.readlines()
+    num_lines = len(library_versions_lines)
+    updated_version = False
+
+    # First check any artifact ids with unique versions.
+    for i in range(num_lines):
+        cur_line = library_versions_lines[i]
+        # Skip any line that doesn't declare a version
+        if 'Version(' not in cur_line: continue
+        version_variable_name = cur_line.split('val ')[1].split(' =')[0]
+        if artifact_id_variable_name == version_variable_name:
+            if not should_update_version_in_library_versions_kt(cur_line, new_version):
+                break
+            # Found the correct variable to modify
+            if version_variable_name == "COMPOSE":
+                new_version_line = ("    val COMPOSE = Version("
+                                    "System.getenv(\"COMPOSE_CUSTOM_VERSION\") "
+                                    "?: \"" + new_version + "\")\n")
+            else:
+                new_version_line = "    val " + version_variable_name + \
+                                   " = Version(\"" + new_version + "\")\n"
+            library_versions_lines[i] = new_version_line
+            updated_version = True
+            break
+
+    if not updated_version:
+        # Then check any group ids.
+        for i in range(num_lines):
+            cur_line = library_versions_lines[i]
+            # Skip any line that doesn't declare a version
+            if 'Version(' not in cur_line: continue
+            version_variable_name = cur_line.split('val ')[1].split(' =')[0]
+            if group_id_variable_name == version_variable_name:
+                if not should_update_version_in_library_versions_kt(cur_line, new_version):
+                    break
+                # Found the correct variable to modify
+                if version_variable_name == "COMPOSE":
+                    new_version_line = ("    val COMPOSE = Version("
+                                        "System.getenv(\"COMPOSE_CUSTOM_VERSION\") "
+                                        "?: \"" + new_version + "\")\n")
+                else:
+                    new_version_line = "    val " + version_variable_name + \
+                                       " = Version(\"" + new_version + "\")\n"
+                library_versions_lines[i] = new_version_line
+                updated_version = True
+                break
+
+    # Open file for writing and update all lines
+    with open(LIBRARY_VERSIONS_FP, 'w') as f:
+        f.writelines(library_versions_lines)
+    return updated_version
+
+
+def commit_updates(release_date):
+    subprocess.check_call(['git', 'add', FRAMEWORKS_SUPPORT_FP])
+    # ensure that we've actually made a change:
+    staged_changes = subprocess.check_output('git diff --cached', stderr=subprocess.STDOUT, shell=True)
+    if not staged_changes:
+        return
+    msg = "Update versions for release id %s\n\nThis commit was generated from the command:\n%s\n\n%s" % (release_date, " ".join(sys.argv), 'Test: ./gradlew checkApi')
+    subprocess.check_call(['git', 'commit', '-m', msg])
+    subprocess.check_call(['yes', '|', 'repo', 'upload', '.', '--cbr', '--label', 'Presubmit-Ready+1'])
+
+def main(args):
+    # Parse arguments and check for existence of build ID or file
+    args = parser.parse_args()
+    if not args.date:
+        parser.error("You must specify a release date in Milliseconds since epoch")
+        sys.exit(1)
+    release_json_object = getJetpadRelease(args.date, False)
+    non_updated_libraries = []
+    for group_id in release_json_object["modules"]:
+        for artifact in release_json_object["modules"][group_id]:
+            updated = update_versions_in_library_versions_kt(group_id,
+                artifact["artifactId"], artifact["version"])
+            if not updated:
+                non_updated_libraries.append("%s:%s:%s" % (group_id,
+                                             artifact["artifactId"],
+                                             artifact["version"]))
+    if non_updated_libraries:
+        print("The following libraries were not updated:")
+        for library in non_updated_libraries:
+            print("\t", library)
+    print("Updated library versions. \nRunning updateApi for the new "
+          "versions, this may take a minute...", end='')
+    if run_update_api():
+        print("done.")
+    else:
+        print_e("failed.  Please investigate manually.")
+    if not args.no_commit:
+        commit_updates(args.date)
+
+
+if __name__ == '__main__':
+    main(sys.argv)
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 4c612db..4e3dea0 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -215,6 +215,7 @@
 DIST_DIR=\$DIST_DIR
 CHECKOUT=\$CHECKOUT
 GRADLE_USER_HOME=\$GRADLE_USER_HOME
+Starting a Gradle Daemon \(subsequent builds will be faster\)
 Downloading file\:\$SUPPORT\/gradle\/wrapper\/\.\.\/\.\.\/\.\.\/\.\.\/tools\/external\/gradle\/gradle\-[0-9]+\.[0-9]+\.[0-9]+\-bin\.zip
 \.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%
 Welcome to Gradle [0-9]+\.[0-9]+\.[0-9]+\!
@@ -328,8 +329,8 @@
 # > Task :support-preference-demos:compileDebugJavaWithJavac
 # > Task :startup:integration-tests:first-library:processDebugManifest
 \$SUPPORT/startup/integration\-tests/first\-library/src/main/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-meta\-data\#androidx\.work\.impl\.WorkManagerInitializer was tagged at AndroidManifest\.xml\:[0-9]+ to remove other declarations but no other declaration present
-provider\#androidx\.work\.impl\.WorkManagerInitializer was tagged at AndroidManifest\.xml:[0-9]+ to remove other declarations but no other declaration present
+meta\-data\#androidx\.work\.WorkManagerInitializer was tagged at AndroidManifest\.xml\:[0-9]+ to remove other declarations but no other declaration present
+provider\#androidx\.work\.WorkManagerInitializer was tagged at AndroidManifest\.xml:[0-9]+ to remove other declarations but no other declaration present
 # > Task :camera:integration-tests:camera-testapp-extensions:compileDebugJavaWithJavac
 # > Task :camera:integration-tests:camera-testapp-core:compileDebugJavaWithJavac
 # > Task :room:integration-tests:room-testapp:processDebugMainManifest
@@ -553,6 +554,7 @@
 java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\.awaitHorizontalTouchSlopOrCancellation\-jO[0-9]+t[0-9]+\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, long, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
 java\.lang\.Object androidx\.compose\.foundation\.gestures\.TransformGestureDetectorKt\$detectTransformGestures\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
 Type information in locals\-table is inconsistent\. Cannot constrain type: INT for value: v[0-9]+\(index\$iv\$iv\) by constraint FLOAT\.
+java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.translatePointerEventsToChannel\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, kotlinx\.coroutines\.CoroutineScope, kotlinx\.coroutines\.channels\.SendChannel, androidx\.compose\.runtime\.State, androidx\.compose\.runtime\.MutableState, kotlin\.coroutines\.Continuation\)
 java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancellation\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, kotlin\.coroutines\.Continuation\)
 # > Task :preference:preference:compileDebugAndroidTestKotlin
 w\: \$SUPPORT\/preference\/preference\/src\/androidTest\/java\/androidx\/preference\/tests\/PreferenceDialogFragmentCompatTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTargetFragment\(Fragment\?\, Int\)\: Unit\' is deprecated\. Deprecated in Java
@@ -608,4 +610,4 @@
 # > Task :compose:animation:animation-core:animation-core-benchmark:processReleaseAndroidTestManifest
 \$OUT_DIR\/androidx\/compose\/animation\/animation\-core\/animation\-core\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
 # > Task :compose:ui:ui-graphics:ui-graphics-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR\/androidx\/compose\/ui\/ui\-graphics\/ui\-graphics\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
+\$OUT_DIR\/androidx\/compose\/ui\/ui\-graphics\/ui\-graphics\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
\ No newline at end of file
diff --git a/development/importMaven/build.gradle.kts b/development/importMaven/build.gradle.kts
index 9c9513a..63b7fea 100644
--- a/development/importMaven/build.gradle.kts
+++ b/development/importMaven/build.gradle.kts
@@ -446,7 +446,11 @@
             }
         }
         val variantNames = listOf(
-            "runtimeElements", "releaseRuntimePublication", "metadata-api", "runtime"
+            "runtimeElements",
+            "releaseRuntimePublication",
+            "metadata-api",
+            "metadataApiElements-published",
+            "runtime"
         )
         variantNames.forEach { name ->
             ctx.details.maybeAddVariant("allFilesWithDependencies${name.capitalize()}", name) {
@@ -464,6 +468,7 @@
                     addFile("${id.name}-${id.version}.jar")
                     addFile("${id.name}-${id.version}.aar")
                     addFile("${id.name}-${id.version}-sources.jar")
+                    addFile("${id.name}-${id.version}.klib")
                 }
             }
         }
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 9111882..78a2806 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -37,6 +37,7 @@
     docs(project(":camera:camera-extensions"))
     stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
     docs(project(":camera:camera-lifecycle"))
+    docs(project(":camera:camera-video"))
     docs(project(":camera:camera-view"))
     docs(project(":car:app:app"))
     docs(project(":car:app:app-activity"))
@@ -56,12 +57,15 @@
     docs(project(":compose:material:material-icons-core"))
     samples(project(":compose:material:material-icons-core:material-icons-core-samples"))
     docs(project(":compose:material:material-icons-extended"))
+    docs(project(":compose:material:material-ripple"))
     samples(project(":compose:material:material:material-samples"))
     docs(project(":compose:runtime:runtime"))
     docs(project(":compose:runtime:runtime-livedata"))
     samples(project(":compose:runtime:runtime-livedata:runtime-livedata-samples"))
     docs(project(":compose:runtime:runtime-rxjava2"))
     samples(project(":compose:runtime:runtime-rxjava2:runtime-rxjava2-samples"))
+    docs(project(":compose:runtime:runtime-rxjava3"))
+    samples(project(":compose:runtime:runtime-rxjava3:runtime-rxjava3-samples"))
     docs(project(":compose:runtime:runtime-saveable"))
     samples(project(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
     samples(project(":compose:runtime:runtime:runtime-samples"))
@@ -76,6 +80,7 @@
     docs(project(":compose:ui:ui-text"))
     samples(project(":compose:ui:ui-text:ui-text-samples"))
     docs(project(":compose:ui:ui-tooling"))
+    docs(project(":compose:ui:ui-tooling-data"))
     docs(project(":compose:ui:ui-unit"))
     samples(project(":compose:ui:ui-unit:ui-unit-samples"))
     docs(project(":compose:ui:ui-util"))
@@ -100,8 +105,11 @@
     docs(project(":datastore:datastore-preferences"))
     docs(project(":datastore:datastore-preferences-core"))
     docs(project(":datastore:datastore-preferences-core:datastore-preferences-proto"))
+    docs(project(":datastore:datastore-preferences-rxjava2"))
+    docs(project(":datastore:datastore-preferences-rxjava3"))
     docs(project(":datastore:datastore-proto"))
     docs(project(":datastore:datastore-rxjava2"))
+    docs(project(":datastore:datastore-rxjava3"))
     docs(project(":documentfile:documentfile"))
     docs(project(":drawerlayout:drawerlayout"))
     docs(project(":dynamicanimation:dynamicanimation"))
@@ -142,6 +150,7 @@
     docs(project(":lifecycle:lifecycle-service"))
     docs(project(":lifecycle:lifecycle-viewmodel"))
     docs(project(":lifecycle:lifecycle-viewmodel-compose"))
+    samples(project(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
     docs(project(":lifecycle:lifecycle-viewmodel-ktx"))
     docs(project(":lifecycle:lifecycle-viewmodel-savedstate"))
     docs(project(":loader:loader"))
@@ -216,6 +225,7 @@
     docs(project(":startup:startup-runtime"))
     docs(project(":legacy-support-core-utils"))
     docs(project(":swiperefreshlayout:swiperefreshlayout"))
+    docs(project(":text:text"))
     docs(project(":textclassifier:textclassifier"))
     docs(project(":tracing:tracing"))
     docs(project(":tracing:tracing-ktx"))
@@ -232,6 +242,7 @@
     stubs(fileTree(dir: "../wear/wear_stubs/", include: ["com.google.android.wearable-stubs.jar"]))
     docs(project(":wear:wear-complications-data"))
     docs(project(":wear:wear-complications-provider"))
+    samples(project(":wear:wear-complications-provider-samples"))
     docs(project(":wear:wear-input"))
     docs(project(":wear:wear-input-testing"))
     docs(project(":wear:wear-ongoing"))
@@ -247,7 +258,9 @@
     docs(project(":wear:wear-watchface-data"))
     docs(project(":wear:wear-watchface-editor"))
     docs(project(":wear:wear-watchface-editor-guava"))
+    docs(project(":wear:wear-watchface-editor-samples"))
     docs(project(":wear:wear-watchface-guava"))
+    samples(project(":wear:wear-watchface-samples"))
     docs(project(":wear:wear-watchface-style"))
     docs(project(":webkit:webkit"))
     docs(project(":window:window"))
diff --git a/emoji2/emoji2-benchmark/build.gradle b/emoji2/emoji2-benchmark/build.gradle
index 643376d..17f0792 100644
--- a/emoji2/emoji2-benchmark/build.gradle
+++ b/emoji2/emoji2-benchmark/build.gradle
@@ -13,11 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import static androidx.build.dependencies.DependenciesKt.*
+
 import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
 import androidx.build.Publish
 
+import static androidx.build.dependencies.DependenciesKt.*
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
diff --git a/emoji2/emoji2-bundled/build.gradle b/emoji2/emoji2-bundled/build.gradle
index 9d49183..b48c953 100644
--- a/emoji2/emoji2-bundled/build.gradle
+++ b/emoji2/emoji2-bundled/build.gradle
@@ -1,7 +1,8 @@
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 import androidx.build.Publish
-import androidx.build.RunApiTasks
+
+import static androidx.build.dependencies.DependenciesKt.*
 
 plugins {
     id("AndroidXPlugin")
@@ -14,12 +15,34 @@
 
 android {
     sourceSets {
-        main.assets.srcDirs new File(fontDir, "font").getAbsolutePath()
+        main {
+            assets.srcDirs new File(fontDir, "font").getAbsolutePath()
+            resources {
+                srcDirs += [fontDir.getAbsolutePath()]
+                includes += ["LICENSE_UNICODE", "LICENSE_OFL"]
+            }
+        }
+
+        androidTest {
+            assets {
+                srcDirs = [new File(fontDir, "supported-emojis").getAbsolutePath()]
+            }
+        }
     }
+
 }
 
 dependencies {
     api(project(":emoji2:emoji2"))
+
+    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation project(':internal-testutils-runtime')
 }
 
 androidx {
diff --git a/emoji2/emoji2/src/androidTest/AndroidManifest.xml b/emoji2/emoji2-bundled/src/androidTest/AndroidManifest.xml
similarity index 94%
rename from emoji2/emoji2/src/androidTest/AndroidManifest.xml
rename to emoji2/emoji2-bundled/src/androidTest/AndroidManifest.xml
index 0b2cc8b..3b9ba38f 100644
--- a/emoji2/emoji2/src/androidTest/AndroidManifest.xml
+++ b/emoji2/emoji2-bundled/src/androidTest/AndroidManifest.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="androidx.emoji2.text">
+          package="androidx.emoji2.bundled">
 
     <application>
         <activity android:name=".TestActivity"/>
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
similarity index 95%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
index 6e5a680..1e5a600 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
@@ -23,8 +23,11 @@
 import android.text.Spanned;
 
 import androidx.core.graphics.PaintCompat;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiMetadata;
+import androidx.emoji2.text.EmojiSpan;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/ConfigTest.java
similarity index 95%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/ConfigTest.java
index 34525b1..fb1be38 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/ConfigTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -31,9 +31,10 @@
 import android.content.Context;
 import android.graphics.Color;
 
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
@@ -58,16 +59,19 @@
 
     @Test(expected = NullPointerException.class)
     public void testConstructor_throwsExceptionIfMetadataLoaderNull() {
+        //noinspection ConstantConditions
         new TestConfigBuilder.TestConfig(null);
     }
 
     @Test(expected = NullPointerException.class)
     public void testInitCallback_throwsExceptionIfNull() {
+        //noinspection ConstantConditions
         new ValidTestConfig().registerInitCallback(null);
     }
 
     @Test(expected = NullPointerException.class)
     public void testUnregisterInitCallback_throwsExceptionIfNull() {
+        //noinspection ConstantConditions
         new ValidTestConfig().unregisterInitCallback(null);
     }
 
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiCompatTest.java
similarity index 97%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiCompatTest.java
index 46d51bc..2315bff 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiCompatTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.hamcrest.Matchers.instanceOf;
 import static org.junit.Assert.assertEquals;
@@ -46,10 +46,12 @@
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.KeyboardUtil;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.KeyboardUtil;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiSpan;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SdkSuppress;
@@ -703,8 +705,8 @@
     @Test
     @SdkSuppress(maxSdkVersion = 18)
     public void testLoad_pre19() {
-        final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
-                .TestEmojiDataLoader());
+        final EmojiCompat.MetadataRepoLoader loader =
+                Mockito.spy(new TestConfigBuilder.TestEmojiDataLoader());
         final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
                 .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
 
@@ -720,8 +722,8 @@
     @Test
     @SdkSuppress(minSdkVersion = 19)
     public void testLoad_startsLoading() {
-        final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
-                .TestEmojiDataLoader());
+        final EmojiCompat.MetadataRepoLoader loader =
+                Mockito.spy(new TestConfigBuilder.TestEmojiDataLoader());
         final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
                 .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
 
@@ -738,8 +740,8 @@
     @Test
     @SdkSuppress(minSdkVersion = 19)
     public void testLoad_onceSuccessDoesNotStartLoading() {
-        final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
-                .TestEmojiDataLoader());
+        final EmojiCompat.MetadataRepoLoader loader =
+                Mockito.spy(new TestConfigBuilder.TestEmojiDataLoader());
         final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
                 .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
 
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiKeyboardTest.java
similarity index 95%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiKeyboardTest.java
index 9b813d4..8b4de90 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiKeyboardTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.junit.Assert.assertThat;
 
@@ -22,11 +22,12 @@
 import android.view.inputmethod.InputConnection;
 import android.widget.EditText;
 
-import androidx.emoji2.test.R;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.KeyboardUtil;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.test.R;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.KeyboardUtil;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.Suppress;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiSpanInstrumentationTest.java
similarity index 92%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiSpanInstrumentationTest.java
index 573a8f4..7ece287 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiSpanInstrumentationTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -27,10 +27,12 @@
 import android.util.TypedValue;
 import android.widget.TextView;
 
-import androidx.emoji2.test.R;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.test.R;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiSpan;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/FontRequestEmojiCompatConfigTest.java
similarity index 86%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/FontRequestEmojiCompatConfigTest.java
index 92afb67..18cff62 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/FontRequestEmojiCompatConfigTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static android.content.res.AssetManager.ACCESS_BUFFER;
 
@@ -53,6 +53,9 @@
 import androidx.core.provider.FontRequest;
 import androidx.core.provider.FontsContractCompat.FontFamilyResult;
 import androidx.core.provider.FontsContractCompat.FontInfo;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.FontRequestEmojiCompatConfig;
+import androidx.emoji2.text.MetadataRepo;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -68,7 +71,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -84,17 +86,19 @@
     public void setup() {
         mContext = ApplicationProvider.getApplicationContext();
         mFontRequest = new FontRequest("authority", "package", "query",
-                new ArrayList<List<byte[]>>());
+                new ArrayList<>());
         mFontProviderHelper = mock(FontRequestEmojiCompatConfig.FontProviderHelper.class);
     }
 
     @Test(expected = NullPointerException.class)
     public void testConstructor_withNullContext() {
+        //noinspection ConstantConditions
         new FontRequestEmojiCompatConfig(null, mFontRequest);
     }
 
     @Test(expected = NullPointerException.class)
     public void testConstructor_withNullFontRequest() {
+        //noinspection ConstantConditions
         new FontRequestEmojiCompatConfig(mContext, null);
     }
 
@@ -105,10 +109,10 @@
         doThrow(exception).when(mFontProviderHelper).fetchFonts(
                 any(Context.class), any(FontRequest.class));
         final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
-        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
-                mFontProviderHelper);
+        final TestFontRequestEmojiCompatConfig config =
+                new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
 
-        config.getMetadataRepoLoader().load(callback);
+        config.loadForTests(callback);
         callback.await(DEFAULT_TIMEOUT_MILLIS);
         verify(callback, times(1)).onFailed(same(exception));
     }
@@ -119,15 +123,16 @@
         doThrow(new NameNotFoundException()).when(mFontProviderHelper).fetchFonts(
                 any(Context.class), any(FontRequest.class));
         final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
-        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
-                mFontRequest, mFontProviderHelper);
+        final TestFontRequestEmojiCompatConfig config =
+                new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
 
-        config.getMetadataRepoLoader().load(callback);
+        config.loadForTests(callback);
         callback.await(DEFAULT_TIMEOUT_MILLIS);
 
         final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
         verify(callback, times(1)).onFailed(argumentCaptor.capture());
-        assertThat(argumentCaptor.getValue().getMessage(), containsString("provider not found"));
+        assertThat(argumentCaptor.getValue().getMessage(),
+                containsString("provider not found"));
     }
 
     @Test
@@ -195,10 +200,10 @@
         doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
                 any(Context.class), any(FontRequest.class));
         final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
-        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
-                mFontRequest, mFontProviderHelper);
+        final TestFontRequestEmojiCompatConfig config =
+                new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
 
-        config.getMetadataRepoLoader().load(callback);
+        config.loadForTests(callback);
         callback.await(DEFAULT_TIMEOUT_MILLIS);
         verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
     }
@@ -215,10 +220,11 @@
                 any(Context.class), any(FontRequest.class));
         final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
         final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(-1, 1));
-        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
-                mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
+        final TestFontRequestEmojiCompatConfig config =
+                new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
+        config.setRetryPolicy(retryPolicy);
 
-        config.getMetadataRepoLoader().load(callback);
+        config.loadForTests(callback);
         callback.await(DEFAULT_TIMEOUT_MILLIS);
         verify(callback, never()).onLoaded(any(MetadataRepo.class));
         verify(callback, times(1)).onFailed(any(Throwable.class));
@@ -237,10 +243,11 @@
                 any(Context.class), any(FontRequest.class));
         final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
         final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
-        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
-                mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
+        final TestFontRequestEmojiCompatConfig config =
+                new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
+        config.setRetryPolicy(retryPolicy);
 
-        config.getMetadataRepoLoader().load(callback);
+        config.loadForTests(callback);
         retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
         verify(callback, never()).onLoaded(any(MetadataRepo.class));
         verify(callback, never()).onFailed(any(Throwable.class));
@@ -271,11 +278,11 @@
         try {
             Handler handler = new Handler(thread.getLooper());
 
-            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
-                    mFontRequest, mFontProviderHelper).setHandler(handler)
-                    .setRetryPolicy(retryPolicy);
+            final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+                    mContext, mFontRequest, mFontProviderHelper);
+            config.setHandler(handler).setRetryPolicy(retryPolicy);
 
-            config.getMetadataRepoLoader().load(callback);
+            config.loadForTests(callback);
             retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
             verify(callback, never()).onLoaded(any(MetadataRepo.class));
             verify(callback, never()).onFailed(any(Throwable.class));
@@ -328,11 +335,11 @@
         try {
             Handler handler = new Handler(thread.getLooper());
 
-            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
-                    mFontRequest, mFontProviderHelper).setHandler(handler)
-                    .setRetryPolicy(retryPolicy);
+            final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+                    mContext, mFontRequest, mFontProviderHelper);
+            config.setHandler(handler).setRetryPolicy(retryPolicy);
 
-            config.getMetadataRepoLoader().load(callback);
+            config.loadForTests(callback);
             retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
             verify(callback, never()).onLoaded(any(MetadataRepo.class));
             verify(callback, never()).onFailed(any(Throwable.class));
@@ -383,14 +390,14 @@
         thread.start();
         try {
             Handler handler = new Handler(thread.getLooper());
-            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
-                    mFontRequest, mFontProviderHelper).setHandler(handler)
-                    .setRetryPolicy(retryPolicy);
+            final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+                    mContext, mFontRequest, mFontProviderHelper);
+            config.setHandler(handler).setRetryPolicy(retryPolicy);
 
             ArgumentCaptor<ContentObserver> observerCaptor =
                     ArgumentCaptor.forClass(ContentObserver.class);
 
-            config.getMetadataRepoLoader().load(callback);
+            config.loadForTests(callback);
             retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
             verify(callback, never()).onLoaded(any(MetadataRepo.class));
             verify(callback, never()).onFailed(any(Throwable.class));
@@ -439,14 +446,14 @@
         thread.start();
         try {
             Handler handler = new Handler(thread.getLooper());
-            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
-                    mFontRequest, mFontProviderHelper).setHandler(handler)
-                    .setRetryPolicy(retryPolicy);
+            final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+                    mContext, mFontRequest, mFontProviderHelper);
+            config.setRetryPolicy(retryPolicy).setHandler(handler);
 
             ArgumentCaptor<ContentObserver> observerCaptor =
                     ArgumentCaptor.forClass(ContentObserver.class);
 
-            config.getMetadataRepoLoader().load(callback);
+            config.loadForTests(callback);
             retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
             verify(callback, never()).onLoaded(any(MetadataRepo.class));
             verify(callback, never()).onFailed(any(Throwable.class));
@@ -482,10 +489,10 @@
         doReturn(new FontFamilyResult(statusCode, fonts)).when(mFontProviderHelper).fetchFonts(
                 any(Context.class), any(FontRequest.class));
         final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
-        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
-                mFontProviderHelper);
+        final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+                mContext, mFontRequest, mFontProviderHelper);
 
-        config.getMetadataRepoLoader().load(callback);
+        config.loadForTests(callback);
         callback.await(DEFAULT_TIMEOUT_MILLIS);
 
         final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
@@ -582,4 +589,28 @@
         return new FontInfo[] { new FontInfo(Uri.parse("file:///some/placeholder/file"),
                 0 /* ttc index */, 400 /* weight */, false /* italic */, resultCode) };
     }
+
+    /**
+     * This class exists since the test suite is in another package and we need to call the
+     * `protected` method `getMetadataRepoLoader` in these tests.
+     */
+    private static class TestFontRequestEmojiCompatConfig extends FontRequestEmojiCompatConfig {
+
+        TestFontRequestEmojiCompatConfig(
+                @NonNull Context context,
+                @NonNull FontRequest request) {
+            super(context, request);
+        }
+
+        TestFontRequestEmojiCompatConfig(
+                @NonNull Context context,
+                @NonNull FontRequest request,
+                @NonNull FontRequestEmojiCompatConfig.FontProviderHelper fontProviderHelper) {
+            super(context, request, fontProviderHelper);
+        }
+
+        public void loadForTests(EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            getMetadataRepoLoader().load(loaderCallback);
+        }
+    }
 }
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/HardDeleteTest.java
similarity index 96%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/HardDeleteTest.java
index 5ea5680..5ef6199 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/HardDeleteTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -24,10 +24,11 @@
 import android.text.SpannableStringBuilder;
 import android.view.KeyEvent;
 
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.KeyboardUtil;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.KeyboardUtil;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/InitCallbackTest.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/InitCallbackTest.java
index 118d823..40329c0 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/InitCallbackTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.nullable;
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.verify;
 
 import androidx.annotation.NonNull;
+import androidx.emoji2.text.EmojiCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SdkSuppress;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/SoftDeleteTest.java
similarity index 97%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/SoftDeleteTest.java
index 9890903..a945feb 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/SoftDeleteTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -26,9 +26,10 @@
 import android.text.SpannableStringBuilder;
 import android.view.inputmethod.InputConnection;
 
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestActivity.java
similarity index 92%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestActivity.java
index bae3630..82ae1a0 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestActivity.java
@@ -13,12 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import android.app.Activity;
 import android.os.Bundle;
 
-import androidx.emoji2.test.R;
+import androidx.emoji2.bundled.test.R;
 
 public class TestActivity extends Activity {
 
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestConfigBuilder.java
similarity index 95%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestConfigBuilder.java
index e44b5a4..e0f9112 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestConfigBuilder.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
 
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
@@ -24,8 +24,9 @@
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.MetadataRepo;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.text.emoji.flatbuffer.MetadataList;
 
 import java.util.concurrent.CountDownLatch;
 
@@ -52,7 +53,7 @@
             super(new TestEmojiDataLoader());
         }
 
-        TestConfig(final EmojiCompat.MetadataRepoLoader metadataLoader) {
+        TestConfig(@NonNull final EmojiCompat.MetadataRepoLoader metadataLoader) {
             super(metadataLoader);
         }
     }
@@ -88,8 +89,7 @@
                     try {
                         mLoaderLatch.await();
                         if (mSuccess) {
-                            loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class),
-                                    new MetadataList()));
+                            loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class)));
                         } else {
                             loaderCallback.onFailed(null);
                         }
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/Emoji.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/Emoji.java
index 73117a2..1413eb4 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/Emoji.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.emoji2.util;
+package androidx.emoji2.bundled.util;
 
 import androidx.annotation.NonNull;
 
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/EmojiMatcher.java
similarity index 99%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/EmojiMatcher.java
index 32d8e03..3025c68 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/EmojiMatcher.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.util;
+package androidx.emoji2.bundled.util;
 
 import static org.mockito.ArgumentMatchers.argThat;
 
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/KeyboardUtil.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/KeyboardUtil.java
index 48a6d2e..f640452 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/KeyboardUtil.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.util;
+package androidx.emoji2.bundled.util;
 
 import android.app.Instrumentation;
 import android.text.Selection;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/TestString.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/TestString.java
index 83728da..eaf0213 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/TestString.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.emoji2.util;
+package androidx.emoji2.bundled.util;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml b/emoji2/emoji2-bundled/src/androidTest/res/layout/activity_default.xml
similarity index 100%
rename from emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
rename to emoji2/emoji2-bundled/src/androidTest/res/layout/activity_default.xml
diff --git a/emoji2/emoji2-bundled/src/main/AndroidManifest.xml b/emoji2/emoji2-bundled/src/main/AndroidManifest.xml
index 56ac589..ee983ff 100644
--- a/emoji2/emoji2-bundled/src/main/AndroidManifest.xml
+++ b/emoji2/emoji2-bundled/src/main/AndroidManifest.xml
@@ -13,4 +13,4 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<manifest package="androidx.emoji.bundled"/>
\ No newline at end of file
+<manifest package="androidx.emoji2.bundled"/>
\ No newline at end of file
diff --git a/emoji2/emoji2-views-helper/build.gradle b/emoji2/emoji2-views-helper/build.gradle
index 6137849..4acd93b 100644
--- a/emoji2/emoji2-views-helper/build.gradle
+++ b/emoji2/emoji2-views-helper/build.gradle
@@ -1,4 +1,3 @@
-import androidx.build.BundleInsideHelper
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 import androidx.build.Publish
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
index 2882681..b60150b 100644
--- a/emoji2/emoji2/build.gradle
+++ b/emoji2/emoji2/build.gradle
@@ -2,7 +2,6 @@
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 import androidx.build.Publish
-import androidx.build.RunApiTasks
 
 import static androidx.build.dependencies.DependenciesKt.*
 
@@ -12,10 +11,6 @@
     id("com.github.johnrengelman.shadow")
 }
 
-ext {
-    fontDir = project(':noto-emoji-compat-font').projectDir
-}
-
 BundleInsideHelper.forInsideAar(
     project,
     /* from = */ "com.google.flatbuffers",
@@ -43,17 +38,6 @@
         main {
             // We use a non-standard manifest path.
             manifest.srcFile 'AndroidManifest.xml'
-            resources {
-                srcDirs += [fontDir.getAbsolutePath()]
-                includes += ["LICENSE_UNICODE", "LICENSE_OFL"]
-            }
-        }
-
-        androidTest {
-            assets {
-                srcDirs = [new File(fontDir, "font").getAbsolutePath(),
-                           new File(fontDir, "supported-emojis").getAbsolutePath()]
-            }
         }
     }
 }
@@ -65,14 +49,4 @@
     mavenGroup = LibraryGroups.EMOJI2
     inceptionYear = "2017"
     description = "Core library to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters."
-
-    license {
-        name = "SIL Open Font License, Version 1.1"
-        url = "http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web"
-    }
-
-    license {
-        name = "Unicode, Inc. License"
-        url = "http://www.unicode.org/copyright.html#License"
-    }
 }
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java
index 77694aa..ff969db 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java
@@ -46,7 +46,7 @@
 
     @Before
     public void setup() {
-        EmojiCompat.reset(TestConfigBuilder.config());
+        EmojiCompat.reset(NoFontTestEmojiConfig.emptyConfig());
     }
 
     @Test
@@ -97,14 +97,14 @@
         final int bottom = 30;
 
         // verify the case where indicators are disabled
-        EmojiCompat.reset(TestConfigBuilder.config().setEmojiSpanIndicatorEnabled(false));
+        EmojiCompat.reset(NoFontTestEmojiConfig.emptyConfig().setEmojiSpanIndicatorEnabled(false));
         span.draw(canvas, "a", 0 /*start*/, 1 /*end*/, x, top, y, bottom, mock(Paint.class));
 
         verify(canvas, times(0)).drawRect(eq(x), eq((float) top), eq(x + spanWidth),
                 eq((float) bottom), any(Paint.class));
 
         // verify the case where indicators are enabled
-        EmojiCompat.reset(TestConfigBuilder.config().setEmojiSpanIndicatorEnabled(true));
+        EmojiCompat.reset(NoFontTestEmojiConfig.emptyConfig().setEmojiSpanIndicatorEnabled(true));
         reset(canvas);
         span.draw(canvas, "a", 0 /*start*/, 1 /*end*/, x, top, y, bottom, mock(Paint.class));
 
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
index 3039f17..0a51020 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
@@ -24,7 +24,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.text.emoji.flatbuffer.MetadataList;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -39,7 +38,7 @@
 
     @Before
     public void clearResourceIndex() {
-        mMetadataRepo = MetadataRepo.create(mock(Typeface.class), new MetadataList());
+        mMetadataRepo = MetadataRepo.create(mock(Typeface.class));
     }
 
     @Test(expected = NullPointerException.class)
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/NoFontTestEmojiConfig.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/NoFontTestEmojiConfig.java
new file mode 100644
index 0000000..812a223
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/NoFontTestEmojiConfig.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.text;
+
+import static org.mockito.Mockito.mock;
+
+import android.graphics.Typeface;
+
+import androidx.annotation.NonNull;
+
+public class NoFontTestEmojiConfig extends EmojiCompat.Config {
+
+    static EmojiCompat.Config emptyConfig() {
+        return new NoFontTestEmojiConfig(new EmptyEmojiDataLoader());
+    }
+
+    static EmojiCompat.Config neverLoadsConfig() {
+        return new NoFontTestEmojiConfig(new NeverCompletesMetadataRepoLoader());
+    }
+
+    static EmojiCompat.Config fromLoader(EmojiCompat.MetadataRepoLoader loader) {
+        return new NoFontTestEmojiConfig(loader);
+    }
+
+    private NoFontTestEmojiConfig(EmojiCompat.MetadataRepoLoader loader) {
+        super(loader);
+    }
+
+    private static class EmptyEmojiDataLoader implements EmojiCompat.MetadataRepoLoader {
+        @Override
+        public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class)));
+        }
+    }
+
+    private static class NeverCompletesMetadataRepoLoader
+            implements EmojiCompat.MetadataRepoLoader {
+        @Override
+        public void load(@NonNull final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            // do nothing, this will be called on the test thread and is a no-op
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java
index 6cc85cd..1a15f44 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java
@@ -19,7 +19,6 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,19 +28,9 @@
 @SdkSuppress(minSdkVersion = 19)
 public class UninitializedStateTest {
 
-    private TestConfigBuilder.WaitingDataLoader mWaitingDataLoader;
-
     @Before
     public void setup() {
-        mWaitingDataLoader = new TestConfigBuilder.WaitingDataLoader(true);
-        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(mWaitingDataLoader);
-        EmojiCompat.reset(config);
-    }
-
-    @After
-    public void after() {
-        mWaitingDataLoader.getLoaderLatch().countDown();
-        mWaitingDataLoader.getTestLatch().countDown();
+        EmojiCompat.reset(NoFontTestEmojiConfig.neverLoadsConfig());
     }
 
     @Test(expected = IllegalStateException.class)
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
index f13c2a9..ac35a2d 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
@@ -509,20 +509,20 @@
     }
 
     /**
-     * @return whether a background should be drawn for the emoji.
+     * @return whether a background should be drawn for the emoji for debugging
      * @hide
      */
-    @RestrictTo(LIBRARY)
-    boolean isEmojiSpanIndicatorEnabled() {
+    @RestrictTo(LIBRARY_GROUP)
+    public boolean isEmojiSpanIndicatorEnabled() {
         return mEmojiSpanIndicatorEnabled;
     }
 
     /**
-     * @return whether a background should be drawn for the emoji.
+     * @return color of background drawn if {@link EmojiCompat#isEmojiSpanIndicatorEnabled} is true
      * @hide
      */
-    @RestrictTo(LIBRARY)
-    @ColorInt int getEmojiSpanIndicatorColor() {
+    @RestrictTo(LIBRARY_GROUP)
+    public @ColorInt int getEmojiSpanIndicatorColor() {
         return mEmojiSpanIndicatorColor;
     }
 
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
index 5760f20..28f39ef 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
@@ -16,6 +16,7 @@
 package androidx.emoji2.text;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 import static androidx.annotation.RestrictTo.Scope.TESTS;
 
 import android.annotation.SuppressLint;
@@ -102,8 +103,9 @@
     /**
      * @hide
      */
-    @RestrictTo(LIBRARY)
-    final EmojiMetadata getMetadata() {
+    @NonNull
+    @RestrictTo(LIBRARY_GROUP)
+    public final EmojiMetadata getMetadata() {
         return mMetadata;
     }
 
@@ -122,8 +124,8 @@
      *
      * @hide
      */
-    @RestrictTo(LIBRARY)
-    final int getHeight() {
+    @RestrictTo(TESTS)
+    public final int getHeight() {
         return mHeight;
     }
 
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
index 3549952..27afc5d 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
@@ -79,12 +79,15 @@
     }
 
     /**
-     * Construct MetadataRepo from a preloaded MetadatList.
+     * Construct MetadataRepo with empty metadata.
+     *
+     * This should only be used from tests.
+     * @hide
      */
     @NonNull
-    static MetadataRepo create(@NonNull final Typeface typeface,
-            @NonNull final MetadataList metadataList) {
-        return new MetadataRepo(typeface, metadataList);
+    @RestrictTo(RestrictTo.Scope.TESTS)
+    public static MetadataRepo create(@NonNull final Typeface typeface) {
+        return new MetadataRepo(typeface, new MetadataList());
     }
 
     /**
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index b850c56..8939d1c 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -298,6 +298,7 @@
     method public void registerFragmentLifecycleCallbacks(androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks, boolean);
     method public void removeFragmentOnAttachListener(androidx.fragment.app.FragmentOnAttachListener);
     method public void removeOnBackStackChangedListener(androidx.fragment.app.FragmentManager.OnBackStackChangedListener);
+    method public void restoreBackStack(String);
     method public void saveBackStack(String);
     method public androidx.fragment.app.Fragment.SavedState? saveFragmentInstanceState(androidx.fragment.app.Fragment);
     method public void setFragmentFactory(androidx.fragment.app.FragmentFactory);
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index 292c35f..1fb52f0 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -300,6 +300,7 @@
     method public void registerFragmentLifecycleCallbacks(androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks, boolean);
     method public void removeFragmentOnAttachListener(androidx.fragment.app.FragmentOnAttachListener);
     method public void removeOnBackStackChangedListener(androidx.fragment.app.FragmentManager.OnBackStackChangedListener);
+    method public void restoreBackStack(String);
     method public void saveBackStack(String);
     method public androidx.fragment.app.Fragment.SavedState? saveFragmentInstanceState(androidx.fragment.app.Fragment);
     method public void setFragmentFactory(androidx.fragment.app.FragmentFactory);
@@ -459,8 +460,14 @@
 
 package androidx.fragment.app.strictmode {
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentReuseViolation extends androidx.fragment.app.strictmode.Violation {
+    ctor public FragmentReuseViolation();
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentStrictMode {
     method public static androidx.fragment.app.strictmode.FragmentStrictMode.Policy getDefaultPolicy();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onFragmentReuse(androidx.fragment.app.Fragment);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onFragmentTagUsage(androidx.fragment.app.Fragment);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onRetainInstanceUsage(androidx.fragment.app.Fragment);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onSetUserVisibleHint(androidx.fragment.app.Fragment);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onTargetFragmentUsage(androidx.fragment.app.Fragment);
@@ -478,6 +485,8 @@
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static final class FragmentStrictMode.Policy.Builder {
     ctor public FragmentStrictMode.Policy.Builder();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy build();
+    method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectFragmentReuse();
+    method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectFragmentTagUsage();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectRetainInstanceUsage();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
@@ -486,6 +495,10 @@
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentTagUsageViolation extends androidx.fragment.app.strictmode.Violation {
+    ctor public FragmentTagUsageViolation();
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class RetainInstanceUsageViolation extends androidx.fragment.app.strictmode.Violation {
     ctor public RetainInstanceUsageViolation();
   }
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 86e9117..6abd717 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -305,6 +305,7 @@
     method public void registerFragmentLifecycleCallbacks(androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks, boolean);
     method public void removeFragmentOnAttachListener(androidx.fragment.app.FragmentOnAttachListener);
     method public void removeOnBackStackChangedListener(androidx.fragment.app.FragmentManager.OnBackStackChangedListener);
+    method public void restoreBackStack(String);
     method public void saveBackStack(String);
     method public androidx.fragment.app.Fragment.SavedState? saveFragmentInstanceState(androidx.fragment.app.Fragment);
     method public void setFragmentFactory(androidx.fragment.app.FragmentFactory);
@@ -485,8 +486,14 @@
 
 package androidx.fragment.app.strictmode {
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentReuseViolation extends androidx.fragment.app.strictmode.Violation {
+    ctor public FragmentReuseViolation();
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentStrictMode {
     method public static androidx.fragment.app.strictmode.FragmentStrictMode.Policy getDefaultPolicy();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onFragmentReuse(androidx.fragment.app.Fragment);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onFragmentTagUsage(androidx.fragment.app.Fragment);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onRetainInstanceUsage(androidx.fragment.app.Fragment);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onSetUserVisibleHint(androidx.fragment.app.Fragment);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onTargetFragmentUsage(androidx.fragment.app.Fragment);
@@ -504,6 +511,8 @@
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static final class FragmentStrictMode.Policy.Builder {
     ctor public FragmentStrictMode.Policy.Builder();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy build();
+    method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectFragmentReuse();
+    method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectFragmentTagUsage();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectRetainInstanceUsage();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
@@ -512,6 +521,10 @@
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentTagUsageViolation extends androidx.fragment.app.strictmode.Violation {
+    ctor public FragmentTagUsageViolation();
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class RetainInstanceUsageViolation extends androidx.fragment.app.strictmode.Violation {
     ctor public RetainInstanceUsageViolation();
   }
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 05dc225..115e903 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -29,6 +29,10 @@
     buildTypes.all {
         consumerProguardFiles "proguard-rules.pro"
     }
+
+    defaultConfig {
+        multiDexEnabled true
+    }
 }
 
 dependencies {
@@ -57,6 +61,7 @@
     androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(MULTIDEX)
     androidTestImplementation(project(":internal-testutils-runtime"), {
         exclude group: "androidx.fragment", module: "fragment"
     })
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
index f082f79..b66133b 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
@@ -48,8 +48,9 @@
             setMaxLifecycle(fragment, Lifecycle.State.STARTED)
         }
 
+        fragmentManager.fragmentStore.setSavedState(fragment.mWho, FragmentState(fragment))
         val backStackState = BackStackState(
-            listOf(FragmentState(fragment)),
+            listOf(fragment.mWho),
             listOf(BackStackRecordState(backStackRecord))
         )
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
index 1a115e0..1fddf91 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
@@ -54,7 +54,8 @@
     @Test
     fun constructorFragmentFactory() {
         val fragment = StrictFragment()
-        val fragmentState = FragmentStateManager(dispatcher, fragmentStore, fragment).saveState()
+        FragmentStateManager(dispatcher, fragmentStore, fragment).saveState()
+        val fragmentState = fragmentStore.getSavedState(fragment.mWho)!!
 
         val fragmentStateManager = FragmentStateManager(
             dispatcher, fragmentStore,
@@ -71,7 +72,8 @@
     @Test
     fun constructorRetainedFragment() {
         val fragment = StrictFragment()
-        val fragmentState = FragmentStateManager(dispatcher, fragmentStore, fragment).saveState()
+        FragmentStateManager(dispatcher, fragmentStore, fragment).saveState()
+        val fragmentState = fragmentStore.getSavedState(fragment.mWho)!!
         assertThat(fragment.mSavedFragmentState)
             .isNull()
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
index 4c2457b..980468f 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
@@ -159,7 +159,7 @@
         val savedActiveFragments = fragmentStore.saveActiveFragments()
         assertThat(savedActiveFragments)
             .hasSize(1)
-        assertThat(savedActiveFragments[0].mWho)
+        assertThat(savedActiveFragments[0])
             .isEqualTo(emptyFragment.mWho)
     }
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
index d97a019..da3653d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.fragment.app
 
+import android.os.Bundle
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
 import androidx.test.core.app.ActivityScenario
@@ -23,6 +24,7 @@
 import androidx.test.filters.MediumTest
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -55,8 +57,14 @@
             fm.saveBackStack("replacement")
             executePendingTransactions()
 
+            assertWithMessage("Saved Fragments should have their state saved")
+                .that(fragmentReplacement.calledOnSaveInstanceState)
+                .isTrue()
+
             // Saved Fragments should be destroyed
-            assertThat(fragmentReplacement.calledOnDestroy).isTrue()
+            assertWithMessage("Saved Fragments should be destroyed")
+                .that(fragmentReplacement.calledOnDestroy)
+                .isTrue()
         }
     }
 
@@ -220,4 +228,129 @@
             }
         }
     }
+
+    @Test
+    fun restoreBackStack() {
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fm = withActivity {
+                supportFragmentManager
+            }
+            val fragmentBase = StrictFragment()
+            val fragmentReplacement = StateSaveFragment("saved", "unsaved")
+
+            fm.beginTransaction()
+                .add(R.id.content, fragmentBase)
+                .commit()
+            executePendingTransactions()
+
+            fm.beginTransaction()
+                .setReorderingAllowed(true)
+                .replace(R.id.content, fragmentReplacement)
+                .addToBackStack("replacement")
+                .commit()
+            executePendingTransactions()
+
+            fm.saveBackStack("replacement")
+            executePendingTransactions()
+
+            assertWithMessage("Saved Fragments should have their state saved")
+                .that(fragmentReplacement.calledOnSaveInstanceState)
+                .isTrue()
+
+            // Saved Fragments should be destroyed
+            assertWithMessage("Saved Fragments should be destroyed")
+                .that(fragmentReplacement.calledOnDestroy)
+                .isTrue()
+
+            fm.restoreBackStack("replacement")
+            executePendingTransactions()
+
+            val newFragmentReplacement = fm.findFragmentById(R.id.content)
+
+            assertThat(newFragmentReplacement).isInstanceOf(StateSaveFragment::class.java)
+
+            val stateSavedReplacement = newFragmentReplacement as StateSaveFragment
+
+            // Assert that restored fragment has its saved state restored
+            assertThat(stateSavedReplacement.savedState).isEqualTo("saved")
+            assertThat(stateSavedReplacement.unsavedState).isNull()
+        }
+    }
+
+    @Test
+    fun restoreBackStackAfterRecreate() {
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            var fm = withActivity {
+                supportFragmentManager
+            }
+            val fragmentBase = StrictFragment()
+            val fragmentReplacement = StateSaveFragment("saved", "unsaved")
+
+            fm.beginTransaction()
+                .add(R.id.content, fragmentBase)
+                .commit()
+            executePendingTransactions()
+
+            fm.beginTransaction()
+                .setReorderingAllowed(true)
+                .replace(R.id.content, fragmentReplacement)
+                .addToBackStack("replacement")
+                .commit()
+            executePendingTransactions()
+
+            fm.saveBackStack("replacement")
+            executePendingTransactions()
+
+            assertWithMessage("Saved Fragments should have their state saved")
+                .that(fragmentReplacement.calledOnSaveInstanceState)
+                .isTrue()
+
+            // Saved Fragments should be destroyed
+            assertWithMessage("Saved Fragments should be destroyed")
+                .that(fragmentReplacement.calledOnDestroy)
+                .isTrue()
+
+            // Now recreate the whole activity while the state of the back stack is saved
+            recreate()
+
+            fm = withActivity {
+                supportFragmentManager
+            }
+
+            fm.restoreBackStack("replacement")
+            executePendingTransactions()
+
+            val newFragmentReplacement = fm.findFragmentById(R.id.content)
+
+            assertThat(newFragmentReplacement).isInstanceOf(StateSaveFragment::class.java)
+
+            val stateSavedReplacement = newFragmentReplacement as StateSaveFragment
+
+            // Assert that restored fragment has its saved state restored
+            assertThat(stateSavedReplacement.savedState).isEqualTo("saved")
+            assertThat(stateSavedReplacement.unsavedState).isNull()
+        }
+    }
+
+    public class StateSaveFragment(
+        public var savedState: String? = null,
+        public val unsavedState: String? = null
+    ) : StrictFragment() {
+
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            if (savedInstanceState != null) {
+                savedState = savedInstanceState.getString(STATE_KEY)
+            }
+        }
+
+        override fun onSaveInstanceState(outState: Bundle) {
+            super.onSaveInstanceState(outState)
+            outState.putString(STATE_KEY, savedState)
+        }
+
+        private companion object {
+            private const val STATE_KEY = "state"
+        }
+    }
 }
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
index f2f100f..669e9f8d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
@@ -20,6 +20,7 @@
 import androidx.fragment.app.StrictFragment
 import androidx.fragment.app.executePendingTransactions
 import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -129,6 +130,87 @@
     }
 
     @Test
+    public fun detectFragmentReuse() {
+        var violation: Violation? = null
+        val policy = FragmentStrictMode.Policy.Builder()
+            .detectFragmentReuse()
+            .penaltyListener { violation = it }
+            .build()
+        FragmentStrictMode.setDefaultPolicy(policy)
+
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fragmentManager = withActivity { supportFragmentManager }
+            val fragment = StrictFragment()
+
+            fragmentManager.beginTransaction()
+                .add(fragment, null)
+                .commit()
+            executePendingTransactions()
+
+            fragmentManager.beginTransaction()
+                .remove(fragment)
+                .commit()
+            executePendingTransactions()
+
+            fragmentManager.beginTransaction()
+                .add(fragment, null)
+                .commit()
+            executePendingTransactions()
+
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+            assertThat(violation).isInstanceOf(FragmentReuseViolation::class.java)
+        }
+    }
+
+    @Test
+    public fun detectFragmentReuseInFlightTransaction() {
+        var violation: Violation? = null
+        val policy = FragmentStrictMode.Policy.Builder()
+            .detectFragmentReuse()
+            .penaltyListener { violation = it }
+            .build()
+        FragmentStrictMode.setDefaultPolicy(policy)
+
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fragmentManager = withActivity { supportFragmentManager }
+            val fragment = StrictFragment()
+
+            fragmentManager.beginTransaction()
+                .add(fragment, null)
+                .commit()
+            executePendingTransactions()
+
+            fragmentManager.beginTransaction()
+                .remove(fragment)
+                .commit()
+            // Don't execute transaction here, keep it in-flight
+
+            fragmentManager.beginTransaction()
+                .add(fragment, null)
+                .commit()
+            executePendingTransactions()
+
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+            assertThat(violation).isInstanceOf(FragmentReuseViolation::class.java)
+        }
+    }
+
+    @Test
+    public fun detectFragmentTagUsage() {
+        var violation: Violation? = null
+        val policy = FragmentStrictMode.Policy.Builder()
+            .detectFragmentTagUsage()
+            .penaltyListener { violation = it }
+            .build()
+        FragmentStrictMode.setDefaultPolicy(policy)
+
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            withActivity { setContentView(R.layout.activity_inflated_fragment) }
+            assertThat(violation).isInstanceOf(FragmentTagUsageViolation::class.java)
+        }
+    }
+
+    @Test
     public fun detectRetainInstanceUsage() {
         var violation: Violation? = null
         val policy = FragmentStrictMode.Policy.Builder()
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
index 448ee87..68d5867 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
@@ -36,6 +36,7 @@
 
     boolean mCommitted;
     int mIndex = -1;
+    boolean mBeingSaved = false;
 
     @Override
     public String toString() {
@@ -280,6 +281,15 @@
         }
     }
 
+    void runOnExecuteRunnables() {
+        if (mExecuteRunnables != null) {
+            for (int i = 0; i < mExecuteRunnables.size(); i++) {
+                mExecuteRunnables.get(i).run();
+            }
+            mExecuteRunnables = null;
+        }
+    }
+
     public void runOnCommitRunnables() {
         if (mCommitRunnables != null) {
             for (int i = 0; i < mCommitRunnables.size(); i++) {
@@ -404,6 +414,7 @@
             final Op op = mOps.get(opNum);
             final Fragment f = op.mFragment;
             if (f != null) {
+                f.mBeingSaved = mBeingSaved;
                 f.setPopDirection(false);
                 f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim);
                 f.setNextTransition(mTransition);
@@ -467,6 +478,7 @@
             final Op op = mOps.get(opNum);
             Fragment f = op.mFragment;
             if (f != null) {
+                f.mBeingSaved = mBeingSaved;
                 f.setPopDirection(true);
                 f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim);
                 f.setNextTransition(FragmentManager.reverseTransit(mTransition));
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackState.java b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackState.java
index 7184b5c..c102873 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackState.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackState.java
@@ -28,29 +28,33 @@
 
 @SuppressLint("BanParcelableUsage")
 class BackStackState implements Parcelable {
-    final ArrayList<FragmentState> mFragments;
-    final ArrayList<BackStackRecordState> mTransactions;
+    final List<String> mFragments;
+    final List<BackStackRecordState> mTransactions;
 
-    BackStackState(List<FragmentState> fragments,
+    BackStackState(List<String> fragments,
             List<BackStackRecordState> transactions) {
-        mFragments = new ArrayList<>(fragments);
-        mTransactions = new ArrayList<>(transactions);
+        mFragments = fragments;
+        mTransactions = transactions;
     }
 
     BackStackState(@NonNull Parcel in) {
-        mFragments = in.createTypedArrayList(FragmentState.CREATOR);
+        mFragments = in.createStringArrayList();
         mTransactions = in.createTypedArrayList(BackStackRecordState.CREATOR);
     }
 
     @NonNull
-    ArrayList<BackStackRecord> instantiate(@NonNull FragmentManager fm) {
+    List<BackStackRecord> instantiate(@NonNull FragmentManager fm) {
         // First instantiate the saved Fragments from state.
         // These will populate the transactions we instantiate.
         HashMap<String, Fragment> fragments = new HashMap<>(mFragments.size());
-        for (FragmentState fragmentState : mFragments) {
-            Fragment fragment = fragmentState.instantiate(fm.getFragmentFactory(),
-                    fm.getHost().getContext().getClassLoader());
-            fragments.put(fragment.mWho, fragment);
+        for (String fWho : mFragments) {
+            // Retrieve any saved state, clearing it out for future calls
+            FragmentState fragmentState = fm.getFragmentStore().setSavedState(fWho, null);
+            if (fragmentState != null) {
+                Fragment fragment = fragmentState.instantiate(fm.getFragmentFactory(),
+                        fm.getHost().getContext().getClassLoader());
+                fragments.put(fragment.mWho, fragment);
+            }
         }
 
         // Now instantiate all of the BackStackRecords
@@ -68,7 +72,7 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeTypedList(mFragments);
+        dest.writeStringList(mFragments);
         dest.writeTypedList(mTransactions);
     }
 
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 7ca7617..6a65314 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -167,6 +167,8 @@
     // If set this fragment is being removed from its activity.
     boolean mRemoving;
 
+    boolean mBeingSaved;
+
     // Set to true if this fragment was instantiated from a layout file.
     boolean mFromLayout;
 
@@ -283,6 +285,10 @@
     // track it separately.
     boolean mIsCreated;
 
+    // True if the fragment was already added to a FragmentManager, but has since been removed
+    // again.
+    boolean mRemoved;
+
     // Max Lifecycle state this Fragment can achieve.
     Lifecycle.State mMaxState = Lifecycle.State.RESUMED;
 
@@ -2184,6 +2190,7 @@
         mTag = null;
         mHidden = false;
         mDetached = false;
+        mRemoved = true;
     }
 
     /**
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
index d1702ac..7826175 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
@@ -27,6 +27,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.R;
+import androidx.fragment.app.strictmode.FragmentStrictMode;
 
 class FragmentLayoutInflaterFactory implements LayoutInflater.Factory2 {
     private static final String TAG = FragmentManager.TAG;
@@ -130,6 +131,7 @@
                         + "re-attached via the <fragment> tag: id=0x" + Integer.toHexString(id));
             }
         }
+        FragmentStrictMode.onFragmentTagUsage(fragment);
 
         // Explicitly set the container for the fragment as we already know
         // the parent that the fragment will be added to by the LayoutInflater
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 6484fae..05cf431 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -680,6 +680,22 @@
     }
 
     /**
+     * Restores the back stack previously saved via {@link #saveBackStack(String)}. This
+     * will result in all of the transactions that made up that back stack to be re-executed,
+     * thus re-adding any fragments that were added through those transactions. All state of
+     * those fragments will be restored as part of this process.
+     * <p>
+     * This function is asynchronous -- it enqueues the
+     * request to restore, but the action will not be performed until the application
+     * returns to its event loop.
+     *
+     * @param name The name of the back stack previously saved by {@link #saveBackStack(String)}.
+     */
+    public void restoreBackStack(@NonNull String name) {
+        enqueueAction(new RestoreBackStackState(name), false);
+    }
+
+    /**
      * Save the back stack. While this functions similarly to
      * {@link #popBackStack(String, int)}, it <strong>does not</strong> throw away the
      * state of any fragments that were added through those transactions. Instead, the
@@ -1728,6 +1744,9 @@
     }
 
     FragmentStateManager addFragment(@NonNull Fragment fragment) {
+        if (fragment.mRemoved) {
+            FragmentStrictMode.onFragmentReuse(fragment);
+        }
         if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "add: " + fragment);
         FragmentStateManager fragmentStateManager = createOrGetFragmentStateManager(fragment);
         fragment.mFragmentManager = this;
@@ -2184,6 +2203,11 @@
         }
         executeOps(records, isRecordPop, startIndex, endIndex);
 
+        for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
+            final BackStackRecord record = records.get(recordNum);
+            record.runOnExecuteRunnables();
+        }
+
         if (USE_STATE_MANAGER) {
             // The last operation determines the overall direction, this ensures that operations
             // such as push, push, pop, push are correctly considered a push
@@ -2599,9 +2623,24 @@
         mBackStack.add(state);
     }
 
+    boolean restoreBackStackState(@NonNull ArrayList<BackStackRecord> records,
+            @NonNull ArrayList<Boolean> isRecordPop, @NonNull String name) {
+        BackStackState backStackState = mBackStackStates.get(name);
+        if (backStackState == null) {
+            return false;
+        }
+
+        List<BackStackRecord> backStackRecords = backStackState.instantiate(this);
+        for (BackStackRecord record : backStackRecords) {
+            records.add(record);
+            isRecordPop.add(false);
+        }
+        return true;
+    }
+
     boolean saveBackStackState(@NonNull ArrayList<BackStackRecord> records,
             @NonNull ArrayList<Boolean> isRecordPop, @NonNull String name) {
-        int index = findBackStackIndex(name, -1, true);
+        final int index = findBackStackIndex(name, -1, true);
         if (index < 0) {
             return false;
         }
@@ -2677,11 +2716,37 @@
         }
 
         // Now actually record each save
+        final ArrayList<String> fragments = new ArrayList<>();
+        for (Fragment f : allFragments) {
+            fragments.add(f.mWho);
+        }
+        final ArrayList<BackStackRecordState> backStackRecordStates =
+                new ArrayList<>(mBackStack.size() - index);
+        // Add placeholders for each BackStackRecordState
+        for (int i = index; i < mBackStack.size(); i++) {
+            backStackRecordStates.add(null);
+        }
+        final BackStackState backStackState = new BackStackState(
+                fragments, backStackRecordStates);
         for (int i = mBackStack.size() - 1; i >= index; i--) {
-            // TODO: Pre-process each BackStackRecord so that they actually save state
-            records.add(mBackStack.remove(i));
+            final BackStackRecord record = mBackStack.remove(i);
+            record.mBeingSaved = true;
+            // Get a callback when the BackStackRecord is actually finished
+            final int currentIndex = i;
+            record.addOnExecuteRunnable(new Runnable() {
+                @Override
+                public void run() {
+                    // First collapse the record to remove expanded ops and get it ready to save
+                    record.collapseOps();
+                    // Then save the state
+                    BackStackRecordState state = new BackStackRecordState(record);
+                    backStackRecordStates.set(currentIndex - index, state);
+                }
+            });
+            records.add(record);
             isRecordPop.add(true);
         }
+        mBackStackStates.put(name, backStackState);
         return true;
     }
 
@@ -2789,10 +2854,13 @@
         mStateSaved = true;
         mNonConfig.setIsStateSaved(true);
 
-        // First collect all active fragments.
-        ArrayList<FragmentState> active = mFragmentStore.saveActiveFragments();
+        // First save all active fragments.
+        ArrayList<String> active = mFragmentStore.saveActiveFragments();
 
-        if (active.isEmpty()) {
+        // And grab all FragmentState objects
+        ArrayList<FragmentState> savedState = mFragmentStore.getAllSavedState();
+
+        if (savedState.isEmpty()) {
             if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "saveAllState: no fragments!");
             return null;
         }
@@ -2817,6 +2885,7 @@
         }
 
         FragmentManagerState fms = new FragmentManagerState();
+        fms.mSavedState = savedState;
         fms.mActive = active;
         fms.mAdded = added;
         fms.mBackStack = backStack;
@@ -2846,12 +2915,17 @@
         // If there is no saved state at all, then there's nothing else to do
         if (state == null) return;
         FragmentManagerState fms = (FragmentManagerState) state;
-        if (fms.mActive == null) return;
+        if (fms.mSavedState == null) return;
+
+        // Restore the saved state of all fragments
+        mFragmentStore.restoreSaveState(fms.mSavedState);
 
         // Build the full list of active fragments, instantiating them from
         // their saved state.
         mFragmentStore.resetActiveFragments();
-        for (FragmentState fs : fms.mActive) {
+        for (String who : fms.mActive) {
+            // Retrieve any saved state, clearing it out for future calls
+            FragmentState fs = mFragmentStore.setSavedState(who, null);
             if (fs != null) {
                 FragmentStateManager fragmentStateManager;
                 Fragment retainedFragment = mNonConfig.findRetainedFragmentByWho(fs.mWho);
@@ -3712,6 +3786,21 @@
         }
     }
 
+    private class RestoreBackStackState implements OpGenerator {
+
+        private final String mName;
+
+        RestoreBackStackState(@NonNull String name) {
+            mName = name;
+        }
+
+        @Override
+        public boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
+                @NonNull ArrayList<Boolean> isRecordPop) {
+            return restoreBackStackState(records, isRecordPop, mName);
+        }
+    }
+
     private class SaveBackStackState implements OpGenerator {
 
         private final String mName;
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
index ed56a59..a90909a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
@@ -25,7 +25,8 @@
 
 @SuppressLint("BanParcelableUsage")
 final class FragmentManagerState implements Parcelable {
-    ArrayList<FragmentState> mActive;
+    ArrayList<FragmentState> mSavedState;
+    ArrayList<String> mActive;
     ArrayList<String> mAdded;
     BackStackRecordState[] mBackStack;
     int mBackStackIndex;
@@ -40,7 +41,8 @@
     }
 
     public FragmentManagerState(Parcel in) {
-        mActive = in.createTypedArrayList(FragmentState.CREATOR);
+        mSavedState = in.createTypedArrayList(FragmentState.CREATOR);
+        mActive = in.createStringArrayList();
         mAdded = in.createStringArrayList();
         mBackStack = in.createTypedArray(BackStackRecordState.CREATOR);
         mBackStackIndex = in.readInt();
@@ -59,7 +61,8 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeTypedList(mActive);
+        dest.writeTypedList(mSavedState);
+        dest.writeStringList(mActive);
         dest.writeStringList(mAdded);
         dest.writeTypedArray(mBackStack, flags);
         dest.writeInt(mBackStackIndex);
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
index b85f308..669c15a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
@@ -299,7 +299,9 @@
                             if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
                                 Log.d(TAG, "movefrom ACTIVITY_CREATED: " + mFragment);
                             }
-                            if (mFragment.mView != null) {
+                            if (mFragment.mBeingSaved) {
+                                saveState();
+                            } else if (mFragment.mView != null) {
                                 // Need to save the current view state if not done already
                                 // by saveInstanceState()
                                 if (mFragment.mSavedViewState == null) {
@@ -323,6 +325,10 @@
                             mFragment.mState = Fragment.CREATED;
                             break;
                         case Fragment.ATTACHED:
+                            if (mFragment.mBeingSaved
+                                    && mFragmentStore.getSavedState(mFragment.mWho) == null) {
+                                saveState();
+                            }
                             destroy();
                             break;
                         case Fragment.INITIALIZING:
@@ -617,8 +623,7 @@
         mDispatcher.dispatchOnFragmentStopped(mFragment, false);
     }
 
-    @NonNull
-    FragmentState saveState() {
+    void saveState() {
         FragmentState fs = new FragmentState(mFragment);
 
         if (mFragment.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {
@@ -641,7 +646,7 @@
         } else {
             fs.mSavedFragmentState = mFragment.mSavedFragmentState;
         }
-        return fs;
+        mFragmentStore.setSavedState(mFragment.mWho, fs);
     }
 
     @Nullable
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java
index 41bd97e..c7bfb2a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java
@@ -36,6 +36,7 @@
 
     private final ArrayList<Fragment> mAdded = new ArrayList<>();
     private final HashMap<String, FragmentStateManager> mActive = new HashMap<>();
+    private final HashMap<String, FragmentState> mSavedState = new HashMap<>();
 
     private FragmentManagerViewModel mNonConfig;
 
@@ -167,18 +168,47 @@
         values.removeAll(Collections.singleton(null));
     }
 
+    @Nullable
+    FragmentState getSavedState(@NonNull String who) {
+        return mSavedState.get(who);
+    }
+
+    /**
+     * Sets the saved state, returning the previously set FragmentState, if any.
+     */
+    @Nullable
+    FragmentState setSavedState(@NonNull String who, @Nullable FragmentState fragmentState) {
+        if (fragmentState != null) {
+            return mSavedState.put(who, fragmentState);
+        } else {
+            return mSavedState.remove(who);
+        }
+    }
+
+    void restoreSaveState(@NonNull ArrayList<FragmentState> savedState) {
+        mSavedState.clear();
+        for (FragmentState fs : savedState) {
+            mSavedState.put(fs.mWho, fs);
+        }
+    }
+
     @NonNull
-    ArrayList<FragmentState> saveActiveFragments() {
-        ArrayList<FragmentState> active = new ArrayList<>(mActive.size());
+    ArrayList<FragmentState> getAllSavedState() {
+        return new ArrayList<>(mSavedState.values());
+    }
+
+    @NonNull
+    ArrayList<String> saveActiveFragments() {
+        ArrayList<String> active = new ArrayList<>(mActive.size());
         for (FragmentStateManager fragmentStateManager : mActive.values()) {
             if (fragmentStateManager != null) {
                 Fragment f = fragmentStateManager.getFragment();
 
-                FragmentState fs = fragmentStateManager.saveState();
-                active.add(fs);
+                fragmentStateManager.saveState();
+                active.add(f.mWho);
 
                 if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
-                    Log.v(TAG, "Saved state of " + f + ": " + fs.mSavedFragmentState);
+                    Log.v(TAG, "Saved state of " + f + ": " + f.mSavedFragmentState);
                 }
             }
         }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
index d6a2636..7e79244 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
@@ -32,6 +32,7 @@
 import androidx.annotation.StringRes;
 import androidx.annotation.StyleRes;
 import androidx.core.view.ViewCompat;
+import androidx.fragment.app.strictmode.FragmentStrictMode;
 import androidx.lifecycle.Lifecycle;
 
 import java.lang.annotation.Retention;
@@ -121,6 +122,7 @@
     ArrayList<String> mSharedElementTargetNames;
     boolean mReorderingAllowed = false;
 
+    ArrayList<Runnable> mExecuteRunnables;
     ArrayList<Runnable> mCommitRunnables;
 
     /**
@@ -253,6 +255,9 @@
     }
 
     void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) {
+        if (fragment.mRemoved) {
+            FragmentStrictMode.onFragmentReuse(fragment);
+        }
         final Class<?> fragmentClass = fragment.getClass();
         final int modifiers = fragmentClass.getModifiers();
         if (fragmentClass.isAnonymousClass() || !Modifier.isPublic(modifiers)
@@ -821,6 +826,18 @@
     }
 
     /**
+     * Add a runnable that is run immediately after the transaction is executed.
+     * This differs from the commit runnables in that it happens before any
+     * fragments move to their expected state.
+     */
+    void addOnExecuteRunnable(@NonNull Runnable runnable) {
+        if (mExecuteRunnables == null) {
+            mExecuteRunnables = new ArrayList<>();
+        }
+        mExecuteRunnables.add(runnable);
+    }
+
+    /**
      * Add a Runnable to this transaction that will be run after this transaction has
      * been committed. If fragment transactions are {@link #setReorderingAllowed(boolean) optimized}
      * this may be after other subsequent fragment operations have also taken place, or operations
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentReuseViolation.java
similarity index 65%
copy from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
copy to fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentReuseViolation.java
index e022dc3..f209c61 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentReuseViolation.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package androidx.car.app.model.signin;
+package androidx.fragment.app.strictmode;
 
-import androidx.car.app.IOnDoneCallback;
+import androidx.annotation.RestrictTo;
 
-/** @hide */
-oneway interface IOnInputCompletedListener {
-  void onInputCompleted(String value, IOnDoneCallback callback) = 1;
+/** See #{@link FragmentStrictMode.Policy.Builder#detectFragmentReuse()}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // TODO: Make API public as soon as we have a few checks
+public final class FragmentReuseViolation extends Violation {
 }
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.java b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.java
index c4d5f18..1a554d7 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.java
@@ -51,6 +51,8 @@
         PENALTY_LOG,
         PENALTY_DEATH,
 
+        DETECT_FRAGMENT_REUSE,
+        DETECT_FRAGMENT_TAG_USAGE,
         DETECT_RETAIN_INSTANCE_USAGE,
         DETECT_SET_USER_VISIBLE_HINT,
         DETECT_TARGET_FRAGMENT_USAGE,
@@ -145,6 +147,25 @@
             }
 
             /**
+             * Detects cases, where a #{@link Fragment} instance is reused, after it was previously
+             * removed from a #{@link FragmentManager}.
+             */
+            @NonNull
+            @SuppressLint("BuilderSetStyle")
+            public Builder detectFragmentReuse() {
+                flags.add(Flag.DETECT_FRAGMENT_REUSE);
+                return this;
+            }
+
+            /** Detects usage of the &lt;fragment&gt; tag inside XML layouts. */
+            @NonNull
+            @SuppressLint("BuilderSetStyle")
+            public Builder detectFragmentTagUsage() {
+                flags.add(Flag.DETECT_FRAGMENT_TAG_USAGE);
+                return this;
+            }
+
+            /**
              * Detects calls to #{@link Fragment#setRetainInstance} and
              * #{@link Fragment#getRetainInstance()}.
              */
@@ -220,6 +241,22 @@
     }
 
     @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static void onFragmentReuse(@NonNull Fragment fragment) {
+        Policy policy = getNearestPolicy(fragment);
+        if (policy.flags.contains(Flag.DETECT_FRAGMENT_REUSE)) {
+            handlePolicyViolation(fragment, policy, new FragmentReuseViolation());
+        }
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static void onFragmentTagUsage(@NonNull Fragment fragment) {
+        Policy policy = getNearestPolicy(fragment);
+        if (policy.flags.contains(Flag.DETECT_FRAGMENT_TAG_USAGE)) {
+            handlePolicyViolation(fragment, policy, new FragmentTagUsageViolation());
+        }
+    }
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
     public static void onRetainInstanceUsage(@NonNull Fragment fragment) {
         Policy policy = getNearestPolicy(fragment);
         if (policy.flags.contains(Flag.DETECT_RETAIN_INSTANCE_USAGE)) {
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentTagUsageViolation.java
similarity index 65%
copy from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
copy to fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentTagUsageViolation.java
index e022dc3..0084e18 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentTagUsageViolation.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package androidx.car.app.model.signin;
+package androidx.fragment.app.strictmode;
 
-import androidx.car.app.IOnDoneCallback;
+import androidx.annotation.RestrictTo;
 
-/** @hide */
-oneway interface IOnInputCompletedListener {
-  void onInputCompleted(String value, IOnDoneCallback callback) = 1;
+/** See #{@link FragmentStrictMode.Policy.Builder#detectFragmentTagUsage()}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // TODO: Make API public as soon as we have a few checks
+public final class FragmentTagUsageViolation extends Violation {
 }
diff --git a/hilt/integration-tests/workerapp/src/main/AndroidManifest.xml b/hilt/integration-tests/workerapp/src/main/AndroidManifest.xml
index 50f1a690..8503ecd 100644
--- a/hilt/integration-tests/workerapp/src/main/AndroidManifest.xml
+++ b/hilt/integration-tests/workerapp/src/main/AndroidManifest.xml
@@ -36,7 +36,7 @@
             tools:node="merge">
             <!-- Remove initializer for WorkManager on-demand initialization -->
             <meta-data
-                android:name="androidx.work.impl.WorkManagerInitializer"
+                android:name="androidx.work.WorkManagerInitializer"
                 android:value="@string/androidx_startup"
                 tools:node="remove" />
         </provider>
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 63486ac..32e4434 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -13,7 +13,7 @@
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
     api("androidx.leanback:leanback:1.1.0-beta01")
-    api(project(":paging:paging-runtime"))
+    api("androidx.paging:paging-runtime:3.0.0-beta03")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/leanback/leanback-preference/build.gradle b/leanback/leanback-preference/build.gradle
index 5214770..3559967 100644
--- a/leanback/leanback-preference/build.gradle
+++ b/leanback/leanback-preference/build.gradle
@@ -13,7 +13,7 @@
     api("androidx.appcompat:appcompat:1.0.0")
     api("androidx.recyclerview:recyclerview:1.0.0")
     api("androidx.preference:preference:1.1.0")
-    api("androidx.leanback:leanback:1.1.0-beta01")
+    api(project(":leanback:leanback"))
 }
 
 android {
diff --git a/leanback/leanback/build.gradle b/leanback/leanback/build.gradle
index 25216be..e0f38fa 100644
--- a/leanback/leanback/build.gradle
+++ b/leanback/leanback/build.gradle
@@ -15,7 +15,7 @@
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.media:media:1.0.0")
     api("androidx.fragment:fragment:1.0.0")
-    api("androidx.recyclerview:recyclerview:1.2.0-beta01")
+    api("androidx.recyclerview:recyclerview:1.2.0-rc01")
     api("androidx.appcompat:appcompat:1.0.0")
 
     androidTestImplementation(KOTLIN_STDLIB)
diff --git a/lifecycle/OWNERS b/lifecycle/OWNERS
index 77b5892..0982d74 100644
--- a/lifecycle/OWNERS
+++ b/lifecycle/OWNERS
@@ -1,5 +1,6 @@
 [email protected]
 [email protected]
[email protected]
 
 per-file settings.gradle = [email protected], [email protected]
 
diff --git a/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt b/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
index 31c31a6..098de7c 100644
--- a/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
+++ b/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
@@ -98,7 +98,9 @@
                 // Given the field `val liveDataField = MutableLiveData<Boolean>()`
                 // expression: `MutableLiveData<Boolean>()`
                 // argument: `Boolean`
-                val expression = element.sourcePsi?.children?.get(0) as? KtCallExpression
+                val expression = element.sourcePsi
+                    ?.children
+                    ?.firstOrNull { it is KtCallExpression } as? KtCallExpression
                 val argument = expression?.typeArguments?.singleOrNull()
                 return argument?.typeReference
             }
diff --git a/lifecycle/lifecycle-livedata-core-ktx-lint/src/test/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetectorTest.kt b/lifecycle/lifecycle-livedata-core-ktx-lint/src/test/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetectorTest.kt
index bbb2490..095d9f6 100644
--- a/lifecycle/lifecycle-livedata-core-ktx-lint/src/test/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetectorTest.kt
+++ b/lifecycle/lifecycle-livedata-core-ktx-lint/src/test/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetectorTest.kt
@@ -582,6 +582,167 @@
     }
 
     @Test
+    fun modifiersFieldTest() {
+        check(
+            kotlin(
+                """
+                package com.example
+
+                import androidx.lifecycle.LiveData
+                import androidx.lifecycle.MutableLiveData
+
+                class MyClass1 {
+                    internal val firstLiveDataField = MutableLiveData<Boolean>()
+                    protected val secondLiveDataField = MutableLiveData<Boolean?>()
+
+                    fun foo() {
+                        firstLiveDataField.value = false
+                        firstLiveDataField.value = null
+                        secondLiveDataField.value = null
+                        secondLiveDataField.value = false
+                    }
+                }
+            """
+            ).indented()
+        ).expect(
+            """
+src/com/example/MyClass1.kt:12: Error: Cannot set non-nullable LiveData value to null [NullSafeMutableLiveData]
+        firstLiveDataField.value = null
+                                   ~~~~
+1 errors, 0 warnings
+        """
+        ).expectFixDiffs(
+            """
+Fix for src/com/example/MyClass1.kt line 12: Change `LiveData` type to nullable:
+@@ -7 +7
+-     internal val firstLiveDataField = MutableLiveData<Boolean>()
++     internal val firstLiveDataField = MutableLiveData<Boolean?>()
+        """
+        )
+    }
+
+    @Test
+    fun implementationClassTest() {
+        check(
+            kotlin(
+                """
+                package com.example
+
+                import androidx.lifecycle.LiveData
+                import androidx.lifecycle.MutableLiveData
+
+                interface MyClass2 {
+                    val firstLiveDataField : LiveData<Boolean>
+                    val secondLiveDataField : LiveData<Boolean?>
+                    val thirdLiveDataField : LiveData<Boolean?>
+                    val fourLiveDataField : LiveData<List<Boolean>?>
+                    val fiveLiveDataField : LiveData<List<Boolean>?>
+                }
+
+                class MyClass1 : MyClass2 {
+                    override val firstLiveDataField = MutableLiveData<Boolean>()
+                    override val secondLiveDataField = MutableLiveData<Boolean?>()
+                    override val thirdLiveDataField = MutableLiveData<Boolean?>(null)
+                    override val fourLiveDataField = MutableLiveData<List<Boolean>?>(null)
+                    override val fiveLiveDataField : MutableLiveData<List<Boolean>?> = MutableLiveData(null)
+
+                    fun foo() {
+                        firstLiveDataField.value = false
+                        firstLiveDataField.value = null
+                        secondLiveDataField.value = null
+                        secondLiveDataField.value = false
+                        thirdLiveDataField.value = null
+                        thirdLiveDataField.value = false
+                        fourLiveDataField.value = null
+                        fourLiveDataField.value = emptyList()
+                        fiveLiveDataField.value = null
+                        fiveLiveDataField.value = emptyList()
+                    }
+                }
+            """
+            ).indented()
+        ).expect(
+            """
+src/com/example/MyClass2.kt:23: Error: Cannot set non-nullable LiveData value to null [NullSafeMutableLiveData]
+        firstLiveDataField.value = null
+                                   ~~~~
+1 errors, 0 warnings
+        """
+        ).expectFixDiffs(
+            """
+Fix for src/com/example/MyClass2.kt line 23: Change `LiveData` type to nullable:
+@@ -15 +15
+-     override val firstLiveDataField = MutableLiveData<Boolean>()
++     override val firstLiveDataField = MutableLiveData<Boolean?>()
+        """
+        )
+    }
+
+    @Test
+    fun extendClassTest() {
+        check(
+            kotlin(
+                """
+                package com.example
+
+                import androidx.lifecycle.LiveData
+                import androidx.lifecycle.MutableLiveData
+
+                abstract class MyClass2 {
+                    val firstLiveDataField : LiveData<Boolean>
+                    val secondLiveDataField : LiveData<Boolean?>
+                    val thirdLiveDataField : LiveData<Boolean?>
+                    val fourLiveDataField : LiveData<List<Boolean>?>
+                    val fiveLiveDataField : LiveData<List<Boolean>>
+                }
+
+                class MyClass1 : MyClass2() {
+                    override val firstLiveDataField = MutableLiveData<Boolean>()
+                    override val secondLiveDataField = MutableLiveData<Boolean?>()
+                    override val thirdLiveDataField = MutableLiveData<Boolean?>(null)
+                    override val fourLiveDataField = MutableLiveData<List<Boolean>?>(null)
+                    override val fiveLiveDataField = MutableLiveData<List<Boolean>>()
+
+                    fun foo() {
+                        firstLiveDataField.value = false
+                        firstLiveDataField.value = null
+                        secondLiveDataField.value = null
+                        secondLiveDataField.value = false
+                        thirdLiveDataField.value = null
+                        thirdLiveDataField.value = false
+                        fourLiveDataField.value = null
+                        fourLiveDataField.value = emptyList()
+                        fiveLiveDataField.value = null
+                        fiveLiveDataField.value = emptyList()
+                    }
+                }
+            """
+            ).indented()
+        ).expect(
+            """
+src/com/example/MyClass2.kt:23: Error: Cannot set non-nullable LiveData value to null [NullSafeMutableLiveData]
+        firstLiveDataField.value = null
+                                   ~~~~
+src/com/example/MyClass2.kt:30: Error: Cannot set non-nullable LiveData value to null [NullSafeMutableLiveData]
+        fiveLiveDataField.value = null
+                                  ~~~~
+2 errors, 0 warnings
+        """
+        ).expectFixDiffs(
+            """
+Fix for src/com/example/MyClass2.kt line 23: Change `LiveData` type to nullable:
+@@ -15 +15
+-     override val firstLiveDataField = MutableLiveData<Boolean>()
++     override val firstLiveDataField = MutableLiveData<Boolean?>()
+Fix for src/com/example/MyClass2.kt line 30: Change `LiveData` type to nullable:
+@@ -19 +19
+-     override val fiveLiveDataField = MutableLiveData<List<Boolean>>()
++     override val fiveLiveDataField = MutableLiveData<List<Boolean>?>()
+        """
+        )
+    }
+
+    @Test
     fun objectLiveData() {
         check(
             kotlin(
diff --git a/lifecycle/lifecycle-process/api/current.txt b/lifecycle/lifecycle-process/api/current.txt
index 29e1211..429b2b2 100644
--- a/lifecycle/lifecycle-process/api/current.txt
+++ b/lifecycle/lifecycle-process/api/current.txt
@@ -1,6 +1,12 @@
 // Signature format: 4.0
 package androidx.lifecycle {
 
+  public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+    ctor public ProcessLifecycleInitializer();
+    method public androidx.lifecycle.LifecycleOwner create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
   public class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
     method public static androidx.lifecycle.LifecycleOwner get();
     method public androidx.lifecycle.Lifecycle getLifecycle();
diff --git a/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
index 29e1211..429b2b2 100644
--- a/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
@@ -1,6 +1,12 @@
 // Signature format: 4.0
 package androidx.lifecycle {
 
+  public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+    ctor public ProcessLifecycleInitializer();
+    method public androidx.lifecycle.LifecycleOwner create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
   public class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
     method public static androidx.lifecycle.LifecycleOwner get();
     method public androidx.lifecycle.Lifecycle getLifecycle();
diff --git a/lifecycle/lifecycle-process/api/restricted_current.txt b/lifecycle/lifecycle-process/api/restricted_current.txt
index 25bd0dd0..429b2b2 100644
--- a/lifecycle/lifecycle-process/api/restricted_current.txt
+++ b/lifecycle/lifecycle-process/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.lifecycle {
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+  public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
     ctor public ProcessLifecycleInitializer();
     method public androidx.lifecycle.LifecycleOwner create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
diff --git a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleInitializer.java b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleInitializer.java
index 9fad75d..03e32bf 100644
--- a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleInitializer.java
+++ b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleInitializer.java
@@ -19,19 +19,15 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
 import androidx.startup.Initializer;
 
 import java.util.Collections;
 import java.util.List;
 
 /**
- * Internal class to initialize Lifecycles.
- *
- * @hide
+ * Initializes {@link ProcessLifecycleOwner} using {@code androidx.startup}.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ProcessLifecycleInitializer implements Initializer<LifecycleOwner> {
+public final class ProcessLifecycleInitializer implements Initializer<LifecycleOwner> {
 
     @NonNull
     @Override
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index f546dff..1d0082c 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -51,7 +51,7 @@
 androidx {
     name = "Lifecycle ViewModel Compose"
     publish = Publish.SNAPSHOT_AND_RELEASE
-    mavenVersion = LibraryVersions.LIFECYCLE_COMPOSE
+    mavenVersion = LibraryVersions.LIFECYCLE_VIEWMODEL_COMPOSE
     mavenGroup = LibraryGroups.LIFECYCLE
     inceptionYear = "2021"
     description = "Compose integration with Lifecycle ViewModel"
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
index c581d23..e4974d0 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
@@ -143,7 +143,7 @@
      * If the given {@code newValue} is {@link Closeable},
      * it will be closed once {@link #clear()}.
      * <p>
-     * If a value was already set for the given key, this calls do nothing and
+     * If a value was already set for the given key, this call does nothing and
      * returns currently associated value, the given {@code newValue} would be ignored
      * <p>
      * If the ViewModel was already cleared then close() would be called on the returned object if
diff --git a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaControllerTest.java b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaControllerTest.java
index 77b03f5e..afb8ca9 100644
--- a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaControllerTest.java
+++ b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaControllerTest.java
@@ -61,6 +61,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.Suppress;
 import androidx.testutils.PollingCheck;
 
 import org.junit.After;
@@ -907,6 +908,7 @@
     }
 
     @Test
+    @Suppress // b/183700008
     public void setVolumeWithLocalVolume() throws Exception {
         if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
             // This test is not eligible for this device.
@@ -951,6 +953,7 @@
     }
 
     @Test
+    @Suppress // b/183700008
     public void adjustVolumeWithLocalVolume() throws Exception {
         if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
             // This test is not eligible for this device.
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
index 5aac8d1..088c60e 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
@@ -1049,4 +1049,66 @@
             .that(matchArgs?.getInt("id"))
             .isEqualTo(id)
     }
+
+    @Test
+    fun deepLinkCaseInsensitiveDomainWithPath() {
+        val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users/{id}/posts"
+        val deepLink = NavDeepLink(deepLinkArgument)
+
+        val id = 2
+        val matchArgs = deepLink.getMatchingArguments(
+            Uri.parse("${DEEP_LINK_EXACT_HTTPS.toUpperCase()}/users/$id/posts"),
+            mapOf("id" to intArgument())
+        )
+        assertWithMessage("Args should not be null")
+            .that(matchArgs)
+            .isNotNull()
+        assertWithMessage("Args should contain the id")
+            .that(matchArgs?.getInt("id"))
+            .isEqualTo(id)
+    }
+
+    @Test
+    fun deepLinkCaseInsensitivePath() {
+        val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users/{id}/posts"
+        val deepLink = NavDeepLink(deepLinkArgument)
+
+        val id = 2
+        val matchArgs = deepLink.getMatchingArguments(
+            Uri.parse(
+                deepLinkArgument
+                    .replace("{id}", id.toString())
+                    .replace("users", "Users")
+            ),
+            mapOf("id" to intArgument())
+        )
+        assertWithMessage("Args should not be null")
+            .that(matchArgs)
+            .isNotNull()
+        assertWithMessage("Args should contain the id")
+            .that(matchArgs?.getInt("id"))
+            .isEqualTo(id)
+    }
+
+    @Test
+    fun deepLinkCaseSensitiveQueryParams() {
+        val deepLinkString = "$DEEP_LINK_EXACT_HTTP/?myParam={param}"
+        val deepLink = NavDeepLink(deepLinkString)
+
+        val param = 2
+        val deepLinkUpper = deepLinkString
+            .replace("myParam", "MYPARAM")
+            .replace("{param}", param.toString())
+        val matchArgs = deepLink.getMatchingArguments(
+            Uri.parse(deepLinkUpper),
+            mapOf("param" to intArgument())
+        )
+
+        assertWithMessage("Args should be not be null")
+            .that(matchArgs)
+            .isNotNull()
+        assertWithMessage("Args bundle should be empty")
+            .that(matchArgs?.isEmpty)
+            .isTrue()
+    }
 }
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavAction.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavAction.kt
index 9bf2dc9..06c2482 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavAction.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavAction.kt
@@ -63,7 +63,7 @@
     /**
      * Sets the argument bundle to be used by default when navigating to this action.
      *
-     * @param defaultArgs argument bundle that should be used by default
+     * @param defaultArguments argument bundle that should be used by default
      */
     public var defaultArguments: Bundle? = null
 )
\ No newline at end of file
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
index 302b2f7..42d18c1 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
@@ -422,7 +422,7 @@
             // specifically escape any .* instances to ensure
             // they are still treated as wildcards in our final regex
             val finalRegex = uriRegex.toString().replace(".*", "\\E.*\\Q")
-            pattern = Pattern.compile(finalRegex)
+            pattern = Pattern.compile(finalRegex, Pattern.CASE_INSENSITIVE)
         }
         if (mimeType != null) {
             val mimeTypePattern = Pattern.compile("^[\\s\\S]+/[\\s\\S]+$")
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index 426e287..3d2eca2 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -195,9 +195,9 @@
      *
      * @param deepLink to the destination reachable from the current NavGraph
      * @return True if the deepLink exists for the destination.
-     * @see .addDeepLink
+     * @see NavDestination.addDeepLink
      * @see NavController.navigate
-     * @see .hasDeepLink
+     * @see NavDestination.hasDeepLink
      */
     public open fun hasDeepLink(deepLink: Uri): Boolean {
         return hasDeepLink(NavDeepLinkRequest(deepLink, null, null))
@@ -214,7 +214,7 @@
      *
      * @param deepLinkRequest to the destination reachable from the current NavGraph
      * @return True if the deepLink exists for the destination.
-     * @see .addDeepLink
+     * @see NavDestination.addDeepLink
      * @see NavController.navigate
      */
     public open fun hasDeepLink(deepLinkRequest: NavDeepLinkRequest): Boolean {
@@ -249,7 +249,7 @@
      * @param uriPattern The uri pattern to add as a deep link
      * @see NavController.handleDeepLink
      * @see NavController.navigate
-     * @see .addDeepLink
+     * @see NavDestination.addDeepLink
      */
     public fun addDeepLink(uriPattern: String) {
         addDeepLink(NavDeepLink.Builder().setUriPattern(uriPattern).build())
@@ -367,7 +367,7 @@
 
     /**
      * @return Whether this NavDestination supports outgoing actions
-     * @see .putAction
+     * @see NavDestination.putAction
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public open fun supportsActions(): Boolean {
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavOptions.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavOptions.kt
index d21e3be..091afc1 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavOptions.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavOptions.kt
@@ -78,7 +78,7 @@
      * Whether the destination set in [.getPopUpTo] should be popped from the back stack.
      * @see Builder.setPopUpTo
      *
-     * @see .getPopUpTo
+     * @see NavOptions.getPopUpTo
      */
     public fun isPopUpToInclusive(): Boolean {
         return popUpToInclusive
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorProvider.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorProvider.kt
index 40858ec..3051250 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorProvider.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorProvider.kt
@@ -41,7 +41,7 @@
      * [Navigator.Name annotation][Navigator.Name]
      * @throws IllegalStateException if the Navigator has not been added
      *
-     * @see .addNavigator
+     * @see NavigatorProvider.addNavigator
      */
     public fun <T : Navigator<*>> getNavigator(navigatorClass: Class<T>): T {
         val name = getNameForNavigator(navigatorClass)
@@ -56,7 +56,7 @@
      *
      * @throws IllegalStateException if the Navigator has not been added
      *
-     * @see .addNavigator
+     * @see NavigatorProvider.addNavigator
      */
     @Suppress("UNCHECKED_CAST")
     @CallSuper
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index d384453..f7e8faf 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -36,8 +36,8 @@
     api(projectOrArtifact(":compose:runtime:runtime"))
     api(projectOrArtifact(":compose:runtime:runtime-saveable"))
     api(projectOrArtifact(":compose:ui:ui"))
-    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose"))
-    api(prebuiltOrSnapshot("androidx.navigation:navigation-runtime-ktx:2.3.4"))
+    api("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha03")
+    api("androidx.navigation:navigation-runtime-ktx:2.3.4")
 
     androidTestImplementation(projectOrArtifact(":compose:material:material"))
     androidTestImplementation("androidx.navigation:navigation-testing:2.3.1")
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index 8412469..7c9a26b 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -25,6 +25,8 @@
 import android.os.Parcelable
 import android.view.View
 import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.addCallback
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.ViewModelStore
 import androidx.lifecycle.testing.TestLifecycleOwner
@@ -323,6 +325,10 @@
         navController.navigate(deepLink)
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
         assertThat(navigator.backStack.size).isEqualTo(2)
+        val intent = navigator.current.second?.getParcelable<Intent>(
+            NavController.KEY_DEEP_LINK_INTENT
+        )
+        assertThat(intent?.data).isEqualTo(deepLink)
     }
 
     @UiThreadTest
@@ -348,11 +354,16 @@
         val navController = createNavController()
         navController.setGraph(R.navigation.nav_simple)
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
-        val deepLink = NavDeepLinkRequest(null, "test.action", null)
+        val action = "test.action"
+        val deepLink = NavDeepLinkRequest(null, action, null)
 
         navController.navigate(deepLink)
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
         assertThat(navigator.backStack.size).isEqualTo(2)
+        val intent = navigator.current.second?.getParcelable<Intent>(
+            NavController.KEY_DEEP_LINK_INTENT
+        )
+        assertThat(intent?.action).isEqualTo(action)
     }
 
     @UiThreadTest
@@ -387,11 +398,16 @@
         val navController = createNavController()
         navController.setGraph(R.navigation.nav_deeplink)
         val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
-        val deepLink = NavDeepLinkRequest(null, null, "type/test")
+        val mimeType = "type/test"
+        val deepLink = NavDeepLinkRequest(null, null, mimeType)
 
         navController.navigate(deepLink)
         assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.forth_test)
         assertThat(navigator.backStack.size).isEqualTo(2)
+        val intent = navigator.current.second?.getParcelable<Intent>(
+            NavController.KEY_DEEP_LINK_INTENT
+        )
+        assertThat(intent?.type).isEqualTo(mimeType)
     }
 
     @UiThreadTest
@@ -1580,6 +1596,35 @@
             .that(collectedDestinationIds).hasSize(1)
     }
 
+    @UiThreadTest
+    @Test
+    fun testSetOnBackPressedDispatcherOnNavBackStackEntry() {
+        var backPressedIntercepted = false
+        val navController = createNavController()
+        val lifecycleOwner = TestLifecycleOwner()
+        val dispatcher = OnBackPressedDispatcher()
+
+        navController.setLifecycleOwner(lifecycleOwner)
+        navController.setOnBackPressedDispatcher(dispatcher)
+
+        navController.setGraph(R.navigation.nav_simple)
+        navController.navigate(R.id.second_test)
+        assertEquals(R.id.start_test, navController.previousBackStackEntry?.destination?.id ?: 0)
+
+        dispatcher.addCallback(navController.currentBackStackEntry!!) {
+            backPressedIntercepted = true
+        }
+
+        // Move to STOPPED
+        lifecycleOwner.currentState = Lifecycle.State.CREATED
+        // Move back up to RESUMED
+        lifecycleOwner.currentState = Lifecycle.State.RESUMED
+
+        dispatcher.onBackPressed()
+
+        assertThat(backPressedIntercepted).isTrue()
+    }
+
     private fun createNavController(): NavController {
         val navController = NavController(ApplicationProvider.getApplicationContext())
         val navigator = TestNavigator()
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
index b8d2ea9..532a73a 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
@@ -235,7 +235,7 @@
          * @param dataPattern A URI pattern with segments in the form of `{argName}` that
          * will be replaced with URI encoded versions of the Strings in the
          * arguments Bundle.
-         * @see .setData
+         * @see Destination.setData
          *
          * @return this [Destination]
          */
@@ -371,7 +371,7 @@
          * present.
          *
          * @param data A static URI that should always be used.
-         * @see .setDataPattern
+         * @see Destination.setDataPattern
          * @return this [Destination]
          */
         public fun setData(data: Uri?): Destination {
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index c4d6a9e..1eabbd2 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -65,7 +65,7 @@
         /**
          * Gets the topmost navigation graph associated with this NavController.
          *
-         * @see .setGraph
+         * @see NavController.setGraph
          * @throws IllegalStateException if called before `setGraph()`.
          */
         get() {
@@ -80,8 +80,8 @@
          * The graph can be retrieved later via [.getGraph].
          *
          * @param graph graph to set
-         * @see .setGraph
-         * @see .getGraph
+         * @see NavController.setGraph
+         * @see NavController.getGraph
          */
         @CallSuper
         set(graph) {
@@ -500,9 +500,9 @@
      *
      * @param graphResId resource id of the navigation graph to inflate
      *
-     * @see .getNavInflater
-     * @see .setGraph
-     * @see .getGraph
+     * @see NavController.getNavInflater
+     * @see NavController.setGraph
+     * @see NavController.getGraph
      */
     @CallSuper
     public open fun setGraph(@NavigationRes graphResId: Int) {
@@ -518,9 +518,9 @@
      * @param graphResId resource id of the navigation graph to inflate
      * @param startDestinationArgs arguments to send to the start destination of the graph
      *
-     * @see .getNavInflater
-     * @see .setGraph
-     * @see .getGraph
+     * @see NavController.getNavInflater
+     * @see NavController.setGraph
+     * @see NavController.getGraph
      */
     @CallSuper
     public open fun setGraph(@NavigationRes graphResId: Int, startDestinationArgs: Bundle?) {
@@ -534,8 +534,8 @@
      * The graph can be retrieved later via [.getGraph].
      *
      * @param graph graph to set
-     * @see .setGraph
-     * @see .getGraph
+     * @see NavController.setGraph
+     * @see NavController.getGraph
      */
     @CallSuper
     public open fun setGraph(graph: NavGraph, startDestinationArgs: Bundle?) {
@@ -940,7 +940,7 @@
      * thrown.
      *
      * @param deepLink deepLink to the destination reachable from the current NavGraph
-     * @see .navigate
+     * @see NavController.navigate
      */
     public open fun navigate(deepLink: Uri) {
         navigate(NavDeepLinkRequest(deepLink, null, null))
@@ -955,7 +955,7 @@
      *
      * @param deepLink deepLink to the destination reachable from the current NavGraph
      * @param navOptions special options for this navigation operation
-     * @see .navigate
+     * @see NavController.navigate
      */
     public open fun navigate(deepLink: Uri, navOptions: NavOptions?) {
         navigate(NavDeepLinkRequest(deepLink, null, null), navOptions, null)
@@ -971,7 +971,7 @@
      * @param deepLink deepLink to the destination reachable from the current NavGraph
      * @param navOptions special options for this navigation operation
      * @param navigatorExtras extras to pass to the Navigator
-     * @see .navigate
+     * @see NavController.navigate
      */
     public open fun navigate(
         deepLink: Uri,
@@ -1033,8 +1033,13 @@
         val deepLinkMatch = _graph!!.matchDeepLink(request)
         if (deepLinkMatch != null) {
             val destination = deepLinkMatch.destination
-            val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs)
+            val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
             val node = deepLinkMatch.destination
+            val intent = Intent().apply {
+                setDataAndType(request.uri, request.mimeType)
+                action = request.action
+            }
+            args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
             navigate(node, args, navOptions, navigatorExtras)
         } else {
             throw IllegalArgumentException(
@@ -1275,6 +1280,13 @@
         onBackPressedCallback.remove()
         // Then add it to the new dispatcher
         dispatcher.addCallback(lifecycleOwner!!, onBackPressedCallback)
+
+        // Make sure that listener for updating the NavBackStackEntry lifecycles comes after
+        // the dispatcher
+        lifecycleOwner!!.lifecycle.apply {
+            removeObserver(lifecycleObserver)
+            addObserver(lifecycleObserver)
+        }
     }
 
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
index 10af220..2b65a53 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
@@ -52,7 +52,7 @@
  * default activity to launch, if available.
  *
  * @param context Context used to create deep links
- * @see .setComponentName
+ * @see NavDeepLinkBuilder.setComponentName
  */
 constructor(private val context: Context) {
     private class DeepLinkDestination constructor(
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHostController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHostController.kt
index 4edde8c..db850b7 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHostController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHostController.kt
@@ -69,7 +69,7 @@
      * [NavHost].
      * @throws IllegalStateException if you have not called
      * [.setLifecycleOwner] before calling this method.
-     * @see .setLifecycleOwner
+     * @see NavHostController.setLifecycleOwner
      */
     public final override fun setOnBackPressedDispatcher(dispatcher: OnBackPressedDispatcher) {
         super.setOnBackPressedDispatcher(dispatcher)
diff --git a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
index 55b453f..f929423 100644
--- a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
+++ b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
@@ -168,7 +168,7 @@
      * @param navController The NavController whose navigation actions will be reflected
      * in the title of the action bar.
      * @param openableLayout The Openable layout that should be toggled from the home button
-     * @see .setupActionBarWithNavController
+     * @see NavigationUI.setupActionBarWithNavController
      */
     @JvmStatic
     public fun setupActionBarWithNavController(
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/PagedListSampleActivity.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/PagedListSampleActivity.kt
index c9634cb..775f65b 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/PagedListSampleActivity.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/PagedListSampleActivity.kt
@@ -20,13 +20,11 @@
 import android.widget.Button
 import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.Observer
 import androidx.paging.LoadState
 import androidx.paging.LoadState.Error
-import androidx.paging.LoadState.NotLoading
 import androidx.paging.LoadState.Loading
+import androidx.paging.LoadState.NotLoading
 import androidx.paging.LoadType
-import androidx.paging.PagedList
 import androidx.paging.integration.testapp.R
 import androidx.paging.integration.testapp.v3.StateItemAdapter
 import androidx.recyclerview.widget.RecyclerView
@@ -48,10 +46,9 @@
         )
 
         @Suppress("DEPRECATION")
-        viewModel.livePagedList.observe(
-            this,
-            Observer<PagedList<Item>> { pagingAdapter.submitList(it) }
-        )
+        viewModel.livePagedList.observe(this) { pagedList ->
+            pagingAdapter.submitList(pagedList)
+        }
 
         setupLoadStateButtons(viewModel, pagingAdapter)
 
@@ -79,7 +76,7 @@
                     button.text = if (state.endOfPaginationReached) "Refresh" else "Done"
                     button.isEnabled = state.endOfPaginationReached
                 }
-                is Loading -> {
+                Loading -> {
                     button.text = "Loading"
                     button.isEnabled = false
                 }
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
index b9b3d3d..e07551d 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
@@ -19,7 +19,6 @@
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.arch.core.executor.TaskExecutor
 import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.Observer
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.paging.LoadState.Error
 import androidx.paging.LoadState.Loading
@@ -42,6 +41,7 @@
 import org.junit.runner.RunWith
 
 @SmallTest
+@Suppress("DEPRECATION")
 @RunWith(AndroidJUnit4::class)
 class LivePagedListBuilderTest {
     private val backgroundExecutor = TestExecutor()
@@ -78,7 +78,7 @@
         ArchTaskExecutor.getInstance().setDelegate(null)
     }
 
-    class MockDataSourceFactory {
+    class MockPagingSourceFactory {
         fun create(): PagingSource<Int, String> {
             return MockPagingSource()
         }
@@ -136,8 +136,7 @@
         // represent the common case when writing tests.
         ArchTaskExecutor.getInstance().setDelegate(null)
 
-        @Suppress("DEPRECATION")
-        LivePagedListBuilder(MockDataSourceFactory()::create, 2)
+        LivePagedListBuilder(MockPagingSourceFactory()::create, 2)
             .build()
     }
 
@@ -145,21 +144,15 @@
     fun executorBehavior() {
         // specify a background dispatcher via builder, and verify it gets used for all loads,
         // overriding default IO dispatcher
-        @Suppress("DEPRECATION")
-        val livePagedList = LivePagedListBuilder(MockDataSourceFactory()::create, 2)
+        val livePagedList = LivePagedListBuilder(MockPagingSourceFactory()::create, 2)
             .setFetchExecutor(backgroundExecutor)
             .build()
 
-        @Suppress("DEPRECATION")
         val pagedListHolder: Array<PagedList<String>?> = arrayOfNulls(1)
 
-        @Suppress("DEPRECATION")
-        livePagedList.observe(
-            lifecycleOwner,
-            Observer<PagedList<String>> { newList ->
-                pagedListHolder[0] = newList
-            }
-        )
+        livePagedList.observe(lifecycleOwner) { newList ->
+            pagedListHolder[0] = newList
+        }
 
         // initially, immediately get passed empty initial list
         assertNotNull(pagedListHolder[0])
@@ -181,24 +174,18 @@
 
     @Test
     fun failedLoad() {
-        val factory = MockDataSourceFactory()
+        val factory = MockPagingSourceFactory()
         factory.enqueueError()
 
-        @Suppress("DEPRECATION")
         val livePagedList = LivePagedListBuilder(factory::create, 2)
             .setFetchExecutor(backgroundExecutor)
             .build()
 
-        @Suppress("DEPRECATION")
         val pagedListHolder: Array<PagedList<String>?> = arrayOfNulls(1)
 
-        @Suppress("DEPRECATION")
-        livePagedList.observe(
-            lifecycleOwner,
-            Observer<PagedList<String>> { newList ->
-                pagedListHolder[0] = newList
-            }
-        )
+        livePagedList.observe(lifecycleOwner) { newList ->
+            pagedListHolder[0] = newList
+        }
 
         val loadStates = mutableListOf<LoadStateEvent>()
 
diff --git a/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.kt b/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.kt
index 26e7cd5..907a45a 100644
--- a/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.kt
+++ b/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.kt
@@ -274,8 +274,8 @@
     /**
      * Constructs the `LiveData<PagedList>`.
      *
-     * No work (such as loading) is done immediately, the creation of the first PagedList is is
-     * deferred until the LiveData is observed.
+     * No work (such as loading) is done immediately, the creation of the first [PagedList] is
+     * deferred until the [LiveData] is observed.
      *
      * @return The [LiveData] of [PagedList]s
      */
diff --git a/resourceinspection/resourceinspection-processor/build.gradle b/resourceinspection/resourceinspection-processor/build.gradle
index 4b3a931..7e881ef9 100644
--- a/resourceinspection/resourceinspection-processor/build.gradle
+++ b/resourceinspection/resourceinspection-processor/build.gradle
@@ -31,7 +31,8 @@
     implementation(project(":resourceinspection:resourceinspection-annotation"))
     implementation(project(":annotation:annotation"))
 
-    implementation(AUTO_COMMON)
+    // TODO(183520738): Upgrade auto-common dependency to newest version 0.11.
+    implementation("com.google.auto:auto-common:0.10")
     implementation(AUTO_SERVICE_ANNOTATIONS)
     implementation(GRADLE_INCAP_HELPER)
     implementation(GUAVA)
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
index 5bc1d03..4263bda 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.util
 
 import com.tschuchort.compiletesting.KotlinCompilation
+import org.jetbrains.kotlin.config.JvmTarget
 import java.io.File
 import java.io.OutputStream
 import java.net.URLClassLoader
@@ -43,7 +44,7 @@
         // workaround for https://github.com/tschuchortdev/kotlin-compile-testing/issues/105
         compilation.kotlincArguments += "-Xjava-source-roots=${javaSrcRoot.absolutePath}"
         compilation.jvmDefault = "enable"
-        compilation.jvmTarget = "1.8"
+        compilation.jvmTarget = JvmTarget.JVM_1_8.description
         compilation.inheritClassPath = false
         compilation.verbose = false
         compilation.classpaths = Classpaths.inheritedClasspath + classpaths
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotated.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotated.kt
new file mode 100644
index 0000000..815df3f
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotated.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing
+
+import kotlin.reflect.KClass
+
+/**
+ * Internal API for [XAnnotated] that handles repeated annotations.
+ */
+internal interface InternalXAnnotated : XAnnotated {
+    /**
+     * Repeated annotations show up differently between source and .class files.
+     *
+     * To avoid that inconsistency, [XAnnotated] only provides [XAnnotated.getAnnotations] and in
+     * this internal wrapper, we handle that inconsistency by finding the container class and
+     * asking implementers to implement the 2 arg version instead.
+     *
+     * see: https://github.com/google/ksp/issues/356
+     * see: https://github.com/google/ksp/issues/358
+     * see: https://youtrack.jetbrains.com/issue/KT-12794
+     *
+     * @param annotation The annotation to query
+     * @param containerAnnotation The container annotation of the [annotation] if it is a repeatable
+     * annotation.
+     *
+     * @see hasAnnotation
+     */
+    fun <T : Annotation> getAnnotations(
+        annotation: KClass<T>,
+        containerAnnotation: KClass<out Annotation>? = annotation.containerAnnotation
+    ): List<XAnnotationBox<T>>
+
+    override fun <T : Annotation> getAnnotations(annotation: KClass<T>) = getAnnotations(
+        annotation = annotation,
+        containerAnnotation = annotation.containerAnnotation
+    )
+
+    override fun hasAnnotation(annotation: KClass<out Annotation>) = hasAnnotation(
+        annotation = annotation,
+        containerAnnotation = annotation.containerAnnotation
+    )
+
+    /**
+     * Returns `true` if this element is annotated with the given [annotation].
+     *
+     * Note that this method should check for both [annotation] and [containerAnnotation] to
+     * support repeated annotations.
+     *
+     * @param annotation The annotation to query
+     * @param containerAnnotation The container annotation of the [annotation] if it is a repeatable
+     * annotation.
+     *
+     * @see [toAnnotationBox]
+     * @see [hasAnyOf]
+     */
+    fun hasAnnotation(
+        annotation: KClass<out Annotation>,
+        containerAnnotation: KClass<out Annotation>? = annotation.containerAnnotation
+    ): Boolean
+}
\ No newline at end of file
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/KClassExt.kt
similarity index 61%
copy from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
copy to room/compiler-processing/src/main/java/androidx/room/compiler/processing/KClassExt.kt
index e022dc3..02207f2 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/KClassExt.kt
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package androidx.car.app.model.signin;
+package androidx.room.compiler.processing
 
-import androidx.car.app.IOnDoneCallback;
+import kotlin.reflect.KClass
 
-/** @hide */
-oneway interface IOnInputCompletedListener {
-  void onInputCompleted(String value, IOnDoneCallback callback) = 1;
-}
+private typealias JavaRepeatable = java.lang.annotation.Repeatable
+
+/**
+ * Returns the container annotation if `this` is a Repeatable annotation.
+ */
+internal val <T : Annotation> KClass<T>.containerAnnotation: KClass<out Annotation>?
+    get() = this.java.getAnnotation(JavaRepeatable::class.java)?.value
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
index bc43682..de23361 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
@@ -23,13 +23,30 @@
  */
 interface XAnnotated {
     /**
-     * If the current element has an annotation with the given [annotation] class, a boxed instance
-     * of it will be returned where fields can be read. Otherwise, `null` value is returned.
+     * Gets the list of annotations with the given type.
+     *
+     * For repeated annotations declared in Java code, please use the repeated annotation type,
+     * not the container. Calling this method with a container annotation will have inconsistent
+     * behaviour between Java AP and KSP.
      *
      * @see [hasAnnotation]
      * @see [hasAnnotationWithPackage]
      */
-    fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>?
+    fun <T : Annotation> getAnnotations(
+        annotation: KClass<T>
+    ): List<XAnnotationBox<T>>
+
+    /**
+     * Returns `true` if this element is annotated with the given [annotation].
+     *
+     * For repeated annotations declared in Java code, please use the repeated annotation type,
+     * not the container. Calling this method with a container annotation will have inconsistent
+     * behaviour between Java AP and KSP.
+     * @see [hasAnyOf]
+     */
+    fun hasAnnotation(
+        annotation: KClass<out Annotation>
+    ): Boolean
 
     /**
      * Returns `true` if this element has an annotation that is declared in the given package.
@@ -38,15 +55,26 @@
     fun hasAnnotationWithPackage(pkg: String): Boolean
 
     /**
-     * Returns `true` if this element is annotated with the given [annotation].
-     *
-     * @see [toAnnotationBox]
-     * @see [hasAnyOf]
-     */
-    fun hasAnnotation(annotation: KClass<out Annotation>): Boolean
-
-    /**
      * Returns `true` if this element has one of the [annotations].
      */
     fun hasAnyOf(vararg annotations: KClass<out Annotation>) = annotations.any(this::hasAnnotation)
+
+    @Deprecated(
+        replaceWith = ReplaceWith("getAnnotation(annotation)"),
+        message = "Use getAnnotation(not repeatable) or getAnnotations (repeatable)"
+    )
+    fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>? =
+        getAnnotation(annotation)
+
+    /**
+     * If the current element has an annotation with the given [annotation] class, a boxed instance
+     * of it will be returned where fields can be read. Otherwise, `null` value is returned.
+     *
+     * @see [hasAnnotation]
+     * @see [getAnnotations]
+     * @see [hasAnnotationWithPackage]
+     */
+    fun <T : Annotation> getAnnotation(annotation: KClass<T>): XAnnotationBox<T>? {
+        return getAnnotations(annotation).firstOrNull()
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingStep.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingStep.kt
index 7063b75..725faa16 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingStep.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingStep.kt
@@ -22,13 +22,12 @@
 import androidx.room.compiler.processing.ksp.KspTypeElement
 import com.google.auto.common.BasicAnnotationProcessor
 import com.google.auto.common.MoreElements
-import com.google.common.collect.SetMultimap
+import com.google.common.collect.ImmutableSetMultimap
 import com.google.devtools.ksp.symbol.KSAnnotated
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import javax.annotation.processing.ProcessingEnvironment
 import javax.lang.model.element.Element
 import javax.tools.Diagnostic
-import kotlin.reflect.KClass
 
 /**
  * Specialized processing step which only supports annotations on TypeElements.
@@ -47,13 +46,13 @@
      */
     fun process(
         env: XProcessingEnv,
-        elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+        elementsByAnnotation: Map<String, List<XTypeElement>>
     ): Set<XTypeElement>
 
     /**
-     * The set of annotations processed by this step.
+     * The set of annotation qualified names processed by this step.
      */
-    fun annotations(): Set<KClass<out Annotation>>
+    fun annotations(): Set<String>
 
     /**
      * Wraps current [XProcessingStep] into an Auto Common
@@ -61,7 +60,7 @@
      */
     fun asAutoCommonProcessor(
         env: ProcessingEnvironment
-    ): BasicAnnotationProcessor.ProcessingStep {
+    ): BasicAnnotationProcessor.Step {
         return JavacProcessingStepDelegate(
             env = env,
             delegate = this
@@ -72,7 +71,7 @@
         check(env is KspProcessingEnv)
         val args = annotations().associateWith { annotation ->
             val elements = env.resolver.getSymbolsWithAnnotation(
-                annotation.java.canonicalName
+                annotation
             ).filterIsInstance<KSClassDeclaration>()
                 .map {
                     env.requireTypeElement(it.qualifiedName!!.asString())
@@ -84,15 +83,17 @@
     }
 }
 
-@Suppress("UnstableApiUsage")
 internal class JavacProcessingStepDelegate(
     val env: ProcessingEnvironment,
     val delegate: XProcessingStep
-) : BasicAnnotationProcessor.ProcessingStep {
+) : BasicAnnotationProcessor.Step {
+    override fun annotations(): Set<String> = delegate.annotations()
+
+    @Suppress("UnstableApiUsage")
     override fun process(
-        elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>
+        elementsByAnnotation: ImmutableSetMultimap<String, Element>
     ): Set<Element> {
-        val converted = mutableMapOf<KClass<out Annotation>, List<XTypeElement>>()
+        val converted = mutableMapOf<String, List<XTypeElement>>()
         // create a new x processing environment for each step to ensure it can freely cache
         // whatever it wants and we don't keep elements references across rounds.
         val xEnv = JavacProcessingEnv(env)
@@ -109,17 +110,11 @@
                     null
                 }
             }
-            converted[annotation.kotlin] = elements
+            converted[annotation] = elements
         }
         val result = delegate.process(xEnv, converted)
         return result.map {
             (it as JavacElement).element
         }.toSet()
     }
-
-    override fun annotations(): Set<Class<out Annotation>> {
-        return delegate.annotations().mapTo(mutableSetOf()) {
-            it.java
-        }
-    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XRoundEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XRoundEnv.kt
index 0f41811..e7b19ef 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XRoundEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XRoundEnv.kt
@@ -36,9 +36,9 @@
     val rootElements: Set<XElement>
 
     /**
-     * Returns the set of [XElement]s that are annotated with the given [klass].
+     * Returns the set of [XElement]s that are annotated with the given [annotationQualifiedName].
      */
-    fun getTypeElementsAnnotatedWith(klass: Class<out Annotation>): Set<XTypeElement>
+    fun getTypeElementsAnnotatedWith(annotationQualifiedName: String): Set<XTypeElement>
 
     companion object {
         /**
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
index d12eb4d..7397551 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
@@ -16,10 +16,12 @@
 
 package androidx.room.compiler.processing.javac
 
+import androidx.room.compiler.processing.InternalXAnnotated
 import androidx.room.compiler.processing.XAnnotationBox
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XEquality
 import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreElements.isAnnotationPresent
 import java.util.Locale
 import javax.lang.model.element.Element
 import kotlin.reflect.KClass
@@ -28,16 +30,34 @@
 internal abstract class JavacElement(
     protected val env: JavacProcessingEnv,
     open val element: Element
-) : XElement, XEquality {
-    override fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>? {
-        return MoreElements
-            .getAnnotationMirror(element, annotation.java)
-            .orNull()
-            ?.box(env, annotation.java)
+) : XElement, XEquality, InternalXAnnotated {
+    override fun <T : Annotation> getAnnotations(
+        annotation: KClass<T>,
+        containerAnnotation: KClass<out Annotation>?
+    ): List<XAnnotationBox<T>> {
+        return if (containerAnnotation == null) {
+            MoreElements
+                .getAnnotationMirror(element, annotation.java)
+                .orNull()
+                ?.box(env, annotation.java)
+                ?.let {
+                    listOf(it)
+                }
+        } else {
+            val container = MoreElements
+                .getAnnotationMirror(element, containerAnnotation.java)
+                .orNull()
+                ?.box(env, containerAnnotation.java)
+            container?.getAsAnnotationBoxArray<T>("value")?.toList()
+        } ?: emptyList()
     }
 
-    override fun hasAnnotation(annotation: KClass<out Annotation>): Boolean {
-        return MoreElements.isAnnotationPresent(element, annotation.java)
+    override fun hasAnnotation(
+        annotation: KClass<out Annotation>,
+        containerAnnotation: KClass<out Annotation>?
+    ): Boolean {
+        return isAnnotationPresent(element, annotation.java) ||
+            (containerAnnotation != null && isAnnotationPresent(element, containerAnnotation.java))
     }
 
     override fun toString(): String {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacRoundEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacRoundEnv.kt
index 672a539..d878f1d 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacRoundEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacRoundEnv.kt
@@ -37,8 +37,10 @@
     }
 
     // TODO this is only for tests but we may need to support more types of elements
-    override fun getTypeElementsAnnotatedWith(klass: Class<out Annotation>): Set<XTypeElement> {
-        val result = delegate.getElementsAnnotatedWith(klass)
+    override fun getTypeElementsAnnotatedWith(annotationQualifiedName: String): Set<XTypeElement> {
+        val element = env.elementUtils.getTypeElement(annotationQualifiedName)
+            ?: error("Cannot find TypeElement: $annotationQualifiedName")
+        val result = delegate.getElementsAnnotatedWith(element)
         return result.filter {
             MoreElements.isType(it)
         }.map {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
index 0e6a7aa..0cf6f88 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
+import androidx.room.compiler.processing.InternalXAnnotated
 import androidx.room.compiler.processing.XAnnotated
 import androidx.room.compiler.processing.XAnnotationBox
 import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
@@ -25,20 +26,40 @@
 
 internal sealed class KspAnnotated(
     val env: KspProcessingEnv
-) : XAnnotated {
+) : InternalXAnnotated {
     abstract fun annotations(): Sequence<KSAnnotation>
 
-    override fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>? {
-        return annotations().firstOrNull {
+    private fun <T : Annotation> findAnnotations(annotation: KClass<T>): Sequence<KSAnnotation> {
+        return annotations().filter {
             val qName = it.annotationType.resolve().declaration.qualifiedName?.asString()
             qName == annotation.qualifiedName
-        }?.let {
+        }
+    }
+
+    override fun <T : Annotation> getAnnotations(
+        annotation: KClass<T>,
+        containerAnnotation: KClass<out Annotation>?
+    ): List<XAnnotationBox<T>> {
+        // we'll try both because it can be the container or the annotation itself.
+        // try container first
+        if (containerAnnotation != null) {
+            // if container also repeats, this won't work but we don't have that use case
+            findAnnotations(containerAnnotation).firstOrNull()?.let {
+                return KspAnnotationBox(
+                    env = env,
+                    annotation = it,
+                    annotationClass = containerAnnotation.java,
+                ).getAsAnnotationBoxArray<T>("value").toList()
+            }
+        }
+        // didn't find anything with the container, try the annotation class
+        return findAnnotations(annotation).map {
             KspAnnotationBox(
                 env = env,
                 annotationClass = annotation.java,
                 annotation = it
             )
-        }
+        }.toList()
     }
 
     override fun hasAnnotationWithPackage(pkg: String): Boolean {
@@ -47,10 +68,14 @@
         }
     }
 
-    override fun hasAnnotation(annotation: KClass<out Annotation>): Boolean {
+    override fun hasAnnotation(
+        annotation: KClass<out Annotation>,
+        containerAnnotation: KClass<out Annotation>?
+    ): Boolean {
         return annotations().any {
             val qName = it.annotationType.resolve().declaration.qualifiedName?.asString()
-            qName == annotation.qualifiedName
+            qName == annotation.qualifiedName ||
+                (containerAnnotation != null && qName == containerAnnotation.qualifiedName)
         }
     }
 
@@ -143,4 +168,4 @@
             } ?: NotAnnotated(env)
         }
     }
-}
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
index 864b472..8162e8c 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
@@ -29,9 +29,9 @@
     override val rootElements: Set<XElement>
         get() = TODO("not supported")
 
-    override fun getTypeElementsAnnotatedWith(klass: Class<out Annotation>): Set<XTypeElement> {
+    override fun getTypeElementsAnnotatedWith(annotationQualifiedName: String): Set<XTypeElement> {
         return env.resolver.getSymbolsWithAnnotation(
-            klass.canonicalName
+            annotationQualifiedName
         ).filterIsInstance<KSClassDeclaration>()
             .map {
                 env.wrapClassDeclaration(it)
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt
index 625454e..3f67ea3 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt
@@ -42,7 +42,7 @@
             override fun doProcess(annotations: Set<XTypeElement>, roundEnv: XRoundEnv): Boolean {
                 invoked.set(true)
                 val annotatedElements = roundEnv.getTypeElementsAnnotatedWith(
-                    OtherAnnotation::class.java
+                    OtherAnnotation::class.qualifiedName!!
                 )
                 val targetElement = xProcessingEnv.requireTypeElement("foo.bar.Baz")
                 assertThat(
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
index f8bf777..06a3390 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
@@ -22,6 +22,7 @@
 import androidx.room.compiler.processing.testcode.JavaEnum
 import androidx.room.compiler.processing.testcode.MainAnnotation
 import androidx.room.compiler.processing.testcode.OtherAnnotation
+import androidx.room.compiler.processing.testcode.RepeatableJavaAnnotation
 import androidx.room.compiler.processing.testcode.TestSuppressWarnings
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
@@ -95,7 +96,7 @@
             sources = listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
-            val annotationBox = element.toAnnotationBox(TestSuppressWarnings::class)
+            val annotationBox = element.getAnnotation(TestSuppressWarnings::class)
             assertThat(annotationBox).isNotNull()
             assertThat(
                 annotationBox!!.value.value
@@ -134,7 +135,7 @@
             listOf(mySource)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
-            element.toAnnotationBox(MainAnnotation::class)!!.let { annotation ->
+            element.getAnnotation(MainAnnotation::class)!!.let { annotation ->
                 assertThat(
                     annotation.getAsTypeList("typeList")
                 ).containsExactly(
@@ -177,7 +178,7 @@
             sources = listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("Subject")
-            val annotationBox = element.toAnnotationBox(TestSuppressWarnings::class)
+            val annotationBox = element.getAnnotation(TestSuppressWarnings::class)
             assertThat(annotationBox).isNotNull()
             assertThat(
                 annotationBox!!.value.value
@@ -217,7 +218,7 @@
             listOf(mySource)
         ) { invocation ->
             val element = invocation.processingEnv.requireTypeElement("Subject")
-            element.toAnnotationBox(MainAnnotation::class)!!.let { annotation ->
+            element.getAnnotation(MainAnnotation::class)!!.let { annotation ->
                 assertThat(
                     annotation.getAsTypeList("typeList").map {
                         it.typeName
@@ -267,7 +268,7 @@
             sources = listOf(src)
         ) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("Subject")
-            val annotationValue = subject.toAnnotationBox(
+            val annotationValue = subject.getAnnotation(
                 JavaAnnotationWithTypeReferences::class
             )?.getAsTypeList("value")
             assertThat(annotationValue?.map { it.typeName }).containsExactly(
@@ -423,7 +424,7 @@
                     invocation.processingEnv.requireTypeElement(it)
                 }.forEach { typeElement ->
                     val annotation =
-                        typeElement.toAnnotationBox(JavaAnnotationWithDefaults::class)
+                        typeElement.getAnnotation(JavaAnnotationWithDefaults::class)
                     checkNotNull(annotation)
                     assertThat(annotation.value.intVal).isEqualTo(3)
                     assertThat(annotation.value.intArrayVal).isEqualTo(intArrayOf(1, 3, 5))
@@ -499,7 +500,7 @@
             arrayOf("JavaSubject", "KotlinSubject").map {
                 invocation.processingEnv.requireTypeElement(it)
             }.forEach { subject ->
-                val annotation = subject.getField("annotated1").toAnnotationBox(
+                val annotation = subject.getField("annotated1").getAnnotation(
                     JavaAnnotationWithPrimitiveArray::class
                 )
                 assertThat(
@@ -511,9 +512,65 @@
         }
     }
 
+    @Test
+    fun javaRepeatableAnnotation() {
+        val javaSrc = Source.java(
+            "JavaSubject",
+            """
+            import ${RepeatableJavaAnnotation::class.qualifiedName};
+            @RepeatableJavaAnnotation("x")
+            @RepeatableJavaAnnotation("y")
+            @RepeatableJavaAnnotation("z")
+            public class JavaSubject {}
+            """.trimIndent()
+        )
+        val kotlinSrc = Source.kotlin(
+            "KotlinSubject.kt",
+            """
+            import ${RepeatableJavaAnnotation::class.qualifiedName}
+            // TODO update when https://youtrack.jetbrains.com/issue/KT-12794 is fixed.
+            // right now, kotlin does not support repeatable annotations.
+            @RepeatableJavaAnnotation.List(
+                RepeatableJavaAnnotation("x"),
+                RepeatableJavaAnnotation("y"),
+                RepeatableJavaAnnotation("z")
+            )
+            public class KotlinSubject
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(javaSrc, kotlinSrc)
+        ) { invocation ->
+            listOf("JavaSubject", "KotlinSubject")
+                .map(invocation.processingEnv::requireTypeElement)
+                .forEach { subject ->
+                    if (invocation.isKsp && preCompiled) {
+                        // TODO remove once https://github.com/google/ksp/issues/356 is fixed
+                        // KSP cannot read array of annotation values in compiled code
+                    } else {
+                        val annotations = subject.getAnnotations(
+                            RepeatableJavaAnnotation::class
+                        )
+                        assertThat(
+                            subject.hasAnnotation(
+                                RepeatableJavaAnnotation::class
+                            )
+                        ).isTrue()
+                        val values = annotations
+                            .map {
+                                it.value.value
+                            }
+                        assertWithMessage(subject.qualifiedName)
+                            .that(values)
+                            .containsExactly("x", "y", "z")
+                    }
+                }
+        }
+    }
+
     // helper function to read what we need
     private fun XAnnotated.getSuppressValues(): Array<String>? {
-        return this.toAnnotationBox(TestSuppressWarnings::class)?.value?.value
+        return this.getAnnotation(TestSuppressWarnings::class)?.value?.value
     }
 
     private fun XAnnotated.assertHasSuppressWithValue(vararg expected: String) {
@@ -541,7 +598,7 @@
     }
 
     private fun XAnnotated.getOtherAnnotationValue(): String? {
-        return this.toAnnotationBox(OtherAnnotation::class)?.value?.value
+        return this.getAnnotation(OtherAnnotation::class)?.value?.value
     }
 
     companion object {
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
index 0eacab4..ac75488 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
@@ -54,23 +54,26 @@
         val processingStep = object : XProcessingStep {
             override fun process(
                 env: XProcessingEnv,
-                elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+                elementsByAnnotation: Map<String, List<XTypeElement>>
             ): Set<XTypeElement> {
-                elementsByAnnotation[OtherAnnotation::class]?.forEach {
+                elementsByAnnotation[OtherAnnotation::class.qualifiedName]?.forEach {
                     annotatedElements[OtherAnnotation::class] = it.qualifiedName
                 }
-                elementsByAnnotation[MainAnnotation::class]?.forEach {
+                elementsByAnnotation[MainAnnotation::class.qualifiedName]?.forEach {
                     annotatedElements[MainAnnotation::class] = it.qualifiedName
                 }
                 return emptySet()
             }
 
-            override fun annotations(): Set<KClass<out Annotation>> {
-                return setOf(OtherAnnotation::class, MainAnnotation::class)
+            override fun annotations(): Set<String> {
+                return setOf(
+                    OtherAnnotation::class.qualifiedName!!,
+                    MainAnnotation::class.qualifiedName!!
+                )
             }
         }
         val mainProcessor = object : BasicAnnotationProcessor() {
-            override fun initSteps(): Iterable<ProcessingStep> {
+            override fun steps(): Iterable<Step> {
                 return listOf(
                     processingStep.asAutoCommonProcessor(processingEnv)
                 )
@@ -123,11 +126,11 @@
         val processingStep = object : XProcessingStep {
             override fun process(
                 env: XProcessingEnv,
-                elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+                elementsByAnnotation: Map<String, List<XTypeElement>>
             ): Set<XTypeElement> {
                 // for each element annotated with Main annotation, create a class with Other
                 // annotation to trigger another round
-                elementsByAnnotation[MainAnnotation::class]?.forEach {
+                elementsByAnnotation[MainAnnotation::class.qualifiedName]?.forEach {
                     val className = ClassName.get(it.packageName, "${it.name}_Impl")
                     val spec = TypeSpec.classBuilder(className)
                         .addAnnotation(
@@ -140,14 +143,17 @@
                         .build()
                         .writeTo(env.filer)
                 }
-                elementsByAnnotation[OtherAnnotation::class]?.forEach {
+                elementsByAnnotation[OtherAnnotation::class.qualifiedName]?.forEach {
                     otherAnnotatedElements.add(it.type.typeName)
                 }
                 return emptySet()
             }
 
-            override fun annotations(): Set<KClass<out Annotation>> {
-                return setOf(OtherAnnotation::class, MainAnnotation::class)
+            override fun annotations(): Set<String> {
+                return setOf(
+                    OtherAnnotation::class.qualifiedName!!,
+                    MainAnnotation::class.qualifiedName!!
+                )
             }
         }
         val main = JavaFileObjects.forSourceString(
@@ -171,16 +177,16 @@
             listOf(main)
         ).processedWith(
             object : BasicAnnotationProcessor() {
-                override fun initSteps(): Iterable<ProcessingStep> {
+                override fun steps(): Iterable<Step> {
                     return listOf(
                         processingStep.asAutoCommonProcessor(processingEnv)
                     )
                 }
 
-                override fun getSupportedOptions(): MutableSet<String> {
-                    return mutableSetOf(
-                        MainAnnotation::class.java.canonicalName,
-                        OtherAnnotation::class.java.canonicalName
+                override fun getSupportedOptions(): Set<String> {
+                    return setOf(
+                        MainAnnotation::class.qualifiedName!!,
+                        OtherAnnotation::class.qualifiedName!!
                     )
                 }
             }
@@ -198,14 +204,14 @@
             var roundCounter = 0
             override fun process(
                 env: XProcessingEnv,
-                elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+                elementsByAnnotation: Map<String, List<XTypeElement>>
             ): Set<XTypeElement> {
                 elementPerRound[roundCounter++] = listOf(
                     env.requireTypeElement("foo.bar.Main"),
                     env.requireTypeElement("foo.bar.Main")
                 )
                 // trigger another round
-                elementsByAnnotation[MainAnnotation::class]?.forEach {
+                elementsByAnnotation[MainAnnotation::class.qualifiedName]?.forEach {
                     val className = ClassName.get(it.packageName, "${it.name}_Impl")
                     val spec = TypeSpec.classBuilder(className)
                         .addAnnotation(
@@ -221,8 +227,11 @@
                 return emptySet()
             }
 
-            override fun annotations(): Set<KClass<out Annotation>> {
-                return setOf(OtherAnnotation::class, MainAnnotation::class)
+            override fun annotations(): Set<String> {
+                return setOf(
+                    OtherAnnotation::class.qualifiedName!!,
+                    MainAnnotation::class.qualifiedName!!
+                )
             }
         }
         val main = JavaFileObjects.forSourceString(
@@ -246,16 +255,16 @@
             listOf(main)
         ).processedWith(
             object : BasicAnnotationProcessor() {
-                override fun initSteps(): Iterable<ProcessingStep> {
+                override fun steps(): Iterable<Step> {
                     return listOf(
                         processingStep.asAutoCommonProcessor(processingEnv)
                     )
                 }
 
-                override fun getSupportedOptions(): MutableSet<String> {
-                    return mutableSetOf(
-                        MainAnnotation::class.java.canonicalName,
-                        OtherAnnotation::class.java.canonicalName
+                override fun getSupportedOptions(): Set<String> {
+                    return setOf(
+                        MainAnnotation::class.qualifiedName!!,
+                        OtherAnnotation::class.qualifiedName!!
                     )
                 }
             }
@@ -278,15 +287,15 @@
         val processingStep = object : XProcessingStep {
             override fun process(
                 env: XProcessingEnv,
-                elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+                elementsByAnnotation: Map<String, List<XTypeElement>>
             ): Set<XTypeElement> {
                 return elementsByAnnotation.values
                     .flatten()
                     .toSet()
             }
 
-            override fun annotations(): Set<KClass<out Annotation>> {
-                return setOf(OtherAnnotation::class)
+            override fun annotations(): Set<String> {
+                return setOf(OtherAnnotation::class.qualifiedName!!)
             }
         }
         var returned: List<KSAnnotated>? = null
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/RepeatableJavaAnnotation.java b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/RepeatableJavaAnnotation.java
new file mode 100644
index 0000000..cc0ba38
--- /dev/null
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/RepeatableJavaAnnotation.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.testcode;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Repeatable(RepeatableJavaAnnotation.List.class)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RepeatableJavaAnnotation {
+    String value();
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface List {
+        RepeatableJavaAnnotation[] value();
+    }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt b/room/compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
index ede6dab..dd00786 100644
--- a/room/compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
@@ -29,16 +29,15 @@
 import androidx.room.writer.DaoWriter
 import androidx.room.writer.DatabaseWriter
 import java.io.File
-import kotlin.reflect.KClass
 
 class DatabaseProcessingStep : XProcessingStep {
     override fun process(
         env: XProcessingEnv,
-        elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+        elementsByAnnotation: Map<String, List<XTypeElement>>
     ): Set<XTypeElement> {
         val context = Context(env)
         val rejectedElements = mutableSetOf<XTypeElement>()
-        val databases = elementsByAnnotation[Database::class]
+        val databases = elementsByAnnotation[Database::class.qualifiedName]
             ?.mapNotNull {
                 try {
                     DatabaseProcessor(
@@ -100,8 +99,8 @@
         return rejectedElements
     }
 
-    override fun annotations(): Set<KClass<out Annotation>> {
-        return mutableSetOf(Database::class)
+    override fun annotations(): Set<String> {
+        return mutableSetOf(Database::class.qualifiedName!!)
     }
 
     /**
diff --git a/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
index 76a7a8a..ed1595c 100644
--- a/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
@@ -38,7 +38,7 @@
     /** Helper variable to avoid reporting the warning twice. */
     private var jdkVersionHasBugReported = false
 
-    override fun initSteps(): MutableIterable<ProcessingStep> {
+    override fun steps(): MutableIterable<Step> {
         return mutableListOf(
             DatabaseProcessingStep().asAutoCommonProcessor(processingEnv)
         )
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
index 00268f2..d8818644 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
@@ -26,6 +26,9 @@
 import androidx.room.vo.AutoMigrationResult
 import java.io.File
 
+// TODO: (b/183435544) Support downgrades in AutoMigrations.
+// TODO: (b/183007590) Use the callback in the AutoMigration annotation while end-to-end
+//  testing, when column/table rename/deletes are supported
 class AutoMigrationProcessor(
     val context: Context,
     val element: XTypeElement,
@@ -46,7 +49,8 @@
             return null
         }
 
-        if (!context.processingEnv.requireType(RoomTypeNames.AUTO_MIGRATION_CALLBACK)
+        if (!context.processingEnv
+            .requireType(RoomTypeNames.AUTO_MIGRATION_CALLBACK)
             .isAssignableFrom(element.type)
         ) {
             context.logger.e(
@@ -56,7 +60,7 @@
             return null
         }
 
-        val annotationBox = element.toAnnotationBox(AutoMigration::class)
+        val annotationBox = element.getAnnotation(AutoMigration::class)
         if (annotationBox == null) {
             context.logger.e(
                 element,
@@ -100,18 +104,18 @@
             return null
         }
 
+        // TODO: (b/183434667) Update the automigration result data object to handle complex
+        //  schema changes' presence when writer code is introduced
         return AutoMigrationResult(
             element = element,
             from = fromSchemaBundle.version,
             to = toSchemaBundle.version,
-            addedColumns = schemaDiff.addedColumn,
-            addedTables = schemaDiff.addedTable
+            addedColumns = schemaDiff.addedColumns,
+            addedTables = schemaDiff.addedTables
         )
     }
 
-    // TODO: File bug for not supporting downgrades.
-    // TODO: (b/180389433) If the files don't exist the getSchemaFile() method should return
-    //  null and before calling process
+    // TODO: (b/180389433) Verify automigration schemas before calling the AutoMigrationProcessor
     private fun getValidatedSchemaFile(version: Int): File? {
         val schemaFile = File(
             context.schemaOutFolder,
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
index 4f284c2..aceb277 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
@@ -44,7 +44,7 @@
             isError() || isVoid() || isNone()
 
         fun findConverters(context: Context, element: XElement): ProcessResult {
-            val annotation = element.toAnnotationBox(TypeConverters::class)
+            val annotation = element.getAnnotation(TypeConverters::class)
             return annotation?.let {
                 val classes = it.getAsTypeList("value")
                     .mapTo(LinkedHashSet()) { it }
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
index 745bd1f..35ca51e 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -55,7 +55,7 @@
     }
 
     private fun doProcess(): Database {
-        val dbAnnotation = element.toAnnotationBox(androidx.room.Database::class)!!
+        val dbAnnotation = element.getAnnotation(androidx.room.Database::class)!!
 
         val entities = processEntities(dbAnnotation, element)
         val viewsMap = processDatabaseViews(dbAnnotation)
@@ -125,7 +125,7 @@
         element: XTypeElement,
         latestDbSchema: DatabaseBundle
     ): List<AutoMigrationResult> {
-        val dbAnnotation = element.toAnnotationBox(androidx.room.Database::class)!!
+        val dbAnnotation = element.getAnnotation(androidx.room.Database::class)!!
         val autoMigrationList = dbAnnotation.getAsTypeList("autoMigrations")
         context.checker.check(
             autoMigrationList.isEmpty() || dbAnnotation.value.exportSchema,
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseViewProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseViewProcessor.kt
index 0253959..67aecd0 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseViewProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseViewProcessor.kt
@@ -35,7 +35,7 @@
             element, androidx.room.DatabaseView::class,
             ProcessorErrors.VIEW_MUST_BE_ANNOTATED_WITH_DATABASE_VIEW
         )
-        val annotationBox = element.toAnnotationBox(androidx.room.DatabaseView::class)
+        val annotationBox = element.getAnnotation(androidx.room.DatabaseView::class)
 
         val viewName: String = if (annotationBox != null) {
             extractViewName(element, annotationBox.value)
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
index 9e08987..f436016 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
@@ -37,7 +37,7 @@
     fun process(): Field {
         val member = element.asMemberOf(containing)
         val type = member.typeName
-        val columnInfo = element.toAnnotationBox(ColumnInfo::class)?.value
+        val columnInfo = element.getAnnotation(ColumnInfo::class)?.value
         val name = element.name
         val rawCName = if (columnInfo != null && columnInfo.name != ColumnInfo.INHERIT_FIELD_NAME) {
             columnInfo.name
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/FtsTableEntityProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/FtsTableEntityProcessor.kt
index eec6de9..7956c0d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/FtsTableEntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/FtsTableEntityProcessor.kt
@@ -57,7 +57,7 @@
             element, androidx.room.Entity::class,
             ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
         )
-        val entityAnnotation = element.toAnnotationBox(androidx.room.Entity::class)
+        val entityAnnotation = element.getAnnotation(androidx.room.Entity::class)
         val tableName: String
         if (entityAnnotation != null) {
             tableName = extractTableName(element, entityAnnotation.value)
@@ -84,9 +84,9 @@
         context.checker.check(pojo.relations.isEmpty(), element, ProcessorErrors.RELATION_IN_ENTITY)
 
         val (ftsVersion, ftsOptions) = if (element.hasAnnotation(androidx.room.Fts3::class)) {
-            FtsVersion.FTS3 to getFts3Options(element.toAnnotationBox(Fts3::class)!!)
+            FtsVersion.FTS3 to getFts3Options(element.getAnnotation(Fts3::class)!!)
         } else {
-            FtsVersion.FTS4 to getFts4Options(element.toAnnotationBox(Fts4::class)!!)
+            FtsVersion.FTS4 to getFts4Options(element.getAnnotation(Fts4::class)!!)
         }
 
         val shadowTableName = if (ftsOptions.contentEntity != null) {
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
index 8747d0d..05c820d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
@@ -140,7 +140,7 @@
             }
 
         val ignoredColumns =
-            element.toAnnotationBox(androidx.room.Entity::class)?.value?.ignoredColumns?.toSet()
+            element.getAnnotation(androidx.room.Entity::class)?.value?.ignoredColumns?.toSet()
                 ?: emptySet()
         val fieldBindingErrors = mutableMapOf<Field, String>()
         val unfilteredMyFields = allFields[null]
@@ -403,7 +403,7 @@
             return null
         }
 
-        val fieldPrefix = variableElement.toAnnotationBox(Embedded::class)?.value?.prefix ?: ""
+        val fieldPrefix = variableElement.getAnnotation(Embedded::class)?.value?.prefix ?: ""
         val inheritedPrefix = parent?.prefix ?: ""
         val embeddedField = Field(
             variableElement,
@@ -432,7 +432,7 @@
         container: XType?,
         relationElement: XFieldElement
     ): androidx.room.vo.Relation? {
-        val annotation = relationElement.toAnnotationBox(Relation::class)!!
+        val annotation = relationElement.getAnnotation(Relation::class)!!
 
         val parentField = myFields.firstOrNull {
             it.columnName == annotation.value.parentColumn
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 05a3b6a..a62aac8 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -841,8 +841,9 @@
     }
 
     fun columnWithChangedSchemaFound(columnName: String): String {
-        return "Encountered column '$columnName' with a changed FieldBundle schema. This change " +
-            "is not currently supported by AutoMigration."
+        return "Encountered column '$columnName' with an unsupported schema change at the column " +
+            "level (e.g. affinity change). These changes are not yet " +
+            "supported by AutoMigration."
     }
 
     fun removedOrRenamedColumnFound(columnName: String): String {
@@ -850,6 +851,12 @@
             "renamed. This change is not currently supported by AutoMigration."
     }
 
+    fun tableWithComplexChangedSchemaFound(tableName: String): String {
+        return "Encountered table '$tableName' with an unsupported schema change at the table " +
+            "level (e.g. primary key, foreign key or index change). These changes are not yet " +
+            "supported by AutoMigration."
+    }
+
     fun removedOrRenamedTableFound(tableName: String): String {
         return "Table '$tableName' has been either removed or " +
             "renamed. This change is not currently supported by AutoMigration."
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
index b495976..9bee7d3 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
@@ -48,7 +48,7 @@
      * implemented as a sub procedure in [InternalQueryProcessor].
      */
     fun process(): QueryMethod {
-        val annotation = executableElement.toAnnotationBox(Query::class)?.value
+        val annotation = executableElement.getAnnotation(Query::class)?.value
         context.checker.check(
             annotation != null, executableElement,
             ProcessorErrors.MISSING_QUERY_ANNOTATION
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
index 1ee04bb..4d2a30d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
@@ -72,7 +72,7 @@
     }
 
     private fun processObservedTables(): Set<String> {
-        val annotation = executableElement.toAnnotationBox(RawQuery::class)
+        val annotation = executableElement.getAnnotation(RawQuery::class)
         return annotation?.getAsTypeList("observedEntities")
             ?.mapNotNull {
                 it.typeElement.also { typeElement ->
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index 0bf456b..b6bddc9 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -39,7 +39,7 @@
     private val delegate = MethodProcessorDelegate.createFor(context, containing, executableElement)
 
     fun <T : Annotation> extractAnnotation(klass: KClass<T>, errorMsg: String): XAnnotationBox<T>? {
-        val annotation = executableElement.toAnnotationBox(klass)
+        val annotation = executableElement.getAnnotation(klass)
         context.checker.check(annotation != null, executableElement, errorMsg)
         return annotation
     }
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt
index 390b8bd..7781560 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt
@@ -25,7 +25,7 @@
 object SuppressWarningProcessor {
 
     fun getSuppressedWarnings(element: XElement): Set<Warning> {
-        val annotation = element.toAnnotationBox(SuppressWarnings::class)?.value
+        val annotation = element.getAnnotation(SuppressWarnings::class)?.value
         return if (annotation == null) {
             emptySet()
         } else {
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/TableEntityProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/TableEntityProcessor.kt
index efc5e84..4f18a69 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/TableEntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/TableEntityProcessor.kt
@@ -58,7 +58,7 @@
             element, androidx.room.Entity::class,
             ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
         )
-        val annotationBox = element.toAnnotationBox(androidx.room.Entity::class)
+        val annotationBox = element.getAnnotation(androidx.room.Entity::class)
         val tableName: String
         val entityIndices: List<IndexInput>
         val foreignKeyInputs: List<ForeignKeyInput>
@@ -232,7 +232,7 @@
                 context.logger.e(element, ProcessorErrors.FOREIGN_KEY_CANNOT_FIND_PARENT)
                 return@map null
             }
-            val parentAnnotation = parentElement.toAnnotationBox(androidx.room.Entity::class)
+            val parentAnnotation = parentElement.getAnnotation(androidx.room.Entity::class)
             if (parentAnnotation == null) {
                 context.logger.e(
                     element,
@@ -328,7 +328,7 @@
      */
     private fun collectPrimaryKeysFromPrimaryKeyAnnotations(fields: List<Field>): List<PrimaryKey> {
         return fields.mapNotNull { field ->
-            field.element.toAnnotationBox(androidx.room.PrimaryKey::class)?.let {
+            field.element.getAnnotation(androidx.room.PrimaryKey::class)?.let {
                 if (field.parent != null) {
                     // the field in the entity that contains this error.
                     val grandParentField = field.parent.mRootParent.field.element
@@ -359,7 +359,7 @@
         typeElement: XTypeElement,
         availableFields: List<Field>
     ): List<PrimaryKey> {
-        val myPkeys = typeElement.toAnnotationBox(androidx.room.Entity::class)?.let {
+        val myPkeys = typeElement.getAnnotation(androidx.room.Entity::class)?.let {
             val primaryKeyColumns = it.value.primaryKeys
             if (primaryKeyColumns.isEmpty()) {
                 emptyList()
@@ -402,7 +402,7 @@
         embeddedFields: List<EmbeddedField>
     ): List<PrimaryKey> {
         return embeddedFields.mapNotNull { embeddedField ->
-            embeddedField.field.element.toAnnotationBox(androidx.room.PrimaryKey::class)?.let {
+            embeddedField.field.element.getAnnotation(androidx.room.PrimaryKey::class)?.let {
                 context.checker.check(
                     !it.value.autoGenerate || embeddedField.pojo.fields.size == 1,
                     embeddedField.field.element,
@@ -494,7 +494,7 @@
         // see if any embedded field is an entity with indices, if so, report a warning
         pojo.embeddedFields.forEach { embedded ->
             val embeddedElement = embedded.pojo.element
-            embeddedElement.toAnnotationBox(androidx.room.Entity::class)?.let {
+            embeddedElement.getAnnotation(androidx.room.Entity::class)?.let {
                 val subIndices = extractIndices(it, "")
                 if (subIndices.isNotEmpty()) {
                     context.logger.w(
@@ -528,7 +528,7 @@
             return emptyList()
         }
         val myIndices = parentTypeElement
-            .toAnnotationBox(androidx.room.Entity::class)?.let { annotation ->
+            .getAnnotation(androidx.room.Entity::class)?.let { annotation ->
                 val indices = extractIndices(annotation, tableName = "super")
                 if (indices.isEmpty()) {
                     emptyList()
diff --git a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
index a0a327d..53f1f231 100644
--- a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
@@ -17,6 +17,9 @@
 package androidx.room.util
 
 import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.ForeignKeyBundle
+import androidx.room.migration.bundle.IndexBundle
 import androidx.room.processor.ProcessorErrors
 import androidx.room.vo.AutoMigrationResult
 
@@ -29,11 +32,12 @@
  * Contains the added, changed and removed columns detected.
  */
 data class SchemaDiffResult(
-    val addedColumn: MutableList<AutoMigrationResult.AddedColumn>,
-    val changedColumn: List<AutoMigrationResult.ChangedColumn>,
-    val removedColumn: List<AutoMigrationResult.RemovedColumn>,
-    val addedTable: List<AutoMigrationResult.AddedTable>,
-    val removedTable: List<AutoMigrationResult.RemovedTable>
+    val addedColumns: List<AutoMigrationResult.AddedColumn>,
+    val changedColumns: List<AutoMigrationResult.ChangedColumn>,
+    val removedColumns: List<AutoMigrationResult.RemovedColumn>,
+    val addedTables: List<AutoMigrationResult.AddedTable>,
+    val complexChangedTables: List<AutoMigrationResult.ComplexChangedTable>,
+    val removedTables: List<AutoMigrationResult.RemovedTable>
 )
 
 /**
@@ -55,6 +59,7 @@
      */
     fun diffSchemas(): SchemaDiffResult {
         val addedTables = mutableListOf<AutoMigrationResult.AddedTable>()
+        val complexChangedTables = mutableListOf<AutoMigrationResult.ComplexChangedTable>()
         val removedTables = mutableListOf<AutoMigrationResult.RemovedTable>()
 
         val addedColumns = mutableListOf<AutoMigrationResult.AddedColumn>()
@@ -63,28 +68,32 @@
 
         // Check going from the original version of the schema to the new version for changed and
         // removed columns/tables
-        fromSchemaBundle.entitiesByTableName.forEach { v1Table ->
-            val v2Table = toSchemaBundle.entitiesByTableName[v1Table.key]
-            if (v2Table == null) {
-                removedTables.add(AutoMigrationResult.RemovedTable(v1Table.value))
+        fromSchemaBundle.entitiesByTableName.forEach { fromTable ->
+            val toTable = toSchemaBundle.entitiesByTableName[fromTable.key]
+            if (toTable == null) {
+                removedTables.add(AutoMigrationResult.RemovedTable(fromTable.value))
             } else {
-                val v1Columns = v1Table.value.fieldsByColumnName
-                val v2Columns = v2Table.fieldsByColumnName
-                v1Columns.entries.forEach { v1Column ->
-                    val match = v2Columns[v1Column.key]
-                    if (match != null && !match.isSchemaEqual(v1Column.value)) {
+                val complexChangedTable = tableContainsComplexChanges(fromTable.value, toTable)
+                if (complexChangedTable != null) {
+                    complexChangedTables.add(complexChangedTable)
+                }
+                val fromColumns = fromTable.value.fieldsByColumnName
+                val toColumns = toTable.fieldsByColumnName
+                fromColumns.entries.forEach { fromColumn ->
+                    val match = toColumns[fromColumn.key]
+                    if (match != null && !match.isSchemaEqual(fromColumn.value)) {
+                        // Any change in the field bundle schema of a column will be complex
                         changedColumns.add(
                             AutoMigrationResult.ChangedColumn(
-                                v1Table.key,
-                                v1Column.value,
-                                match
+                                fromTable.key,
+                                fromColumn.value
                             )
                         )
                     } else if (match == null) {
                         removedColumns.add(
                             AutoMigrationResult.RemovedColumn(
-                                v1Table.key,
-                                v1Column.value
+                                fromTable.key,
+                                fromColumn.value
                             )
                         )
                     }
@@ -94,25 +103,25 @@
         // Check going from the new version of the schema to the original version for added
         // tables/columns. Skip the columns with the same name as the previous loop would have
         // processed them already.
-        toSchemaBundle.entitiesByTableName.forEach { v2Table ->
-            val v1Table = fromSchemaBundle.entitiesByTableName[v2Table.key]
-            if (v1Table == null) {
-                addedTables.add(AutoMigrationResult.AddedTable(v2Table.value))
+        toSchemaBundle.entitiesByTableName.forEach { toTable ->
+            val fromTable = fromSchemaBundle.entitiesByTableName[toTable.key]
+            if (fromTable == null) {
+                addedTables.add(AutoMigrationResult.AddedTable(toTable.value))
             } else {
-                val v2Columns = v2Table.value.fieldsByColumnName
-                val v1Columns = v1Table.fieldsByColumnName
-                v2Columns.entries.forEach { v2Column ->
-                    val match = v1Columns[v2Column.key]
+                val fromColumns = fromTable.fieldsByColumnName
+                val toColumns = toTable.value.fieldsByColumnName
+                toColumns.entries.forEach { toColumn ->
+                    val match = fromColumns[toColumn.key]
                     if (match == null) {
-                        if (v2Column.value.isNonNull && v2Column.value.defaultValue == null) {
+                        if (toColumn.value.isNonNull && toColumn.value.defaultValue == null) {
                             diffError(
-                                ProcessorErrors.newNotNullColumnMustHaveDefaultValue(v2Column.key)
+                                ProcessorErrors.newNotNullColumnMustHaveDefaultValue(toColumn.key)
                             )
                         }
                         addedColumns.add(
                             AutoMigrationResult.AddedColumn(
-                                v2Table.key,
-                                v2Column.value
+                                toTable.key,
+                                toColumn.value
                             )
                         )
                     }
@@ -120,11 +129,13 @@
             }
         }
 
+        // TODO: (b/183007590) Remove the Processor Errors thrown when a complex change is
+        //  encountered after AutoMigrationWriter supports generating the necessary migrations.
         if (changedColumns.isNotEmpty()) {
             changedColumns.forEach { changedColumn ->
                 diffError(
                     ProcessorErrors.columnWithChangedSchemaFound(
-                        changedColumn.originalFieldBundle.columnName
+                        changedColumn.fieldBundle.columnName
                     )
                 )
             }
@@ -140,6 +151,16 @@
             }
         }
 
+        if (complexChangedTables.isNotEmpty()) {
+            complexChangedTables.forEach { changedTable ->
+                diffError(
+                    ProcessorErrors.tableWithComplexChangedSchemaFound(
+                        changedTable.tableName
+                    )
+                )
+            }
+        }
+
         if (removedTables.isNotEmpty()) {
             removedTables.forEach { removedTable ->
                 diffError(
@@ -151,15 +172,98 @@
         }
 
         return SchemaDiffResult(
-            addedColumn = addedColumns,
-            changedColumn = changedColumns,
-            removedColumn = removedColumns,
-            addedTable = addedTables,
-            removedTable = removedTables
+            addedColumns = addedColumns,
+            changedColumns = changedColumns,
+            removedColumns = removedColumns,
+            addedTables = addedTables,
+            complexChangedTables = complexChangedTables,
+            removedTables = removedTables
         )
     }
 
+    /**
+     * Check for complex schema changes at a Table level and returns a ComplexTableChange
+     * including information on which table changes were found on, and whether foreign key or
+     * index related changes have occurred.
+     *
+     * @return null if complex schema change has not been found
+     */
+    // TODO: (b/181777611) Handle FTS tables
+    private fun tableContainsComplexChanges(
+        fromTable: EntityBundle,
+        toTable: EntityBundle
+    ): AutoMigrationResult.ComplexChangedTable? {
+        val foreignKeyChanged = !isForeignKeyBundlesListEqual(
+            fromTable.foreignKeys,
+            toTable.foreignKeys
+        )
+        val indexChanged = !isIndexBundlesListEqual(fromTable.indices, toTable.indices)
+        val primaryKeyChanged = !fromTable.primaryKey.isSchemaEqual(toTable.primaryKey)
+
+        if (primaryKeyChanged || foreignKeyChanged || indexChanged) {
+            return AutoMigrationResult.ComplexChangedTable(
+                tableName = toTable.tableName,
+                foreignKeyChanged = foreignKeyChanged,
+                indexChanged = indexChanged
+            )
+        }
+        return null
+    }
+
     private fun diffError(errorMsg: String) {
         throw DiffException(errorMsg)
     }
+
+    /**
+     * Takes in two ForeignKeyBundle lists, attempts to find potential matches based on the columns
+     * of the Foreign Keys. Processes these potential matches by checking for schema equality.
+     *
+     * @return true if the two lists of foreign keys are equal
+     */
+    private fun isForeignKeyBundlesListEqual(
+        fromBundle: List<ForeignKeyBundle>,
+        toBundle: List<ForeignKeyBundle>
+    ): Boolean {
+        val set = fromBundle + toBundle
+        val matches = set.groupBy { it.columns }.entries
+
+        matches.forEach { (_, bundles) ->
+            if (bundles.size < 2) {
+                // A bundle was not matched at all, there must be a change between two versions
+                return false
+            }
+            val fromForeignKeyBundle = bundles[0]
+            val toForeignKeyBundle = bundles[1]
+            if (!fromForeignKeyBundle.isSchemaEqual(toForeignKeyBundle)) {
+                // A potential match for a bundle was found, but schemas did not match
+                return false
+            }
+        }
+        return true
+    }
+
+    /**
+     * Takes in two IndexBundle lists, attempts to find potential matches based on the names
+     * of the indexes. Processes these potential matches by checking for schema equality.
+     *
+     * @return true if the two lists of indexes are equal
+     */
+    private fun isIndexBundlesListEqual(
+        fromBundle: List<IndexBundle>,
+        toBundle: List<IndexBundle>
+    ): Boolean {
+        val set = fromBundle + toBundle
+        val matches = set.groupBy { it.name }.entries
+
+        matches.forEach { bundlesWithSameName ->
+            if (bundlesWithSameName.value.size < 2) {
+                // A bundle was not matched at all, there must be a change between two versions
+                return false
+            } else if (!bundlesWithSameName.value[0].isSchemaEqual(bundlesWithSameName.value[1])) {
+                // A potential match for a bundle was found, but schemas did not match
+                return false
+            }
+        }
+        return true
+    }
 }
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
index 91d4bd6..f6fcc1a 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
@@ -49,8 +49,7 @@
      */
     data class ChangedColumn(
         val tableName: String,
-        val originalFieldBundle: FieldBundle,
-        val newFieldBundle: FieldBundle
+        val fieldBundle: FieldBundle
     )
 
     /**
@@ -69,6 +68,25 @@
     data class AddedTable(val entityBundle: EntityBundle)
 
     /**
+     * Stores the table name that contains a change in the primary key, foreign key(s) or index(es)
+     * in a newer version. Explicitly provides information on whether a foreign key change and/or
+     * an index change has occurred.
+     *
+     * As it is possible to have a table with only simple (non-complex) changes, which will be
+     * categorized as "AddedColumn" or "RemovedColumn" changes, all other
+     * changes at the table level are categorized as "complex" changes, using the category
+     * "ComplexChangedTable".
+     *
+     * At the column level, any change that is not a column add or a
+     * removal will be categorized as "ChangedColumn".
+     */
+    data class ComplexChangedTable(
+        val tableName: String,
+        val foreignKeyChanged: Boolean,
+        val indexChanged: Boolean
+    )
+
+    /**
      * Stores the table that was present in the old version of a database but is not present in a
      * new version of the same database, either because it was removed or renamed.
      *
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt
index 3922f26..b463a7b 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt
@@ -42,11 +42,11 @@
      * Might be via Embedded or Relation.
      */
     fun accessedTableNames(): List<String> {
-        val entityAnnotation = element.toAnnotationBox(androidx.room.Entity::class)
+        val entityAnnotation = element.getAnnotation(androidx.room.Entity::class)
         return if (entityAnnotation != null) {
             listOf(EntityProcessor.extractTableName(element, entityAnnotation.value))
         } else {
-            val viewAnnotation = element.toAnnotationBox(androidx.room.DatabaseView::class)
+            val viewAnnotation = element.getAnnotation(androidx.room.DatabaseView::class)
             if (viewAnnotation != null) {
                 listOf(DatabaseViewProcessor.extractViewName(element, viewAnnotation.value))
             } else {
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt
index c442c6f..7e79cd0 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt
@@ -68,7 +68,7 @@
         ) { invocation ->
             val entity = invocation.roundEnv
                 .getTypeElementsAnnotatedWith(
-                    androidx.room.Entity::class.java
+                    androidx.room.Entity::class.qualifiedName!!
                 ).first {
                     it.qualifiedName == "foo.bar.MyEntity"
                 }
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/BaseFtsEntityParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/BaseFtsEntityParserTest.kt
index 8e83ba6..31f63f3 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/BaseFtsEntityParserTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/BaseFtsEntityParserTest.kt
@@ -99,9 +99,9 @@
                     )
                     .nextRunHandler { invocation ->
                         val fts3AnnotatedElements = invocation.roundEnv
-                            .getTypeElementsAnnotatedWith(Fts3::class.java)
+                            .getTypeElementsAnnotatedWith(Fts3::class.qualifiedName!!)
                         val fts4AnnotatedElements = invocation.roundEnv
-                            .getTypeElementsAnnotatedWith(Fts4::class.java)
+                            .getTypeElementsAnnotatedWith(Fts4::class.qualifiedName!!)
                         val entity = (fts3AnnotatedElements + fts4AnnotatedElements).first {
                             it.toString() == "foo.bar.MyEntity"
                         }
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
index d857001..081f84f 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
@@ -427,7 +427,7 @@
         ) { invocation: XTestInvocation ->
             val dao = invocation.roundEnv
                 .getTypeElementsAnnotatedWith(
-                    androidx.room.Dao::class.java
+                    androidx.room.Dao::class.qualifiedName!!
                 )
                 .first()
             check(dao.isTypeElement())
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
index 6902a61..1e72450 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
@@ -1164,7 +1164,7 @@
                     .nextRunHandler { invocation ->
                         val database = invocation.roundEnv
                             .getTypeElementsAnnotatedWith(
-                                androidx.room.Database::class.java
+                                androidx.room.Database::class.qualifiedName!!
                             )
                             .first()
                         val processor = DatabaseProcessor(
@@ -1237,7 +1237,7 @@
                     .nextRunHandler { invocation ->
                         val entity = invocation.roundEnv
                             .getTypeElementsAnnotatedWith(
-                                androidx.room.Database::class.java
+                                androidx.room.Database::class.qualifiedName!!
                             )
                             .first()
                         val parser = DatabaseProcessor(
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt
index 4531a61..9a7fcb2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt
@@ -602,7 +602,7 @@
                         .forAnnotations(androidx.room.Entity::class)
                         .nextRunHandler { invocation ->
                             val (owner, fieldElement) = invocation.roundEnv
-                                .getTypeElementsAnnotatedWith(Entity::class.java)
+                                .getTypeElementsAnnotatedWith(Entity::class.qualifiedName!!)
                                 .map {
                                     Pair(
                                         it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
index b8ee82f..92b679c 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
@@ -89,7 +89,8 @@
                 """
             )
         ) { invocation ->
-            val entity = invocation.roundEnv.getTypeElementsAnnotatedWith(Fts4::class.java)
+            val entity = invocation.roundEnv
+                .getTypeElementsAnnotatedWith(Fts4::class.qualifiedName!!)
                 .first { it.toString() == "foo.bar.MyEntity" }
             FtsTableEntityProcessor(invocation.context, entity)
                 .process()
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
index 9f5d417..a0767a7 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
@@ -863,7 +863,7 @@
                     .forAnnotations(Insert::class, Dao::class)
                     .nextRunHandler { invocation ->
                         val (owner, methods) = invocation.roundEnv
-                            .getTypeElementsAnnotatedWith(Dao::class.java)
+                            .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
                             .map {
                                 Pair(
                                     it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/ProjectionExpanderTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/ProjectionExpanderTest.kt
index 3d03007..d086818 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/ProjectionExpanderTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/ProjectionExpanderTest.kt
@@ -520,7 +520,7 @@
             options = listOf("-Aroom.expandProjection=true")
         ) { invocation ->
             val entities = invocation.roundEnv
-                .getTypeElementsAnnotatedWith(androidx.room.Entity::class.java)
+                .getTypeElementsAnnotatedWith(androidx.room.Entity::class.qualifiedName!!)
                 .map { element ->
                     TableEntityProcessor(
                         invocation.context,
@@ -623,7 +623,7 @@
             options = listOf("-Aroom.expandProjection=true")
         ) { invocation ->
             val entities = invocation.roundEnv
-                .getTypeElementsAnnotatedWith(androidx.room.Entity::class.java)
+                .getTypeElementsAnnotatedWith(androidx.room.Entity::class.qualifiedName!!)
                 .map { element ->
                     TableEntityProcessor(
                         invocation.context,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index 350c1ad..676bf07 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -1166,7 +1166,7 @@
                     )
                     .nextRunHandler { invocation ->
                         val (owner, methods) = invocation.roundEnv
-                            .getTypeElementsAnnotatedWith(Dao::class.java)
+                            .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
                             .map {
                                 Pair(
                                     it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
index e6b825b..6de8d49 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -343,7 +343,7 @@
                     )
                     .nextRunHandler { invocation ->
                         val (owner, methods) = invocation.roundEnv
-                            .getTypeElementsAnnotatedWith(Dao::class.java)
+                            .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
                             .map {
                                 Pair(
                                     it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
index 1810560..7c226d3 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
@@ -536,7 +536,7 @@
                         .forAnnotations(annotation, Dao::class)
                         .nextRunHandler { invocation ->
                             val (owner, methods) = invocation.roundEnv
-                                .getTypeElementsAnnotatedWith(Dao::class.java)
+                                .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
                                 .map {
                                     Pair(
                                         it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
index 8fad90d..988a598 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
@@ -251,7 +251,7 @@
                     .forAnnotations(Transaction::class, Dao::class)
                     .nextRunHandler { invocation ->
                         val (owner, methods) = invocation.roundEnv
-                            .getTypeElementsAnnotatedWith(Dao::class.java)
+                            .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
                             .map {
                                 Pair(
                                     it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
index cbb67cb..8976245 100644
--- a/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
@@ -354,7 +354,7 @@
                     .forAnnotations(Query::class, Dao::class)
                     .nextRunHandler { invocation ->
                         val (owner, methods) = invocation.roundEnv
-                            .getTypeElementsAnnotatedWith(Dao::class.java)
+                            .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
                             .map {
                                 Pair(
                                     it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/XProcessingStepExt.kt b/room/compiler/src/test/kotlin/androidx/room/testing/XProcessingStepExt.kt
index 669f928..ddee174 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/XProcessingStepExt.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/XProcessingStepExt.kt
@@ -27,7 +27,7 @@
     delegate: (XTestInvocation) -> Unit
 ): (XTestInvocation) -> Unit = { invocation ->
     val elementsByAnnotation = annotations().associateWith {
-        invocation.roundEnv.getTypeElementsAnnotatedWith(it.java).toList()
+        invocation.roundEnv.getTypeElementsAnnotatedWith(it).toList()
     }
     this.process(
         env = invocation.processingEnv,
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index d38af3f..d3fd8b2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -278,26 +278,26 @@
 }
 
 fun XTestInvocation.getViews(): List<androidx.room.vo.DatabaseView> {
-    return roundEnv.getTypeElementsAnnotatedWith(DatabaseView::class.java).map {
+    return roundEnv.getTypeElementsAnnotatedWith(DatabaseView::class.qualifiedName!!).map {
         DatabaseViewProcessor(context, it).process()
     }
 }
 
 fun XTestInvocation.getEntities(): List<androidx.room.vo.Entity> {
-    val entities = roundEnv.getTypeElementsAnnotatedWith(Entity::class.java).map {
+    val entities = roundEnv.getTypeElementsAnnotatedWith(Entity::class.qualifiedName!!).map {
         TableEntityProcessor(context, it).process()
     }
     return entities
 }
 
 fun TestInvocation.getViews(): List<androidx.room.vo.DatabaseView> {
-    return roundEnv.getTypeElementsAnnotatedWith(DatabaseView::class.java).map {
+    return roundEnv.getTypeElementsAnnotatedWith(DatabaseView::class.qualifiedName!!).map {
         DatabaseViewProcessor(context, it).process()
     }
 }
 
 fun TestInvocation.getEntities(): List<androidx.room.vo.Entity> {
-    val entities = roundEnv.getTypeElementsAnnotatedWith(Entity::class.java).map {
+    val entities = roundEnv.getTypeElementsAnnotatedWith(Entity::class.qualifiedName!!).map {
         TableEntityProcessor(context, it).process()
     }
     return entities
diff --git a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
index 0dd90dc..94ed63a 100644
--- a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
@@ -19,6 +19,8 @@
 import androidx.room.migration.bundle.DatabaseBundle
 import androidx.room.migration.bundle.EntityBundle
 import androidx.room.migration.bundle.FieldBundle
+import androidx.room.migration.bundle.ForeignKeyBundle
+import androidx.room.migration.bundle.IndexBundle
 import androidx.room.migration.bundle.PrimaryKeyBundle
 import androidx.room.migration.bundle.SchemaBundle
 import androidx.room.processor.ProcessorErrors
@@ -29,12 +31,57 @@
 class SchemaDifferTest {
 
     @Test
+    fun testPrimaryKeyChanged() {
+        try {
+            SchemaDiffer(
+                fromSchemaBundle = from.database,
+                toSchemaBundle = toChangeInPrimaryKey.database
+            ).diffSchemas()
+            fail("DiffException should have been thrown.")
+        } catch (ex: DiffException) {
+            assertThat(ex.errorMessage).isEqualTo(
+                ProcessorErrors.tableWithComplexChangedSchemaFound("Song")
+            )
+        }
+    }
+
+    @Test
+    fun testForeignKeyFieldChanged() {
+        try {
+            SchemaDiffer(
+                fromSchemaBundle = from.database,
+                toSchemaBundle = toForeignKeyAdded.database
+            ).diffSchemas()
+            fail("DiffException should have been thrown.")
+        } catch (ex: DiffException) {
+            assertThat(ex.errorMessage).isEqualTo(
+                ProcessorErrors.tableWithComplexChangedSchemaFound("Song")
+            )
+        }
+    }
+
+    @Test
+    fun testComplexChangeInvolvingIndex() {
+        try {
+            SchemaDiffer(
+                fromSchemaBundle = from.database,
+                toSchemaBundle = toIndexAdded.database
+            ).diffSchemas()
+            fail("DiffException should have been thrown.")
+        } catch (ex: DiffException) {
+            assertThat(ex.errorMessage).isEqualTo(
+                ProcessorErrors.tableWithComplexChangedSchemaFound("Song")
+            )
+        }
+    }
+
+    @Test
     fun testColumnAddedWithColumnInfoDefaultValue() {
         val schemaDiffResult = SchemaDiffer(
             fromSchemaBundle = from.database,
             toSchemaBundle = toColumnAddedWithColumnInfoDefaultValue.database
         ).diffSchemas()
-        assertThat(schemaDiffResult.addedColumn[0].fieldBundle.columnName).isEqualTo("artistId")
+        assertThat(schemaDiffResult.addedColumns[0].fieldBundle.columnName).isEqualTo("artistId")
     }
 
     @Test
@@ -58,8 +105,8 @@
             fromSchemaBundle = from.database,
             toSchemaBundle = toTableAddedWithColumnInfoDefaultValue.database
         ).diffSchemas()
-        assertThat(schemaDiffResult.addedTable[0].entityBundle.tableName).isEqualTo("Artist")
-        assertThat(schemaDiffResult.addedTable[1].entityBundle.tableName).isEqualTo("Album")
+        assertThat(schemaDiffResult.addedTables[0].entityBundle.tableName).isEqualTo("Artist")
+        assertThat(schemaDiffResult.addedTables[1].entityBundle.tableName).isEqualTo("Album")
     }
 
     @Test
@@ -78,7 +125,7 @@
     }
 
     @Test
-    fun testColumnAffinityChanged() {
+    fun testColumnFieldBundleChanged() {
         try {
             SchemaDiffer(
                 fromSchemaBundle = from.database,
@@ -496,10 +543,74 @@
         )
     )
 
-    val toTableAddedWithNoDefaultValue = SchemaBundle(
-        1,
+    private val toForeignKeyAdded = SchemaBundle(
+        2,
         DatabaseBundle(
-            1,
+            2,
+            "",
+            listOf(
+                EntityBundle(
+                    "Song",
+                    "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+                        "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, `artistId` " +
+                        "INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`), FOREIGN KEY(`title`) " +
+                        "REFERENCES `Song`(`artistId`) ON UPDATE NO ACTION ON DELETE NO " +
+                        "ACTION DEFERRABLE INITIALLY DEFERRED))",
+                    listOf(
+                        FieldBundle(
+                            "id",
+                            "id",
+                            "INTEGER",
+                            true,
+                            "1"
+                        ),
+                        FieldBundle(
+                            "title",
+                            "title",
+                            "TEXT",
+                            true,
+                            ""
+                        ),
+                        FieldBundle(
+                            "length",
+                            "length",
+                            "INTEGER",
+                            true,
+                            "1"
+                        ),
+                        FieldBundle(
+                            "artistId",
+                            "artistId",
+                            "INTEGER",
+                            true,
+                            "0"
+                        )
+                    ),
+                    PrimaryKeyBundle(
+                        false,
+                        mutableListOf("id")
+                    ),
+                    emptyList(),
+                    listOf(
+                        ForeignKeyBundle(
+                            "Song",
+                            "onDelete",
+                            "onUpdate",
+                            listOf("title"),
+                            listOf("artistId")
+                        )
+                    )
+                )
+            ),
+            mutableListOf(),
+            mutableListOf()
+        )
+    )
+
+    val toIndexAdded = SchemaBundle(
+        2,
+        DatabaseBundle(
+            2,
             "",
             mutableListOf(
                 EntityBundle(
@@ -533,25 +644,62 @@
                         false,
                         mutableListOf("id")
                     ),
-                    mutableListOf(),
-                    mutableListOf()
-                ),
-                EntityBundle(
-                    "Album",
-                    "CREATE TABLE IF NOT EXISTS `Album` (`id` INTEGER NOT NULL, `name` TEXT NOT " +
-                        "NULL, PRIMARY KEY(`id`))",
                     listOf(
-                        FieldBundle(
-                            "albumId",
-                            "albumId",
-                            "INTEGER",
+                        IndexBundle(
+                            "index1",
                             true,
-                            null
+                            listOf("title"),
+                            "CREATE UNIQUE INDEX IF NOT EXISTS `index1` ON `Song`" +
+                                "(`title`)"
                         )
                     ),
-                    PrimaryKeyBundle(true, listOf("id")),
-                    listOf(),
-                    listOf()
+                    mutableListOf()
+                )
+            ),
+            mutableListOf(),
+            mutableListOf()
+        )
+    )
+
+    val toChangeInPrimaryKey = SchemaBundle(
+        2,
+        DatabaseBundle(
+            2,
+            "",
+            mutableListOf(
+                EntityBundle(
+                    "Song",
+                    "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+                        "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, PRIMARY KEY(`title`))",
+                    listOf(
+                        FieldBundle(
+                            "id",
+                            "id",
+                            "INTEGER",
+                            true,
+                            "1"
+                        ),
+                        FieldBundle(
+                            "title",
+                            "title",
+                            "TEXT",
+                            true,
+                            ""
+                        ),
+                        FieldBundle(
+                            "length",
+                            "length",
+                            "INTEGER",
+                            true,
+                            "1"
+                        )
+                    ),
+                    PrimaryKeyBundle(
+                        false,
+                        mutableListOf("title")
+                    ),
+                    mutableListOf(),
+                    mutableListOf()
                 )
             ),
             mutableListOf(),
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
index cda50d8..3882bd2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
@@ -140,12 +140,12 @@
         ) { invocation ->
             val dao = invocation.roundEnv
                 .getTypeElementsAnnotatedWith(
-                    androidx.room.Dao::class.java
+                    androidx.room.Dao::class.qualifiedName!!
                 ).firstOrNull()
             if (dao != null) {
                 val db = invocation.roundEnv
                     .getTypeElementsAnnotatedWith(
-                        androidx.room.Database::class.java
+                        androidx.room.Database::class.qualifiedName!!
                     ).firstOrNull()
                     ?: invocation.context.processingEnv
                         .requireTypeElement(RoomTypeNames.ROOM_DB)
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
index fc1c911..8b1a973 100644
--- a/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
@@ -214,7 +214,7 @@
             sources = sources + databaseCode
         ) { invocation ->
             val db = invocation.roundEnv
-                .getTypeElementsAnnotatedWith(androidx.room.Database::class.java)
+                .getTypeElementsAnnotatedWith(androidx.room.Database::class.qualifiedName!!)
                 .first()
             handler(DatabaseProcessor(invocation.context, db).process(), invocation)
         }
diff --git a/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index a72e89b..f45d8e5 100644
--- a/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -23,7 +23,9 @@
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emitAll
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
@@ -102,29 +104,33 @@
             tableNames: Array<String>,
             callable: Callable<R>
         ): Flow<@JvmSuppressWildcards R> = flow {
-            // Observer channel receives signals from the invalidation tracker to emit queries.
-            val observerChannel = Channel<Unit>(Channel.CONFLATED)
-            val observer = object : InvalidationTracker.Observer(tableNames) {
-                override fun onInvalidated(tables: MutableSet<String>) {
-                    observerChannel.offer(Unit)
-                }
-            }
-            observerChannel.offer(Unit) // Initial signal to perform first query.
-            val flowContext = coroutineContext
-            val queryContext = coroutineContext[TransactionElement]?.transactionDispatcher
-                ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
-            withContext(queryContext) {
-                db.invalidationTracker.addObserver(observer)
-                try {
-                    // Iterate until cancelled, transforming observer signals to query results to
-                    // be emitted to the flow.
-                    for (signal in observerChannel) {
-                        val result = callable.call()
-                        withContext(flowContext) { emit(result) }
+            coroutineScope {
+                // Observer channel receives signals from the invalidation tracker to emit queries.
+                val observerChannel = Channel<Unit>(Channel.CONFLATED)
+                val observer = object : InvalidationTracker.Observer(tableNames) {
+                    override fun onInvalidated(tables: MutableSet<String>) {
+                        observerChannel.offer(Unit)
                     }
-                } finally {
-                    db.invalidationTracker.removeObserver(observer)
                 }
+                observerChannel.offer(Unit) // Initial signal to perform first query.
+                val queryContext = coroutineContext[TransactionElement]?.transactionDispatcher
+                    ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
+                val resultChannel = Channel<R>()
+                launch(queryContext) {
+                    db.invalidationTracker.addObserver(observer)
+                    try {
+                        // Iterate until cancelled, transforming observer signals to query results
+                        // to be emitted to the flow.
+                        for (signal in observerChannel) {
+                            val result = callable.call()
+                            resultChannel.send(result)
+                        }
+                    } finally {
+                        db.invalidationTracker.removeObserver(observer)
+                    }
+                }
+
+                emitAll(resultChannel)
             }
         }
     }
diff --git a/room/ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt b/room/ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
index 086b217..28c8f23 100644
--- a/room/ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
+++ b/room/ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
@@ -20,8 +20,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.flow.single
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.yield
 import org.junit.Test
@@ -39,25 +38,29 @@
     @Test
     fun testCreateFlow() = testRun {
         var callableExecuted = false
+        val expectedResult = Any()
         val flow = CoroutinesRoom.createFlow(
             db = database,
             inTransaction = false,
             tableNames = arrayOf("Pet"),
-            callable = Callable { callableExecuted = true }
+            callable = Callable {
+                callableExecuted = true
+                expectedResult
+            }
         )
 
         assertThat(invalidationTracker.observers.isEmpty()).isTrue()
         assertThat(callableExecuted).isFalse()
 
         val job = async {
-            flow.single()
+            flow.first()
         }
         yield(); yield() // yield for async and flow
 
         assertThat(invalidationTracker.observers.size).isEqualTo(1)
         assertThat(callableExecuted).isTrue()
 
-        job.cancelAndJoin()
+        assertThat(job.await()).isEqualTo(expectedResult)
         assertThat(invalidationTracker.observers.isEmpty()).isTrue()
     }
 
diff --git a/settings.gradle b/settings.gradle
index 3fe280c..9bc7f250 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -288,6 +288,7 @@
 includeProject(":compose:runtime:runtime-rxjava3", "compose/runtime/runtime-rxjava3", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime-rxjava3:runtime-rxjava3-samples", "compose/runtime/runtime-rxjava3/samples", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime-saveable", "compose/runtime/runtime-saveable", [BuildType.COMPOSE])
+includeProject(":compose:runtime:runtime-saveable-lint", "compose/runtime/runtime-saveable-lint", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime-saveable:runtime-saveable-samples", "compose/runtime/runtime-saveable/samples", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime:benchmark", "compose/runtime/runtime/compose-runtime-benchmark", [BuildType.COMPOSE])
 includeProject(":compose:runtime:runtime:integration-tests", "compose/runtime/runtime/integration-tests", [BuildType.COMPOSE])
diff --git a/startup/integration-tests/first-library/src/main/AndroidManifest.xml b/startup/integration-tests/first-library/src/main/AndroidManifest.xml
index dc4bfdc..b5f96d8 100644
--- a/startup/integration-tests/first-library/src/main/AndroidManifest.xml
+++ b/startup/integration-tests/first-library/src/main/AndroidManifest.xml
@@ -32,7 +32,7 @@
 
             <!-- Remove initializer for WorkManager on-demand initialization -->
             <meta-data
-                android:name="androidx.work.impl.WorkManagerInitializer"
+                android:name="androidx.work.WorkManagerInitializer"
                 android:value="@string/androidx_startup"
                 tools:node="remove" />
         </provider>
diff --git a/startup/startup-runtime/api/current.txt b/startup/startup-runtime/api/current.txt
index 847659c..434ba26 100644
--- a/startup/startup-runtime/api/current.txt
+++ b/startup/startup-runtime/api/current.txt
@@ -7,6 +7,16 @@
     method public boolean isEagerlyInitialized(Class<? extends androidx.startup.Initializer<?>>);
   }
 
+  public class InitializationProvider extends android.content.ContentProvider {
+    ctor public InitializationProvider();
+    method public final int delete(android.net.Uri, String?, String![]?);
+    method public final String? getType(android.net.Uri);
+    method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public final boolean onCreate();
+    method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public interface Initializer<T> {
     method public T create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
diff --git a/startup/startup-runtime/api/public_plus_experimental_current.txt b/startup/startup-runtime/api/public_plus_experimental_current.txt
index 847659c..434ba26 100644
--- a/startup/startup-runtime/api/public_plus_experimental_current.txt
+++ b/startup/startup-runtime/api/public_plus_experimental_current.txt
@@ -7,6 +7,16 @@
     method public boolean isEagerlyInitialized(Class<? extends androidx.startup.Initializer<?>>);
   }
 
+  public class InitializationProvider extends android.content.ContentProvider {
+    ctor public InitializationProvider();
+    method public final int delete(android.net.Uri, String?, String![]?);
+    method public final String? getType(android.net.Uri);
+    method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public final boolean onCreate();
+    method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public interface Initializer<T> {
     method public T create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
diff --git a/startup/startup-runtime/api/restricted_current.txt b/startup/startup-runtime/api/restricted_current.txt
index 847659c..434ba26 100644
--- a/startup/startup-runtime/api/restricted_current.txt
+++ b/startup/startup-runtime/api/restricted_current.txt
@@ -7,6 +7,16 @@
     method public boolean isEagerlyInitialized(Class<? extends androidx.startup.Initializer<?>>);
   }
 
+  public class InitializationProvider extends android.content.ContentProvider {
+    ctor public InitializationProvider();
+    method public final int delete(android.net.Uri, String?, String![]?);
+    method public final String? getType(android.net.Uri);
+    method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+    method public final boolean onCreate();
+    method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+    method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+  }
+
   public interface Initializer<T> {
     method public T create(android.content.Context);
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
diff --git a/startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java b/startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java
index 4d07b2a..a14ce54 100644
--- a/startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java
+++ b/startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java
@@ -25,18 +25,14 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 
 /**
  * The {@link ContentProvider} which discovers {@link Initializer}s in an application and
  * initializes them before {@link Application#onCreate()}.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public final class InitializationProvider extends ContentProvider {
+public class InitializationProvider extends ContentProvider {
     @Override
-    public boolean onCreate() {
+    public final boolean onCreate() {
         Context context = getContext();
         if (context != null) {
             AppInitializer.getInstance(context).discoverAndInitialize();
@@ -48,7 +44,7 @@
 
     @Nullable
     @Override
-    public Cursor query(
+    public final Cursor query(
             @NonNull Uri uri,
             @Nullable String[] projection,
             @Nullable String selection,
@@ -59,18 +55,18 @@
 
     @Nullable
     @Override
-    public String getType(@NonNull Uri uri) {
+    public final String getType(@NonNull Uri uri) {
         throw new IllegalStateException("Not allowed.");
     }
 
     @Nullable
     @Override
-    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+    public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
         throw new IllegalStateException("Not allowed.");
     }
 
     @Override
-    public int delete(
+    public final int delete(
             @NonNull Uri uri,
             @Nullable String selection,
             @Nullable String[] selectionArgs) {
@@ -78,7 +74,7 @@
     }
 
     @Override
-    public int update(
+    public final int update(
             @NonNull Uri uri,
             @Nullable ContentValues values,
             @Nullable String selection,
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutIntrinsicWidthTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutIntrinsicWidthTest.kt
new file mode 100644
index 0000000..3d26c96
--- /dev/null
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutIntrinsicWidthTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.android
+
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE
+import android.text.TextPaint
+import androidx.compose.ui.text.android.style.LetterSpacingSpanEm
+import androidx.compose.ui.text.android.style.LetterSpacingSpanPx
+import androidx.compose.ui.text.android.style.LineHeightSpan
+import androidx.compose.ui.text.font.test.R
+import androidx.core.content.res.ResourcesCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(InternalPlatformTextApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TextLayoutIntrinsicWidthTest {
+    private val defaultText = SpannableString("This is a callout message")
+
+    // values are exact values for the repro case (on Pixel4, Android 11)
+    private val fontScale = 1.15f
+    private val density = 3.051f
+    private val letterSpacingEm = 0.4f / 12f
+    private val fontSize = 12f.spToPx()
+    private val letterSpacingPx = 0.4f.spToPx()
+    private val lineHeight = 16f.spToPx().toInt()
+    private lateinit var defaultPaint: TextPaint
+
+    @Before
+    fun setup() {
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        defaultPaint = TextPaint().apply {
+            typeface = ResourcesCompat.getFont(instrumentation.context, R.font.sample_font)!!
+            textSize = fontSize
+        }
+    }
+
+    @Test
+    fun intrinsicWidth_with_letterSpacing_and_lineHeight_createsOneLine() {
+        val text = defaultText.apply {
+            setSpan(LineHeightSpan(lineHeight))
+            setSpan(LetterSpacingSpanPx(letterSpacingPx))
+        }
+
+        assertLineCount(text)
+    }
+
+    @Test
+    fun intrinsicWidth_with_letterSpacing_and_lineHeight_createsOneLine_multipleSpans() {
+        val text = defaultText.apply {
+            for (i in 0..8) {
+                setSpan(LineHeightSpan(lineHeight), i, i + 1)
+                setSpan(LetterSpacingSpanPx(letterSpacingPx), i, i + 1)
+            }
+        }
+
+        assertLineCount(text)
+    }
+
+    @Test
+    fun intrinsicWidth_with_letterSpacingEm_and_lineHeight_createsOneLine() {
+        val text = defaultText.apply {
+            setSpan(LineHeightSpan(lineHeight))
+            setSpan(LetterSpacingSpanEm(letterSpacingEm))
+        }
+
+        assertLineCount(text)
+    }
+
+    @Test
+    fun intrinsicWidth_with_paintLetterSpacing_and_lineHeight_createsOneLine() {
+        val text = defaultText.apply {
+            setSpan(LineHeightSpan(lineHeight))
+        }
+
+        val paint = defaultPaint.apply {
+            letterSpacing = letterSpacingEm
+        }
+
+        assertLineCount(text, paint)
+    }
+
+    @Test
+    fun intrinsicWidth_with_letterSpacing_and_noLineHeight_createsOneLine() {
+        val text = defaultText.apply {
+            setSpan(LetterSpacingSpanPx(letterSpacingPx))
+        }
+
+        assertLineCount(text)
+    }
+
+    @Test
+    fun intrinsicWidth_with_noLetterSpacing_and_withLineHeight_createsOneLine() {
+        val text = defaultText.apply {
+            setSpan(LineHeightSpan(lineHeight))
+        }
+
+        assertLineCount(text)
+    }
+
+    @Test
+    fun intrinsicWidth_with_noLetterSpacing_and_noLineHeight_createsOneLine() {
+        assertLineCount(defaultText)
+    }
+
+    private fun assertLineCount(text: CharSequence, paint: TextPaint = defaultPaint) {
+        val intrinsics = LayoutIntrinsics(text, paint, LayoutCompat.TEXT_DIRECTION_LTR)
+        assertThat(
+            TextLayout(
+                charSequence = text,
+                width = intrinsics.maxIntrinsicWidth,
+                textPaint = paint
+            ).lineCount
+        ).isEqualTo(1)
+    }
+
+    fun Spannable.setSpan(span: Any, start: Int = 0, end: Int = length) {
+        this.setSpan(span, start, end, SPAN_INCLUSIVE_INCLUSIVE)
+    }
+
+    fun Float.spToPx(): Float = this * fontScale * density
+}
\ No newline at end of file
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt b/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt
index b1fc875..942c5fd 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt
@@ -55,7 +55,7 @@
      *
      * @return the last character in the text, or [java.text.CharacterIterator.DONE] if the
      * text is empty
-     * @see .getEndIndex
+     * @see CharSequenceCharacterIterator.getEndIndex
      */
     override fun last(): Char {
         return if (start == end) {
@@ -73,7 +73,7 @@
      * @return the character at the current position or [java.text.CharacterIterator.DONE]
      * if the current
      * position is off the end of the text.
-     * @see .getIndex
+     * @see CharSequenceCharacterIterator.getIndex
      */
     override fun current(): Char {
         return if (index == end) CharacterIterator.DONE else charSequence[index]
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
index 0480714..65bb944 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
@@ -18,7 +18,11 @@
 
 import android.text.BoringLayout
 import android.text.Layout
+import android.text.Spanned
 import android.text.TextPaint
+import androidx.compose.ui.text.android.style.LetterSpacingSpanEm
+import androidx.compose.ui.text.android.style.LetterSpacingSpanPx
+import androidx.compose.ui.text.android.style.LineHeightSpan
 import java.text.BreakIterator
 import java.util.PriorityQueue
 
@@ -37,7 +41,7 @@
      * Compute Android platform BoringLayout metrics. A null value means the provided CharSequence
      * cannot be laid out using a BoringLayout.
      */
-    val boringMetrics: BoringLayout.Metrics? by lazy {
+    val boringMetrics: BoringLayout.Metrics? by lazy(LazyThreadSafetyMode.NONE) {
         val frameworkTextDir = getTextDirectionHeuristic(textDirectionHeuristic)
         BoringLayoutFactory.measure(charSequence, textPaint, frameworkTextDir)
     }
@@ -47,7 +51,7 @@
      *
      * @see androidx.compose.ui.text.android.minIntrinsicWidth
      */
-    val minIntrinsicWidth: Float by lazy {
+    val minIntrinsicWidth: Float by lazy(LazyThreadSafetyMode.NONE) {
         minIntrinsicWidth(charSequence, textPaint)
     }
 
@@ -55,9 +59,15 @@
      * Calculate maximum intrinsic width for the CharSequence. Maximum intrinsic width is the width
      * of text where no soft line breaks are applied.
      */
-    val maxIntrinsicWidth: Float by lazy {
-        boringMetrics?.width?.toFloat()
+    val maxIntrinsicWidth: Float by lazy(LazyThreadSafetyMode.NONE) {
+        var desiredWidth: Float = boringMetrics?.width?.toFloat()
             ?: Layout.getDesiredWidth(charSequence, 0, charSequence.length, textPaint)
+        if (shouldIncreaseMaxIntrinsic(desiredWidth, charSequence, textPaint)) {
+            // b/173574230, increase maxIntrinsicWidth, so that StaticLayout won't form 2
+            // lines for the given maxIntrinsicWidth
+            desiredWidth += 0.5f
+        }
+        desiredWidth
     }
 }
 
@@ -107,3 +117,28 @@
 
     return minWidth
 }
+
+/**
+ * b/173574230
+ * on Android 11 and above, creating a StaticLayout when
+ * - desiredWidth is an Integer,
+ * - letterSpacing is set
+ * - lineHeight is set
+ * StaticLayout forms 2 lines for the given desiredWidth.
+ *
+ * This function checks if those conditions are met.
+ */
+@OptIn(InternalPlatformTextApi::class)
+private fun shouldIncreaseMaxIntrinsic(
+    desiredWidth: Float,
+    charSequence: CharSequence,
+    textPaint: TextPaint
+): Boolean {
+    return desiredWidth != 0f &&
+        charSequence is Spanned && (
+        textPaint.letterSpacing != 0f ||
+            charSequence.hasSpan(LetterSpacingSpanPx::class.java) ||
+            charSequence.hasSpan(LetterSpacingSpanEm::class.java)
+        ) &&
+        charSequence.hasSpan(LineHeightSpan::class.java)
+}
\ No newline at end of file
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.kt
similarity index 74%
copy from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
copy to text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.kt
index e022dc3..21b4eb4 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.kt
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.car.app.model.signin;
+package androidx.compose.ui.text.android
 
-import androidx.car.app.IOnDoneCallback;
+import android.text.Spanned
 
-/** @hide */
-oneway interface IOnInputCompletedListener {
-  void onInputCompleted(String value, IOnDoneCallback callback) = 1;
-}
+internal fun Spanned.hasSpan(clazz: Class<*>): Boolean {
+    return nextSpanTransition(-1 /* start */, length /* limit */, clazz) != length
+}
\ No newline at end of file
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index 77edef6..1103629 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -160,7 +160,7 @@
                 start = 0,
                 end = charSequence.length,
                 paint = textPaint,
-                width = ceil(width).toInt(),
+                width = widthInt,
                 textDir = frameworkTextDir,
                 alignment = frameworkAlignment,
                 maxLines = maxLines,
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt
index d2ab8e8..7589408 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt
@@ -17,6 +17,7 @@
 
 import android.graphics.Paint.FontMetricsInt
 import androidx.compose.ui.text.android.InternalPlatformTextApi
+import kotlin.math.ceil
 
 /**
  * The span which modifies the height of the covered paragraphs. A paragraph is defined as a
@@ -44,8 +45,7 @@
             return
         }
         val ratio = this.lineHeight * 1.0f / currentHeight
-        fontMetricsInt.descent =
-            Math.ceil(fontMetricsInt.descent * ratio.toDouble()).toInt()
+        fontMetricsInt.descent = ceil(fontMetricsInt.descent * ratio.toDouble()).toInt()
         fontMetricsInt.ascent = fontMetricsInt.descent - this.lineHeight
     }
 }
\ No newline at end of file
diff --git a/tv-provider/tv-provider/api/current.txt b/tv-provider/tv-provider/api/current.txt
index 885ce64..a48d742 100644
--- a/tv-provider/tv-provider/api/current.txt
+++ b/tv-provider/tv-provider/api/current.txt
@@ -75,7 +75,7 @@
 
   @WorkerThread public class ChannelLogoUtils {
     ctor @Deprecated public ChannelLogoUtils();
-    method @WorkerThread public static android.graphics.Bitmap! loadChannelLogo(android.content.Context, long);
+    method @WorkerThread public static android.graphics.Bitmap? loadChannelLogo(android.content.Context, long);
     method public static boolean storeChannelLogo(android.content.Context, long, android.net.Uri);
     method @WorkerThread public static boolean storeChannelLogo(android.content.Context, long, android.graphics.Bitmap);
   }
diff --git a/tv-provider/tv-provider/api/public_plus_experimental_current.txt b/tv-provider/tv-provider/api/public_plus_experimental_current.txt
index 885ce64..a48d742 100644
--- a/tv-provider/tv-provider/api/public_plus_experimental_current.txt
+++ b/tv-provider/tv-provider/api/public_plus_experimental_current.txt
@@ -75,7 +75,7 @@
 
   @WorkerThread public class ChannelLogoUtils {
     ctor @Deprecated public ChannelLogoUtils();
-    method @WorkerThread public static android.graphics.Bitmap! loadChannelLogo(android.content.Context, long);
+    method @WorkerThread public static android.graphics.Bitmap? loadChannelLogo(android.content.Context, long);
     method public static boolean storeChannelLogo(android.content.Context, long, android.net.Uri);
     method @WorkerThread public static boolean storeChannelLogo(android.content.Context, long, android.graphics.Bitmap);
   }
diff --git a/tv-provider/tv-provider/api/restricted_current.txt b/tv-provider/tv-provider/api/restricted_current.txt
index 8b4fe6af..089a93e 100644
--- a/tv-provider/tv-provider/api/restricted_current.txt
+++ b/tv-provider/tv-provider/api/restricted_current.txt
@@ -81,7 +81,7 @@
 
   @WorkerThread public class ChannelLogoUtils {
     ctor @Deprecated public ChannelLogoUtils();
-    method @WorkerThread public static android.graphics.Bitmap! loadChannelLogo(android.content.Context, long);
+    method @WorkerThread public static android.graphics.Bitmap? loadChannelLogo(android.content.Context, long);
     method public static boolean storeChannelLogo(android.content.Context, long, android.net.Uri);
     method @WorkerThread public static boolean storeChannelLogo(android.content.Context, long, android.graphics.Bitmap);
   }
diff --git a/tv-provider/tv-provider/src/androidTest/java/androidx/tvprovider/media/tv/ChannelLogoUtilsTest.java b/tv-provider/tv-provider/src/androidTest/java/androidx/tvprovider/media/tv/ChannelLogoUtilsTest.java
index 8c24f11..83702d2 100644
--- a/tv-provider/tv-provider/src/androidTest/java/androidx/tvprovider/media/tv/ChannelLogoUtilsTest.java
+++ b/tv-provider/tv-provider/src/androidTest/java/androidx/tvprovider/media/tv/ChannelLogoUtilsTest.java
@@ -32,9 +32,9 @@
 import android.net.Uri;
 import android.os.SystemClock;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
-import androidx.test.filters.Suppress;
 import androidx.tvprovider.test.R;
 
 import org.junit.After;
@@ -42,7 +42,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-@Suppress // Test is failing b/70905391
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class ChannelLogoUtilsTest {
@@ -54,6 +53,9 @@
 
     @Before
     public void setUp() throws Exception {
+        if (!Utils.hasTvInputFramework(ApplicationProvider.getApplicationContext())) {
+            return;
+        }
         mContentResolver = getApplicationContext().getContentResolver();
         ContentValues contentValues = new Channel.Builder()
                 .setInputId(FAKE_INPUT_ID)
@@ -64,11 +66,17 @@
 
     @After
     public void tearDown() throws Exception {
+        if (!Utils.hasTvInputFramework(ApplicationProvider.getApplicationContext())) {
+            return;
+        }
         mContentResolver.delete(mChannelUri, null, null);
     }
 
     @Test
     public void testStoreChannelLogo_fromBitmap() {
+        if (!Utils.hasTvInputFramework(ApplicationProvider.getApplicationContext())) {
+            return;
+        }
         assertNull(ChannelLogoUtils.loadChannelLogo(getApplicationContext(), mChannelId));
         Bitmap logo = BitmapFactory.decodeResource(getApplicationContext().getResources(),
                 R.drawable.test_icon);
@@ -82,6 +90,9 @@
 
     @Test
     public void testStoreChannelLogo_fromResUri() {
+        if (!Utils.hasTvInputFramework(ApplicationProvider.getApplicationContext())) {
+            return;
+        }
         assertNull(ChannelLogoUtils.loadChannelLogo(getApplicationContext(), mChannelId));
         int resId = R.drawable.test_icon;
         Resources res = getApplicationContext().getResources();
diff --git a/tv-provider/tv-provider/src/main/java/androidx/tvprovider/media/tv/ChannelLogoUtils.java b/tv-provider/tv-provider/src/main/java/androidx/tvprovider/media/tv/ChannelLogoUtils.java
index df3753c..158e499 100644
--- a/tv-provider/tv-provider/src/main/java/androidx/tvprovider/media/tv/ChannelLogoUtils.java
+++ b/tv-provider/tv-provider/src/main/java/androidx/tvprovider/media/tv/ChannelLogoUtils.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
 import java.io.FileNotFoundException;
@@ -150,16 +151,17 @@
      * @see #storeChannelLogo(Context, long, Uri)
      * @see #storeChannelLogo(Context, long, Bitmap)
      */
+    @Nullable
     @WorkerThread
     @SuppressLint("WrongThread") // TODO https://issuetracker.google.com/issues/116776070
     public static Bitmap loadChannelLogo(@NonNull Context context, long channelId) {
         Bitmap channelLogo = null;
-        try {
-            channelLogo = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(
-                    TvContract.buildChannelLogoUri(channelId)));
-        } catch (FileNotFoundException e) {
+        Uri logoUri = TvContract.buildChannelLogoUri(channelId);
+        try (InputStream is = context.getContentResolver().openInputStream(logoUri)) {
+            channelLogo = BitmapFactory.decodeStream(is);
+        } catch (IOException e) {
             // Channel logo is not found in the content provider.
-            Log.i(TAG, "Channel logo for channel (ID:" + channelId + ") not found.", e);
+            Log.i(TAG, "Could not load channel logo for channel (ID:" + channelId + ").", e);
         }
         return channelLogo;
     }
diff --git a/wear/tiles/tiles-renderer/api/current.txt b/wear/tiles/tiles-renderer/api/current.txt
index 1896e65..014a5a0 100644
--- a/wear/tiles/tiles-renderer/api/current.txt
+++ b/wear/tiles/tiles-renderer/api/current.txt
@@ -11,43 +11,14 @@
 
 package androidx.wear.tiles.renderer {
 
-  public class ResourceAccessors {
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder builder(androidx.wear.tiles.proto.ResourceProto.Resources);
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(String);
-  }
-
-  public static interface ResourceAccessors.AndroidImageResourceByResIdAccessor {
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId);
-  }
-
-  public static final class ResourceAccessors.Builder {
-    method public androidx.wear.tiles.renderer.ResourceAccessors build();
-    method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setAndroidImageResourceByResIdAccessor(androidx.wear.tiles.renderer.ResourceAccessors.AndroidImageResourceByResIdAccessor);
-    method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setInlineImageResourceAccessor(androidx.wear.tiles.renderer.ResourceAccessors.InlineImageResourceAccessor);
-  }
-
-  public static interface ResourceAccessors.InlineImageResourceAccessor {
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.InlineImageResource);
-  }
-
-  public static final class ResourceAccessors.ResourceAccessException extends java.lang.Exception {
-    ctor public ResourceAccessors.ResourceAccessException(String);
-    ctor public ResourceAccessors.ResourceAccessException(String, Exception);
-  }
-
-  public class StandardResourceAccessors {
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forLocalApp(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources);
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forRemoteService(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources, android.content.res.Resources);
-  }
-
   public final class TileRenderer {
-    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
-    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, @StyleRes int, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
     method public android.view.View? inflate(android.view.ViewGroup);
   }
 
   public static interface TileRenderer.LoadActionListener {
-    method public void onClick(androidx.wear.tiles.proto.StateProto.State);
+    method public void onClick(androidx.wear.tiles.builders.StateBuilders.State);
   }
 
 }
diff --git a/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt b/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
index 1896e65..014a5a0 100644
--- a/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
+++ b/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
@@ -11,43 +11,14 @@
 
 package androidx.wear.tiles.renderer {
 
-  public class ResourceAccessors {
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder builder(androidx.wear.tiles.proto.ResourceProto.Resources);
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(String);
-  }
-
-  public static interface ResourceAccessors.AndroidImageResourceByResIdAccessor {
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId);
-  }
-
-  public static final class ResourceAccessors.Builder {
-    method public androidx.wear.tiles.renderer.ResourceAccessors build();
-    method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setAndroidImageResourceByResIdAccessor(androidx.wear.tiles.renderer.ResourceAccessors.AndroidImageResourceByResIdAccessor);
-    method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setInlineImageResourceAccessor(androidx.wear.tiles.renderer.ResourceAccessors.InlineImageResourceAccessor);
-  }
-
-  public static interface ResourceAccessors.InlineImageResourceAccessor {
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.InlineImageResource);
-  }
-
-  public static final class ResourceAccessors.ResourceAccessException extends java.lang.Exception {
-    ctor public ResourceAccessors.ResourceAccessException(String);
-    ctor public ResourceAccessors.ResourceAccessException(String, Exception);
-  }
-
-  public class StandardResourceAccessors {
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forLocalApp(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources);
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forRemoteService(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources, android.content.res.Resources);
-  }
-
   public final class TileRenderer {
-    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
-    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, @StyleRes int, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
     method public android.view.View? inflate(android.view.ViewGroup);
   }
 
   public static interface TileRenderer.LoadActionListener {
-    method public void onClick(androidx.wear.tiles.proto.StateProto.State);
+    method public void onClick(androidx.wear.tiles.builders.StateBuilders.State);
   }
 
 }
diff --git a/wear/tiles/tiles-renderer/api/restricted_current.txt b/wear/tiles/tiles-renderer/api/restricted_current.txt
index 1896e65..014a5a0 100644
--- a/wear/tiles/tiles-renderer/api/restricted_current.txt
+++ b/wear/tiles/tiles-renderer/api/restricted_current.txt
@@ -11,43 +11,14 @@
 
 package androidx.wear.tiles.renderer {
 
-  public class ResourceAccessors {
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder builder(androidx.wear.tiles.proto.ResourceProto.Resources);
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(String);
-  }
-
-  public static interface ResourceAccessors.AndroidImageResourceByResIdAccessor {
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId);
-  }
-
-  public static final class ResourceAccessors.Builder {
-    method public androidx.wear.tiles.renderer.ResourceAccessors build();
-    method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setAndroidImageResourceByResIdAccessor(androidx.wear.tiles.renderer.ResourceAccessors.AndroidImageResourceByResIdAccessor);
-    method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setInlineImageResourceAccessor(androidx.wear.tiles.renderer.ResourceAccessors.InlineImageResourceAccessor);
-  }
-
-  public static interface ResourceAccessors.InlineImageResourceAccessor {
-    method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.InlineImageResource);
-  }
-
-  public static final class ResourceAccessors.ResourceAccessException extends java.lang.Exception {
-    ctor public ResourceAccessors.ResourceAccessException(String);
-    ctor public ResourceAccessors.ResourceAccessException(String, Exception);
-  }
-
-  public class StandardResourceAccessors {
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forLocalApp(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources);
-    method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forRemoteService(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources, android.content.res.Resources);
-  }
-
   public final class TileRenderer {
-    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
-    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, @StyleRes int, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+    ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
     method public android.view.View? inflate(android.view.ViewGroup);
   }
 
   public static interface TileRenderer.LoadActionListener {
-    method public void onClick(androidx.wear.tiles.proto.StateProto.State);
+    method public void onClick(androidx.wear.tiles.builders.StateBuilders.State);
   }
 
 }
diff --git a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
index ad0b0b7..5fb0cfb 100644
--- a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
+++ b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
@@ -40,7 +40,6 @@
 import androidx.wear.tiles.proto.ResourceProto.InlineImageResource;
 import androidx.wear.tiles.proto.ResourceProto.Resources;
 import androidx.wear.tiles.protobuf.ByteString;
-import androidx.wear.tiles.renderer.StandardResourceAccessors;
 import androidx.wear.tiles.renderer.TileRenderer;
 
 import com.google.protobuf.TextFormat;
@@ -238,10 +237,7 @@
                         appContext,
                         LayoutElementBuilders.Layout.fromProto(
                                 Layout.newBuilder().setRoot(rootElement).build()),
-                        StandardResourceAccessors.forLocalApp(
-                                        appContext,
-                                        ResourceBuilders.Resources.fromProto(generateResources()))
-                                .build(),
+                        ResourceBuilders.Resources.fromProto(generateResources()),
                         ContextCompat.getMainExecutor(getApplicationContext()),
                         i -> {});
 
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileManager.kt b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileManager.kt
index 6bc0ffb..51095fd 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileManager.kt
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileManager.kt
@@ -37,7 +37,6 @@
 import androidx.wear.tiles.proto.DeviceParametersProto
 import androidx.wear.tiles.proto.RequestProto
 import androidx.wear.tiles.proto.StateProto
-import androidx.wear.tiles.renderer.StandardResourceAccessors
 import androidx.wear.tiles.renderer.TileRenderer
 import androidx.wear.tiles.timeline.TilesTimelineManager
 import kotlinx.coroutines.CoroutineScope
@@ -184,9 +183,9 @@
         val renderer = TileRenderer(
             context,
             layout,
-            StandardResourceAccessors.forLocalApp(context, tileResources!!).build(),
+            tileResources!!,
             ContextCompat.getMainExecutor(context),
-            { state -> coroutineScope.launch { requestTile(state) } }
+            { state -> coroutineScope.launch { requestTile(state.toProto()) } }
         )
         renderer.inflate(parentView)?.apply {
             (layoutParams as FrameLayout.LayoutParams).gravity = Gravity.CENTER
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 3736144..e77e338 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -16,100 +16,20 @@
 
 package androidx.wear.tiles.renderer;
 
-import static java.lang.Math.max;
-import static java.lang.Math.round;
-
 import android.content.Context;
-import android.content.Intent;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Typeface;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.TextUtils.TruncateAt;
-import android.text.style.AbsoluteSizeSpan;
-import android.text.style.ClickableSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.ImageSpan;
-import android.text.style.MetricAffectingSpan;
-import android.text.style.StyleSpan;
-import android.text.style.UnderlineSpan;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.ContextThemeWrapper;
-import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.ViewOutlineProvider;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.ImageView.ScaleType;
-import android.widget.LinearLayout;
-import android.widget.Space;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StyleRes;
-import androidx.core.content.ContextCompat;
-import androidx.wear.tiles.TileProviderService;
 import androidx.wear.tiles.builders.LayoutElementBuilders;
-import androidx.wear.tiles.proto.ActionProto.Action;
-import androidx.wear.tiles.proto.ActionProto.AndroidActivity;
-import androidx.wear.tiles.proto.ActionProto.LaunchAction;
-import androidx.wear.tiles.proto.ActionProto.LoadAction;
-import androidx.wear.tiles.proto.DimensionProto.ContainerDimension;
-import androidx.wear.tiles.proto.DimensionProto.ContainerDimension.InnerCase;
-import androidx.wear.tiles.proto.DimensionProto.DpProp;
-import androidx.wear.tiles.proto.DimensionProto.ExpandedDimensionProp;
-import androidx.wear.tiles.proto.DimensionProto.ImageDimension;
-import androidx.wear.tiles.proto.DimensionProto.ProportionalDimensionProp;
-import androidx.wear.tiles.proto.DimensionProto.SpacerDimension;
-import androidx.wear.tiles.proto.DimensionProto.WrappedDimensionProp;
-import androidx.wear.tiles.proto.LayoutElementProto.Arc;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcAnchorTypeProp;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcLayoutElement;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcLine;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcSpacer;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcText;
-import androidx.wear.tiles.proto.LayoutElementProto.Box;
-import androidx.wear.tiles.proto.LayoutElementProto.Column;
-import androidx.wear.tiles.proto.LayoutElementProto.ContentScaleMode;
-import androidx.wear.tiles.proto.LayoutElementProto.FontStyle;
-import androidx.wear.tiles.proto.LayoutElementProto.HorizontalAlignmentProp;
-import androidx.wear.tiles.proto.LayoutElementProto.Image;
-import androidx.wear.tiles.proto.LayoutElementProto.Layout;
-import androidx.wear.tiles.proto.LayoutElementProto.LayoutElement;
-import androidx.wear.tiles.proto.LayoutElementProto.Row;
-import androidx.wear.tiles.proto.LayoutElementProto.Spacer;
-import androidx.wear.tiles.proto.LayoutElementProto.Span;
-import androidx.wear.tiles.proto.LayoutElementProto.SpanImage;
-import androidx.wear.tiles.proto.LayoutElementProto.SpanText;
-import androidx.wear.tiles.proto.LayoutElementProto.Spannable;
-import androidx.wear.tiles.proto.LayoutElementProto.Text;
-import androidx.wear.tiles.proto.LayoutElementProto.TextAlignmentProp;
-import androidx.wear.tiles.proto.LayoutElementProto.TextOverflowProp;
-import androidx.wear.tiles.proto.LayoutElementProto.VerticalAlignmentProp;
-import androidx.wear.tiles.proto.ModifiersProto.ArcModifiers;
-import androidx.wear.tiles.proto.ModifiersProto.Background;
-import androidx.wear.tiles.proto.ModifiersProto.Border;
-import androidx.wear.tiles.proto.ModifiersProto.Clickable;
-import androidx.wear.tiles.proto.ModifiersProto.Modifiers;
-import androidx.wear.tiles.proto.ModifiersProto.Padding;
-import androidx.wear.tiles.proto.ModifiersProto.SpanModifiers;
-import androidx.wear.tiles.proto.StateProto.State;
+import androidx.wear.tiles.builders.ResourceBuilders;
+import androidx.wear.tiles.builders.StateBuilders;
+import androidx.wear.tiles.renderer.internal.StandardResourceAccessors;
+import androidx.wear.tiles.renderer.internal.TileRendererInternal;
 
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.List;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
 
 /**
  * Renderer for Wear Tiles.
@@ -117,46 +37,6 @@
  * <p>This variant uses Android views to represent the contents of the Wear Tile.
  */
 public final class TileRenderer {
-
-    private static final String TAG = "TileRenderer";
-
-    private static final int HALIGN_DEFAULT_GRAVITY = Gravity.CENTER_HORIZONTAL;
-    private static final int VALIGN_DEFAULT_GRAVITY = Gravity.CENTER_VERTICAL;
-    private static final int TEXT_ALIGN_DEFAULT = Gravity.CENTER_HORIZONTAL;
-    private static final ScaleType IMAGE_DEFAULT_SCALE_TYPE = ScaleType.FIT_CENTER;
-
-    @WearArcLayout.LayoutParams.VerticalAlignment
-    private static final int ARC_VALIGN_DEFAULT = WearArcLayout.LayoutParams.VALIGN_CENTER;
-
-    // This is pretty badly named; TruncateAt specifies where to place the ellipsis (or whether to
-    // marquee). Disabling truncation with null actually disables the _ellipsis_, but text will
-    // still
-    // be truncated.
-    @Nullable private static final TruncateAt TEXT_OVERFLOW_DEFAULT = null;
-
-    private static final int TEXT_COLOR_DEFAULT = 0xFFFFFFFF;
-    private static final int TEXT_MAX_LINES_DEFAULT = 1;
-    private static final int TEXT_MIN_LINES = 1;
-
-    private static final ContainerDimension CONTAINER_DIMENSION_DEFAULT =
-            ContainerDimension.newBuilder()
-                    .setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
-                    .build();
-
-    @WearArcLayout.AnchorType
-    private static final int ARC_ANCHOR_DEFAULT = WearArcLayout.ANCHOR_CENTER;
-
-    // White
-    private static final int LINE_COLOR_DEFAULT = 0xFFFFFFFF;
-
-    // Need to be package private so that TilesClickableSpan can see them.
-    final Context mAppContext;
-    final LoadActionListener mLoadActionListener;
-    final Executor mLoadActionExecutor;
-
-    private final Layout mLayout;
-    private final ResourceAccessors mResourceAccessors;
-
     /**
      * Listener for clicks on Clickable objects that have an Action to (re)load the contents of a
      * tile.
@@ -168,28 +48,30 @@
          *
          * @param nextState The state that the next tile should be in.
          */
-        void onClick(@NonNull State nextState);
+        void onClick(@NonNull StateBuilders.State nextState);
     }
 
+    private final TileRendererInternal mRenderer;
+
     /**
      * Default constructor.
      *
      * @param appContext The application context.
      * @param layout The portion of the Tile to render.
-     * @param resourceAccessors Accessors for the resources used for rendering this Tile.
+     * @param resources The resources for the Tile.
      * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
      */
     public TileRenderer(
             @NonNull Context appContext,
             @NonNull LayoutElementBuilders.Layout layout,
-            @NonNull ResourceAccessors resourceAccessors,
+            @NonNull ResourceBuilders.Resources resources,
             @NonNull Executor loadActionExecutor,
             @NonNull LoadActionListener loadActionListener) {
         this(
                 appContext,
                 layout,
-                resourceAccessors,
                 /* tilesTheme= */ 0,
+                resources,
                 loadActionExecutor,
                 loadActionListener);
     }
@@ -199,1379 +81,25 @@
      *
      * @param appContext The application context.
      * @param layout The portion of the Tile to render.
-     * @param resourceAccessors Accessors for the resources used for rendering this Tile.
      * @param tilesTheme The theme to use for this Tile instance. This can be used to customise
      *     things like the default font family. Pass 0 to use the default theme.
+     * @param resources The resources for the Tile.
      * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
      */
     public TileRenderer(
             @NonNull Context appContext,
             @NonNull LayoutElementBuilders.Layout layout,
-            @NonNull ResourceAccessors resourceAccessors,
             @StyleRes int tilesTheme,
+            @NonNull ResourceBuilders.Resources resources,
             @NonNull Executor loadActionExecutor,
             @NonNull LoadActionListener loadActionListener) {
-        if (tilesTheme == 0) {
-            tilesTheme = R.style.TilesBaseTheme;
-        }
-
-        this.mAppContext = new ContextThemeWrapper(appContext, tilesTheme);
-        this.mLayout = layout.toProto();
-        this.mResourceAccessors = resourceAccessors;
-        this.mLoadActionListener = loadActionListener;
-        this.mLoadActionExecutor = loadActionExecutor;
-    }
-
-    private int safeDpToPx(DpProp dpProp) {
-        return round(
-                max(0, dpProp.getValue()) * mAppContext.getResources().getDisplayMetrics().density);
-    }
-
-    @Nullable
-    private static Float safeAspectRatioOrNull(
-            ProportionalDimensionProp proportionalDimensionProp) {
-        final int dividend = proportionalDimensionProp.getAspectRatioWidth();
-        final int divisor = proportionalDimensionProp.getAspectRatioHeight();
-
-        if (dividend <= 0 || divisor <= 0) {
-            return null;
-        }
-        return (float) dividend / divisor;
-    }
-
-    /**
-     * Generates a generic LayoutParameters for use by all components. This just defaults to setting
-     * the width/height to WRAP_CONTENT.
-     *
-     * @return The default layout parameters.
-     */
-    private static ViewGroup.LayoutParams generateDefaultLayoutParams() {
-        return new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
-    }
-
-    private LayoutParams updateLayoutParamsInLinearLayout(
-            LinearLayout parent,
-            LayoutParams layoutParams,
-            ContainerDimension width,
-            ContainerDimension height) {
-        // This is a little bit fun. Tiles' semantics is that dimension = expand should eat all
-        // remaining space in that dimension, but not grow the parent. This is easy for standard
-        // containers, but a little trickier in rows and columns on Android.
-        //
-        // A Row (LinearLayout) supports this with width=0 and weight>0. After doing a layout pass,
-        // it
-        // will assign all remaining space to elements with width=0 and weight>0, biased by the
-        // weight.
-        // This causes problems if there are two (or more) "expand" elements in a row, which is
-        // itself
-        // set to WRAP_CONTENTS, and one of those elements has a measured width (e.g. Text). In that
-        // case, the LinearLayout will measure the text, then ensure that all elements with a weight
-        // set
-        // have their widths set according to the weight. For us, that means that _all_ elements
-        // with
-        // expand=true will size themselves to the same width as the Text, pushing out the bounds of
-        // the
-        // parent row. This happens on columns too, but of course regarding height.
-        //
-        // To get around this, if an element with expand=true is added to a row that is WRAP_CONTENT
-        // (e.g. a row with no explicit width, that is not expanded), we ignore the expand=true, and
-        // set the inner element's width to WRAP_CONTENT too.
-
-        LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(layoutParams);
-
-        // Handle the width
-        if (parent.getOrientation() == LinearLayout.HORIZONTAL
-                && width.getInnerCase() == InnerCase.EXPANDED_DIMENSION) {
-            // If the parent container would not normally have "remaining space", ignore the
-            // expand=true.
-            if (parent.getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
-                linearLayoutParams.width = LayoutParams.WRAP_CONTENT;
-            } else {
-                linearLayoutParams.width = 0;
-                linearLayoutParams.weight = 1;
-            }
-        } else {
-            linearLayoutParams.width = dimensionToPx(width);
-        }
-
-        // And the height
-        if (parent.getOrientation() == LinearLayout.VERTICAL
-                && height.getInnerCase() == InnerCase.EXPANDED_DIMENSION) {
-            // If the parent container would not normally have "remaining space", ignore the
-            // expand=true.
-            if (parent.getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
-                linearLayoutParams.height = LayoutParams.WRAP_CONTENT;
-            } else {
-                linearLayoutParams.height = 0;
-                linearLayoutParams.weight = 1;
-            }
-        } else {
-            linearLayoutParams.height = dimensionToPx(height);
-        }
-
-        return linearLayoutParams;
-    }
-
-    private LayoutParams updateLayoutParams(
-            ViewGroup parent,
-            LayoutParams layoutParams,
-            ContainerDimension width,
-            ContainerDimension height) {
-        if (parent instanceof LinearLayout) {
-            // LinearLayouts have a bunch of messy caveats in Tile when their children can be
-            // expanded;
-            // factor that case out to keep this clean.
-            return updateLayoutParamsInLinearLayout(
-                    (LinearLayout) parent, layoutParams, width, height);
-        } else {
-            layoutParams.width = dimensionToPx(width);
-            layoutParams.height = dimensionToPx(height);
-        }
-
-        return layoutParams;
-    }
-
-    private static int horizontalAlignmentToGravity(HorizontalAlignmentProp alignment) {
-        switch (alignment.getValue()) {
-            case HALIGN_START:
-                return Gravity.START;
-            case HALIGN_CENTER:
-                return Gravity.CENTER_HORIZONTAL;
-            case HALIGN_END:
-                return Gravity.END;
-            case HALIGN_LEFT:
-                return Gravity.LEFT;
-            case HALIGN_RIGHT:
-                return Gravity.RIGHT;
-            case UNRECOGNIZED:
-            case HALIGN_UNDEFINED:
-                return HALIGN_DEFAULT_GRAVITY;
-        }
-
-        return HALIGN_DEFAULT_GRAVITY;
-    }
-
-    private static int verticalAlignmentToGravity(VerticalAlignmentProp alignment) {
-        switch (alignment.getValue()) {
-            case VALIGN_TOP:
-                return Gravity.TOP;
-            case VALIGN_CENTER:
-                return Gravity.CENTER_VERTICAL;
-            case VALIGN_BOTTOM:
-                return Gravity.BOTTOM;
-            case UNRECOGNIZED:
-            case VALIGN_UNDEFINED:
-                return VALIGN_DEFAULT_GRAVITY;
-        }
-
-        return VALIGN_DEFAULT_GRAVITY;
-    }
-
-    @WearArcLayout.LayoutParams.VerticalAlignment
-    private static int verticalAlignmentToArcVAlign(VerticalAlignmentProp alignment) {
-        switch (alignment.getValue()) {
-            case VALIGN_TOP:
-                return WearArcLayout.LayoutParams.VALIGN_OUTER;
-            case VALIGN_CENTER:
-                return WearArcLayout.LayoutParams.VALIGN_CENTER;
-            case VALIGN_BOTTOM:
-                return WearArcLayout.LayoutParams.VALIGN_INNER;
-            case UNRECOGNIZED:
-            case VALIGN_UNDEFINED:
-                return ARC_VALIGN_DEFAULT;
-        }
-
-        return ARC_VALIGN_DEFAULT;
-    }
-
-    private static ScaleType contentScaleModeToScaleType(ContentScaleMode contentScaleMode) {
-        switch (contentScaleMode) {
-            case CONTENT_SCALE_MODE_FIT:
-                return ScaleType.FIT_CENTER;
-            case CONTENT_SCALE_MODE_CROP:
-                return ScaleType.CENTER_CROP;
-            case CONTENT_SCALE_MODE_FILL_BOUNDS:
-                return ScaleType.FIT_XY;
-            case CONTENT_SCALE_MODE_UNDEFINED:
-            case UNRECOGNIZED:
-                return IMAGE_DEFAULT_SCALE_TYPE;
-        }
-
-        return IMAGE_DEFAULT_SCALE_TYPE;
-    }
-
-    private static boolean isBold(FontStyle fontStyle) {
-        // Although this method could be a simple equality check against FONT_WEIGHT_BOLD, we list
-        // all
-        // current cases here so that this will become a compile time error as soon as a new
-        // FontWeight
-        // value is added to the schema. If this fails to build, then this means that an int
-        // typeface
-        // style is no longer enough to represent all FontWeight values and a customizable,
-        // per-weight
-        // text style must be introduced to TileRenderer to handle this. See b/176980535
-        switch (fontStyle.getWeight().getValue()) {
-            case FONT_WEIGHT_BOLD:
-                return true;
-            case FONT_WEIGHT_NORMAL:
-            case FONT_WEIGHT_UNDEFINED:
-            case UNRECOGNIZED:
-                return false;
-        }
-
-        return false;
-    }
-
-    private static int fontStyleToTypefaceStyle(FontStyle fontStyle) {
-        final boolean isBold = isBold(fontStyle);
-        final boolean isItalic = fontStyle.getItalic().getValue();
-
-        if (isBold && isItalic) {
-            return Typeface.BOLD_ITALIC;
-        } else if (isBold) {
-            return Typeface.BOLD;
-        } else if (isItalic) {
-            return Typeface.ITALIC;
-        } else {
-            return Typeface.NORMAL;
-        }
-    }
-
-    @SuppressWarnings("nullness")
-    private static Typeface createTypeface(
-            FontStyle fontStyle, @Nullable Typeface currentTypeface) {
-        return Typeface.create(currentTypeface, fontStyleToTypefaceStyle(fontStyle));
-    }
-
-    private static MetricAffectingSpan createTypefaceSpan(FontStyle fontStyle) {
-        return new StyleSpan(fontStyleToTypefaceStyle(fontStyle));
-    }
-
-    private static boolean hasDefaultTypeface(FontStyle fontStyle) {
-        return !fontStyle.getItalic().getValue() && !isBold(fontStyle);
-    }
-
-    private static void applyFontStyle(FontStyle style, TextView textView) {
-        Typeface currentTypeface = textView.getTypeface();
-
-        if (!hasDefaultTypeface(style)) {
-            // Need to supply typefaceStyle when creating the typeface (will select specialist
-            // bold/italic
-            // typefaces), *and* when setting the typeface (will set synthetic bold/italic flags in
-            // Paint
-            // if they're not supported by the given typeface).
-            textView.setTypeface(
-                    createTypeface(style, currentTypeface), fontStyleToTypefaceStyle(style));
-        }
-
-        int currentPaintFlags = textView.getPaintFlags();
-
-        // Remove the bits we're setting
-        currentPaintFlags &= ~Paint.UNDERLINE_TEXT_FLAG;
-
-        if (style.hasUnderline() && style.getUnderline().getValue()) {
-            currentPaintFlags |= Paint.UNDERLINE_TEXT_FLAG;
-        }
-
-        textView.setPaintFlags(currentPaintFlags);
-
-        if (style.hasSize()) {
-            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.getSize().getValue());
-        }
-
-        if (style.hasLetterSpacing()) {
-            textView.setLetterSpacing(style.getLetterSpacing().getValue());
-        }
-
-        textView.setTextColor(extractTextColorArgb(style));
-    }
-
-    private void applyClickable(View view, Clickable clickable) {
-        view.setTag(clickable.getId());
-
-        boolean hasAction = false;
-        switch (clickable.getOnClick().getValueCase()) {
-            case LAUNCH_ACTION:
-                Intent i =
-                        buildLaunchActionIntent(
-                                clickable.getOnClick().getLaunchAction(), clickable.getId());
-                if (i != null) {
-                    hasAction = true;
-                    view.setOnClickListener(
-                            v -> {
-                                if (i.resolveActivity(mAppContext.getPackageManager()) != null) {
-                                    mAppContext.startActivity(i);
-                                }
-                            });
-                }
-                break;
-            case LOAD_ACTION:
-                hasAction = true;
-                view.setOnClickListener(
-                        v ->
-                                mLoadActionListener.onClick(
-                                        buildState(
-                                                clickable.getOnClick().getLoadAction(),
-                                                clickable.getId())));
-                break;
-            case VALUE_NOT_SET:
-                break;
-        }
-
-        if (hasAction) {
-            // Apply ripple effect
-            TypedValue outValue = new TypedValue();
-            mAppContext
-                    .getTheme()
-                    .resolveAttribute(
-                            android.R.attr.selectableItemBackground,
-                            outValue,
-                            /* resolveRefs= */ true);
-            view.setForeground(mAppContext.getDrawable(outValue.resourceId));
-        }
-    }
-
-    private void applyPadding(View view, Padding padding) {
-        if (padding.getRtlAware().getValue()) {
-            view.setPaddingRelative(
-                    safeDpToPx(padding.getStart()),
-                    safeDpToPx(padding.getTop()),
-                    safeDpToPx(padding.getEnd()),
-                    safeDpToPx(padding.getBottom()));
-        } else {
-            view.setPadding(
-                    safeDpToPx(padding.getStart()),
-                    safeDpToPx(padding.getTop()),
-                    safeDpToPx(padding.getEnd()),
-                    safeDpToPx(padding.getBottom()));
-        }
-    }
-
-    private GradientDrawable applyBackground(
-            View view, Background background, @Nullable GradientDrawable drawable) {
-        if (drawable == null) {
-            drawable = new GradientDrawable();
-        }
-
-        if (background.hasColor()) {
-            drawable.setColor(background.getColor().getArgb());
-        }
-
-        if (background.hasCorner()) {
-            drawable.setCornerRadius(safeDpToPx(background.getCorner().getRadius()));
-            view.setClipToOutline(true);
-            view.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
-        }
-
-        return drawable;
-    }
-
-    private GradientDrawable applyBorder(Border border, @Nullable GradientDrawable drawable) {
-        if (drawable == null) {
-            drawable = new GradientDrawable();
-        }
-
-        drawable.setStroke(safeDpToPx(border.getWidth()), border.getColor().getArgb());
-
-        return drawable;
-    }
-
-    private View applyModifiers(View view, Modifiers modifiers) {
-        if (modifiers.hasClickable()) {
-            applyClickable(view, modifiers.getClickable());
-        }
-
-        if (modifiers.hasSemantics()) {
-            applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
-        }
-
-        if (modifiers.hasPadding()) {
-            applyPadding(view, modifiers.getPadding());
-        }
-
-        GradientDrawable backgroundDrawable = null;
-
-        if (modifiers.hasBackground()) {
-            backgroundDrawable =
-                    applyBackground(view, modifiers.getBackground(), backgroundDrawable);
-        }
-
-        if (modifiers.hasBorder()) {
-            backgroundDrawable = applyBorder(modifiers.getBorder(), backgroundDrawable);
-        }
-
-        if (backgroundDrawable != null) {
-            view.setBackground(backgroundDrawable);
-        }
-
-        return view;
-    }
-
-    // This is a little nasty; ArcLayoutWidget is just an interface, so we have no guarantee that
-    // the
-    // instance also extends View (as it should). Instead, just take a View in and rename this, and
-    // check that it's an ArcLayoutWidget internally.
-    private View applyModifiersToArcLayoutView(View view, ArcModifiers modifiers) {
-        if (!(view instanceof WearArcLayout.ArcLayoutWidget)) {
-            Log.e(
-                    TAG,
-                    "applyModifiersToArcLayoutView should only be called with an ArcLayoutWidget");
-            return view;
-        }
-
-        if (modifiers.hasClickable()) {
-            applyClickable(view, modifiers.getClickable());
-        }
-
-        if (modifiers.hasSemantics()) {
-            applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
-        }
-
-        return view;
-    }
-
-    private void applyFontStyle(FontStyle style, WearCurvedTextView textView) {
-        Typeface currentTypeface = textView.getTypeface();
-
-        if (!hasDefaultTypeface(style)) {
-            // Need to supply typefaceStyle when creating the typeface (will select specialist
-            // bold/italic
-            // typefaces), *and* when setting the typeface (will set synthetic bold/italic flags in
-            // Paint
-            // if they're not supported by the given typeface).
-            textView.setTypeface(
-                    createTypeface(style, currentTypeface), fontStyleToTypefaceStyle(style));
-        }
-
-        int currentPaintFlags = textView.getPaintFlags();
-
-        // Remove the bits we're setting
-        currentPaintFlags &= ~Paint.UNDERLINE_TEXT_FLAG;
-
-        if (style.hasUnderline() && style.getUnderline().getValue()) {
-            currentPaintFlags |= Paint.UNDERLINE_TEXT_FLAG;
-        }
-
-        textView.setPaintFlags(currentPaintFlags);
-
-        if (style.hasSize()) {
-            textView.setTextSize(
-                    TypedValue.applyDimension(
-                            TypedValue.COMPLEX_UNIT_SP,
-                            style.getSize().getValue(),
-                            mAppContext.getResources().getDisplayMetrics()));
-        }
-    }
-
-    private static int textAlignToAndroidGravity(TextAlignmentProp alignment) {
-        switch (alignment.getValue()) {
-            case TEXT_ALIGN_START:
-                return Gravity.START;
-            case TEXT_ALIGN_CENTER:
-                return Gravity.CENTER_HORIZONTAL;
-            case TEXT_ALIGN_END:
-                return Gravity.END;
-            case TEXT_ALIGN_UNDEFINED:
-            case UNRECOGNIZED:
-                return TEXT_ALIGN_DEFAULT;
-        }
-
-        return TEXT_ALIGN_DEFAULT;
-    }
-
-    @Nullable
-    private static TruncateAt textTruncationToEllipsize(TextOverflowProp type) {
-        switch (type.getValue()) {
-            case TEXT_OVERFLOW_TRUNCATE:
-                // A null TruncateAt disables adding an ellipsis.
-                return null;
-            case TEXT_OVERFLOW_ELLIPSIZE_END:
-                return TruncateAt.END;
-            case TEXT_OVERFLOW_UNDEFINED:
-            case UNRECOGNIZED:
-                return TEXT_OVERFLOW_DEFAULT;
-        }
-
-        return TEXT_OVERFLOW_DEFAULT;
-    }
-
-    @WearArcLayout.AnchorType
-    private static int anchorTypeToAnchorPos(ArcAnchorTypeProp type) {
-        switch (type.getValue()) {
-            case ARC_ANCHOR_START:
-                return WearArcLayout.ANCHOR_START;
-            case ARC_ANCHOR_CENTER:
-                return WearArcLayout.ANCHOR_CENTER;
-            case ARC_ANCHOR_END:
-                return WearArcLayout.ANCHOR_END;
-            case ARC_ANCHOR_UNDEFINED:
-            case UNRECOGNIZED:
-                return ARC_ANCHOR_DEFAULT;
-        }
-
-        return ARC_ANCHOR_DEFAULT;
-    }
-
-    private int dimensionToPx(ContainerDimension containerDimension) {
-        switch (containerDimension.getInnerCase()) {
-            case LINEAR_DIMENSION:
-                return safeDpToPx(containerDimension.getLinearDimension());
-            case EXPANDED_DIMENSION:
-                return LayoutParams.MATCH_PARENT;
-            case WRAPPED_DIMENSION:
-                return LayoutParams.WRAP_CONTENT;
-            case INNER_NOT_SET:
-                return dimensionToPx(CONTAINER_DIMENSION_DEFAULT);
-        }
-
-        return dimensionToPx(CONTAINER_DIMENSION_DEFAULT);
-    }
-
-    private static int extractTextColorArgb(FontStyle fontStyle) {
-        if (fontStyle.hasColor()) {
-            return fontStyle.getColor().getArgb();
-        } else {
-            return TEXT_COLOR_DEFAULT;
-        }
-    }
-
-    /**
-     * Returns an Android {@link Intent} that can perform the action defined in the given tile
-     * {@link LaunchAction}.
-     */
-    @Nullable
-    static Intent buildLaunchActionIntent(
-            @NonNull LaunchAction launchAction, @NonNull String clickableId) {
-        if (launchAction.hasAndroidActivity()) {
-            AndroidActivity activity = launchAction.getAndroidActivity();
-            Intent i =
-                    new Intent().setClassName(activity.getPackageName(), activity.getClassName());
-            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
-            if (!clickableId.isEmpty()) {
-                i.putExtra(TileProviderService.EXTRA_CLICKABLE_ID, clickableId);
-            }
-
-            return i;
-        }
-
-        return null;
-    }
-
-    static State buildState(LoadAction loadAction, String clickableId) {
-        // Get the state specified by the provider and add the last clicked clickable's ID to it.
-        return loadAction.getRequestState().toBuilder().setLastClickableId(clickableId).build();
-    }
-
-    @Nullable
-    private View inflateColumn(ViewGroup parent, Column column) {
-        ContainerDimension width =
-                column.hasWidth() ? column.getWidth() : CONTAINER_DIMENSION_DEFAULT;
-        ContainerDimension height =
-                column.hasHeight() ? column.getHeight() : CONTAINER_DIMENSION_DEFAULT;
-
-        if (!canMeasureContainer(width, height, column.getContentsList())) {
-            Log.w(TAG, "Column set to wrap but contents are unmeasurable. Ignoring.");
-            return null;
-        }
-
-        LinearLayout linearLayout = new LinearLayout(mAppContext);
-        linearLayout.setOrientation(LinearLayout.VERTICAL);
-
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-
-        linearLayout.setGravity(horizontalAlignmentToGravity(column.getHorizontalAlignment()));
-
-        layoutParams = updateLayoutParams(parent, layoutParams, width, height);
-
-        View wrappedView = applyModifiers(linearLayout, column.getModifiers());
-
-        parent.addView(wrappedView, layoutParams);
-        inflateLayoutElements(linearLayout, column.getContentsList());
-
-        return wrappedView;
-    }
-
-    @Nullable
-    private View inflateRow(ViewGroup parent, Row row) {
-        ContainerDimension width = row.hasWidth() ? row.getWidth() : CONTAINER_DIMENSION_DEFAULT;
-        ContainerDimension height = row.hasHeight() ? row.getHeight() : CONTAINER_DIMENSION_DEFAULT;
-
-        if (!canMeasureContainer(width, height, row.getContentsList())) {
-            Log.w(TAG, "Row set to wrap but contents are unmeasurable. Ignoring.");
-            return null;
-        }
-
-        LinearLayout linearLayout = new LinearLayout(mAppContext);
-        linearLayout.setOrientation(LinearLayout.HORIZONTAL);
-
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-
-        linearLayout.setGravity(verticalAlignmentToGravity(row.getVerticalAlignment()));
-
-        layoutParams = updateLayoutParams(parent, layoutParams, width, height);
-
-        View wrappedView = applyModifiers(linearLayout, row.getModifiers());
-
-        parent.addView(wrappedView, layoutParams);
-        inflateLayoutElements(linearLayout, row.getContentsList());
-
-        return wrappedView;
-    }
-
-    @Nullable
-    private View inflateBox(ViewGroup parent, Box box) {
-        ContainerDimension width = box.hasWidth() ? box.getWidth() : CONTAINER_DIMENSION_DEFAULT;
-        ContainerDimension height = box.hasHeight() ? box.getHeight() : CONTAINER_DIMENSION_DEFAULT;
-
-        if (!canMeasureContainer(width, height, box.getContentsList())) {
-            Log.w(TAG, "Box set to wrap but contents are unmeasurable. Ignoring.");
-            return null;
-        }
-
-        FrameLayout frame = new FrameLayout(mAppContext);
-
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-
-        layoutParams = updateLayoutParams(parent, layoutParams, width, height);
-
-        int gravity =
-                horizontalAlignmentToGravity(box.getHorizontalAlignment())
-                        | verticalAlignmentToGravity(box.getVerticalAlignment());
-
-        View wrappedView = applyModifiers(frame, box.getModifiers());
-
-        parent.addView(wrappedView, layoutParams);
-        inflateLayoutElements(frame, box.getContentsList());
-
-        // We can't set layout gravity to a FrameLayout ahead of time (and foregroundGravity only
-        // sets
-        // the gravity of the foreground Drawable). Go and apply gravity to the child.
-        applyGravityToFrameLayoutChildren(frame, gravity);
-
-        return wrappedView;
-    }
-
-    @Nullable
-    private View inflateSpacer(ViewGroup parent, Spacer spacer) {
-        int widthPx = safeDpToPx(spacer.getWidth().getLinearDimension());
-        int heightPx = safeDpToPx(spacer.getHeight().getLinearDimension());
-
-        if (widthPx == 0 && heightPx == 0) {
-            return null;
-        }
-
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-
-        // Modifiers cannot be applied to android's Space, so use a plain View if this Spacer has
-        // modifiers.
-        View view;
-        if (spacer.hasModifiers()) {
-            view = applyModifiers(new View(mAppContext), spacer.getModifiers());
-            layoutParams =
-                    updateLayoutParams(
-                            parent,
-                            layoutParams,
-                            spacerDimensionToContainerDimension(spacer.getWidth()),
-                            spacerDimensionToContainerDimension(spacer.getHeight()));
-        } else {
-            view = new Space(mAppContext);
-            view.setMinimumWidth(widthPx);
-            view.setMinimumHeight(heightPx);
-        }
-
-        parent.addView(view, layoutParams);
-
-        return view;
-    }
-
-    @Nullable
-    private View inflateArcSpacer(ViewGroup parent, ArcSpacer spacer) {
-        float lengthDegrees = max(0, spacer.getLength().getValue());
-        int thicknessPx = safeDpToPx(spacer.getThickness());
-
-        if (lengthDegrees == 0 && thicknessPx == 0) {
-            return null;
-        }
-
-        WearCurvedSpacer space = new WearCurvedSpacer(mAppContext);
-
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-
-        space.setSweepAngleDegrees(lengthDegrees);
-        space.setThicknessPx(thicknessPx);
-
-        View wrappedView = applyModifiersToArcLayoutView(space, spacer.getModifiers());
-        parent.addView(wrappedView, layoutParams);
-
-        return wrappedView;
-    }
-
-    private View inflateText(ViewGroup parent, Text text) {
-        TextView textView =
-                new TextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
-
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-
-        textView.setText(text.getText().getValue());
-
-        textView.setEllipsize(textTruncationToEllipsize(text.getOverflow()));
-        textView.setGravity(textAlignToAndroidGravity(text.getMultilineAlignment()));
-
-        if (text.hasMaxLines()) {
-            textView.setMaxLines(max(TEXT_MIN_LINES, text.getMaxLines().getValue()));
-        } else {
-            textView.setMaxLines(TEXT_MAX_LINES_DEFAULT);
-        }
-
-        // Setting colours **must** go after setting the Text Appearance, otherwise it will get
-        // immediately overridden.
-        if (text.hasFontStyle()) {
-            applyFontStyle(text.getFontStyle(), textView);
-        } else {
-            applyFontStyle(FontStyle.getDefaultInstance(), textView);
-        }
-
-        if (text.hasLineHeight()) {
-            float lineHeight =
-                    TypedValue.applyDimension(
-                            TypedValue.COMPLEX_UNIT_SP,
-                            text.getLineHeight().getValue(),
-                            mAppContext.getResources().getDisplayMetrics());
-            final float fontHeight = textView.getPaint().getFontSpacing();
-            if (lineHeight != fontHeight) {
-                textView.setLineSpacing(lineHeight - fontHeight, 1f);
-            }
-        }
-
-        View wrappedView = applyModifiers(textView, text.getModifiers());
-        parent.addView(wrappedView, layoutParams);
-
-        // We don't want the text to be screen-reader focusable, unless wrapped in a Audible. This
-        // prevents automatically reading out partial text (e.g. text in a row) etc.
-        textView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-
-        return wrappedView;
-    }
-
-    private View inflateArcText(ViewGroup parent, ArcText text) {
-        WearCurvedTextView textView =
-                new WearCurvedTextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
-
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-        layoutParams.width = LayoutParams.MATCH_PARENT;
-        layoutParams.height = LayoutParams.MATCH_PARENT;
-
-        textView.setText(text.getText().getValue());
-
-        if (text.hasFontStyle()) {
-            applyFontStyle(text.getFontStyle(), textView);
-        }
-
-        textView.setTextColor(extractTextColorArgb(text.getFontStyle()));
-
-        View wrappedView = applyModifiersToArcLayoutView(textView, text.getModifiers());
-        parent.addView(wrappedView, layoutParams);
-
-        return wrappedView;
-    }
-
-    private static boolean isZeroLengthImageDimension(ImageDimension dimension) {
-        return dimension.getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION
-                && dimension.getLinearDimension().getValue() == 0;
-    }
-
-    private static ContainerDimension imageDimensionToContainerDimension(ImageDimension dimension) {
-        switch (dimension.getInnerCase()) {
-            case LINEAR_DIMENSION:
-                return ContainerDimension.newBuilder()
-                        .setLinearDimension(dimension.getLinearDimension())
-                        .build();
-            case EXPANDED_DIMENSION:
-                return ContainerDimension.newBuilder()
-                        .setExpandedDimension(ExpandedDimensionProp.getDefaultInstance())
-                        .build();
-            case PROPORTIONAL_DIMENSION:
-                // A ratio size should be translated to a WRAP_CONTENT; the RatioViewWrapper will
-                // deal with
-                // the sizing of that.
-                return ContainerDimension.newBuilder()
-                        .setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
-                        .build();
-            case INNER_NOT_SET:
-                break;
-        }
-        // Caller should have already checked for this.
-        throw new IllegalArgumentException(
-                "ImageDimension has an unknown dimension type: " + dimension.getInnerCase().name());
-    }
-
-    private static ContainerDimension spacerDimensionToContainerDimension(
-            SpacerDimension dimension) {
-        switch (dimension.getInnerCase()) {
-            case LINEAR_DIMENSION:
-                return ContainerDimension.newBuilder()
-                        .setLinearDimension(dimension.getLinearDimension())
-                        .build();
-            case INNER_NOT_SET:
-                // A spacer is allowed to have missing dimension and this should be considered as
-                // 0dp.
-                return ContainerDimension.newBuilder()
-                        .setLinearDimension(DpProp.getDefaultInstance())
-                        .build();
-        }
-        // Caller should have already checked for this.
-        throw new IllegalArgumentException(
-                "SpacerDimension has an unknown dimension type: "
-                        + dimension.getInnerCase().name());
-    }
-
-    @SuppressWarnings("ExecutorTaskName")
-    @Nullable
-    private View inflateImage(ViewGroup parent, Image image) {
-        String protoResId = image.getResourceId().getValue();
-
-        // If either width or height isn't set, abort.
-        if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.INNER_NOT_SET
-                || image.getHeight().getInnerCase() == ImageDimension.InnerCase.INNER_NOT_SET) {
-            Log.w(TAG, "One of width and height not set on image " + protoResId);
-            return null;
-        }
-
-        // The image must occupy _some_ space.
-        if (isZeroLengthImageDimension(image.getWidth())
-                || isZeroLengthImageDimension(image.getHeight())) {
-            Log.w(TAG, "One of width and height was zero on image " + protoResId);
-            return null;
-        }
-
-        // Both dimensions can't be ratios.
-        if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION
-                && image.getHeight().getInnerCase()
-                        == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
-            Log.w(TAG, "Both width and height were proportional for image " + protoResId);
-            return null;
-        }
-
-        // Pull the ratio for the RatioViewWrapper. Was either argument a proportional dimension?
-        @Nullable Float ratio = RatioViewWrapper.UNDEFINED_ASPECT_RATIO;
-
-        if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
-            ratio = safeAspectRatioOrNull(image.getWidth().getProportionalDimension());
-        }
-
-        if (image.getHeight().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
-            ratio = safeAspectRatioOrNull(image.getHeight().getProportionalDimension());
-        }
-
-        if (ratio == null) {
-            Log.w(TAG, "Invalid aspect ratio for image " + protoResId);
-            return null;
-        }
-
-        ImageView imageView = new ImageView(mAppContext);
-
-        if (image.hasContentScaleMode()) {
-            imageView.setScaleType(
-                    contentScaleModeToScaleType(image.getContentScaleMode().getValue()));
-        }
-
-        if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION) {
-            imageView.setMinimumWidth(safeDpToPx(image.getWidth().getLinearDimension()));
-        }
-
-        if (image.getHeight().getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION) {
-            imageView.setMinimumHeight(safeDpToPx(image.getHeight().getLinearDimension()));
-        }
-
-        // We need to sort out the sizing of the widget now, so we can pass the correct params to
-        // RatioViewWrapper. First, translate the ImageSize to a ContainerSize. A ratio size should
-        // be translated to a WRAP_CONTENT; the RatioViewWrapper will deal with the sizing of that.
-        LayoutParams ratioWrapperLayoutParams = generateDefaultLayoutParams();
-        ratioWrapperLayoutParams =
-                updateLayoutParams(
-                        parent,
-                        ratioWrapperLayoutParams,
-                        imageDimensionToContainerDimension(image.getWidth()),
-                        imageDimensionToContainerDimension(image.getHeight()));
-
-        RatioViewWrapper ratioViewWrapper = new RatioViewWrapper(mAppContext);
-        ratioViewWrapper.setAspectRatio(ratio);
-        ratioViewWrapper.addView(imageView);
-
-        // Finally, wrap the image in any modifiers...
-        View wrappedView = applyModifiers(ratioViewWrapper, image.getModifiers());
-
-        parent.addView(wrappedView, ratioWrapperLayoutParams);
-
-        ListenableFuture<Drawable> drawableFuture = mResourceAccessors.getDrawable(protoResId);
-        if (drawableFuture.isDone()) {
-            // If the future is done, immediately draw.
-            setImageDrawable(imageView, drawableFuture, protoResId);
-        } else {
-            // Otherwise, handle the result on the UI thread.
-            drawableFuture.addListener(
-                    () -> setImageDrawable(imageView, drawableFuture, protoResId),
-                    ContextCompat.getMainExecutor(mAppContext));
-        }
-
-        return wrappedView;
-    }
-
-    private static void setImageDrawable(
-            ImageView imageView, Future<Drawable> drawableFuture, String protoResId) {
-        try {
-            imageView.setImageDrawable(drawableFuture.get());
-        } catch (ExecutionException | InterruptedException e) {
-            Log.w(TAG, "Could not get drawable for image " + protoResId);
-        }
-    }
-
-    @Nullable
-    private View inflateArcLine(ViewGroup parent, ArcLine line) {
-        float lengthDegrees = max(0, line.getLength().getValue());
-        int thicknessPx = safeDpToPx(line.getThickness());
-
-        if (lengthDegrees == 0 && thicknessPx == 0) {
-            return null;
-        }
-
-        WearCurvedLineView lineView = new WearCurvedLineView(mAppContext);
-
-        // A ArcLineView must always be the same width/height as its parent, so it can draw the line
-        // properly inside of those bounds.
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-        layoutParams.width = LayoutParams.MATCH_PARENT;
-        layoutParams.height = LayoutParams.MATCH_PARENT;
-
-        int lineColor = LINE_COLOR_DEFAULT;
-        if (line.hasColor()) {
-            lineColor = line.getColor().getArgb();
-        }
-
-        lineView.setThicknessPx(thicknessPx);
-        lineView.setSweepAngleDegrees(lengthDegrees);
-        lineView.setColor(lineColor);
-
-        View wrappedView = applyModifiersToArcLayoutView(lineView, line.getModifiers());
-        parent.addView(wrappedView, layoutParams);
-
-        return wrappedView;
-    }
-
-    @Nullable
-    private View inflateArc(ViewGroup parent, Arc arc) {
-        WearArcLayout arcLayout = new WearArcLayout(mAppContext);
-
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-        layoutParams.width = LayoutParams.MATCH_PARENT;
-        layoutParams.height = LayoutParams.MATCH_PARENT;
-
-        arcLayout.setAnchorAngleDegrees(arc.getAnchorAngle().getValue());
-        arcLayout.setAnchorType(anchorTypeToAnchorPos(arc.getAnchorType()));
-
-        // Add all children.
-        for (ArcLayoutElement child : arc.getContentsList()) {
-            @Nullable View childView = inflateArcLayoutElement(arcLayout, child);
-            if (childView != null) {
-                WearArcLayout.LayoutParams childLayoutParams =
-                        (WearArcLayout.LayoutParams) childView.getLayoutParams();
-                boolean rotate = false;
-                if (child.hasAdapter()) {
-                    rotate = child.getAdapter().getRotateContents().getValue();
-                }
-
-                // Apply rotation and gravity.
-                childLayoutParams.setRotate(rotate);
-                childLayoutParams.setVerticalAlignment(
-                        verticalAlignmentToArcVAlign(arc.getVerticalAlign()));
-            }
-        }
-
-        View wrappedView = applyModifiers(arcLayout, arc.getModifiers());
-        parent.addView(wrappedView, layoutParams);
-
-        return wrappedView;
-    }
-
-    private void applyStylesToSpan(
-            SpannableStringBuilder builder, int start, int end, FontStyle fontStyle) {
-        if (fontStyle.hasSize()) {
-            float fontSize =
-                    TypedValue.applyDimension(
-                            TypedValue.COMPLEX_UNIT_SP,
-                            fontStyle.getSize().getValue(),
-                            mAppContext.getResources().getDisplayMetrics());
-
-            AbsoluteSizeSpan span = new AbsoluteSizeSpan(round(fontSize));
-            builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
-        }
-
-        if (!hasDefaultTypeface(fontStyle)) {
-            MetricAffectingSpan span = createTypefaceSpan(fontStyle);
-            builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
-        }
-
-        if (fontStyle.getUnderline().getValue()) {
-            UnderlineSpan span = new UnderlineSpan();
-            builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
-        }
-
-        if (fontStyle.hasLetterSpacing()) {
-            LetterSpacingSpan span = new LetterSpacingSpan(fontStyle.getLetterSpacing().getValue());
-            builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
-        }
-
-        ForegroundColorSpan colorSpan;
-
-        colorSpan = new ForegroundColorSpan(extractTextColorArgb(fontStyle));
-
-        builder.setSpan(colorSpan, start, end, Spanned.SPAN_MARK_MARK);
-    }
-
-    private void applyModifiersToSpan(
-            SpannableStringBuilder builder, int start, int end, SpanModifiers modifiers) {
-        if (modifiers.hasClickable()) {
-            ClickableSpan clickableSpan = new TilesClickableSpan(modifiers.getClickable());
-
-            builder.setSpan(clickableSpan, start, end, Spanned.SPAN_MARK_MARK);
-        }
-    }
-
-    private SpannableStringBuilder inflateTextInSpannable(
-            SpannableStringBuilder builder, SpanText text) {
-        int currentPos = builder.length();
-        int lastPos = currentPos + text.getText().getValue().length();
-
-        builder.append(text.getText().getValue());
-
-        applyStylesToSpan(builder, currentPos, lastPos, text.getFontStyle());
-        applyModifiersToSpan(builder, currentPos, lastPos, text.getModifiers());
-
-        return builder;
-    }
-
-    @SuppressWarnings("ExecutorTaskName")
-    private SpannableStringBuilder inflateImageInSpannable(
-            SpannableStringBuilder builder, SpanImage protoImage, TextView textView) {
-        String protoResId = protoImage.getResourceId().getValue();
-
-        if (protoImage.getWidth().getValue() == 0 || protoImage.getHeight().getValue() == 0) {
-            Log.w(TAG, "One of width and height was zero on image " + protoResId);
-            return builder;
-        }
-
-        ListenableFuture<Drawable> drawableFuture = mResourceAccessors.getDrawable(protoResId);
-        if (drawableFuture.isDone()) {
-            // If the future is done, immediately add drawable to builder.
-            try {
-                Drawable drawable = drawableFuture.get();
-                appendSpanDrawable(builder, drawable, protoImage);
-            } catch (ExecutionException | InterruptedException e) {
-                Log.w(
-                        TAG,
-                        "Could not get drawable for image "
-                                + protoImage.getResourceId().getValue());
-            }
-        } else {
-            // If the future is not done, add an empty drawable to builder as a placeholder.
-            Drawable emptyDrawable = new ColorDrawable(Color.TRANSPARENT);
-            int startInclusive = builder.length();
-            ImageSpan emptyDrawableSpan = appendSpanDrawable(builder, emptyDrawable, protoImage);
-            int endExclusive = builder.length();
-
-            // When the future is done, replace the empty drawable with the received one.
-            drawableFuture.addListener(
-                    () -> {
-                        // Remove the placeholder. This should be safe, even with other modifiers
-                        // applied. This
-                        // just removes the single drawable span, and should leave other spans in
-                        // place.
-                        builder.removeSpan(emptyDrawableSpan);
-                        // Add the new drawable to the same range.
-                        setSpanDrawable(
-                                builder, drawableFuture, startInclusive, endExclusive, protoImage);
-                        // Update the TextView.
-                        textView.setText(builder);
-                    },
-                    ContextCompat.getMainExecutor(mAppContext));
-        }
-
-        return builder;
-    }
-
-    private ImageSpan appendSpanDrawable(
-            SpannableStringBuilder builder, Drawable drawable, SpanImage protoImage) {
-        drawable.setBounds(
-                0, 0, safeDpToPx(protoImage.getWidth()), safeDpToPx(protoImage.getHeight()));
-        ImageSpan imgSpan = new ImageSpan(drawable);
-
-        int startPos = builder.length();
-        builder.append(" ", imgSpan, Spanned.SPAN_MARK_MARK);
-        int endPos = builder.length();
-
-        applyModifiersToSpan(builder, startPos, endPos, protoImage.getModifiers());
-
-        return imgSpan;
-    }
-
-    private void setSpanDrawable(
-            SpannableStringBuilder builder,
-            ListenableFuture<Drawable> drawableFuture,
-            int startInclusive,
-            int endExclusive,
-            SpanImage protoImage) {
-        final String protoResourceId = protoImage.getResourceId().getValue();
-
-        try {
-            // Add the image span to the same range occupied by the placeholder.
-            Drawable drawable = drawableFuture.get();
-            drawable.setBounds(
-                    0, 0, safeDpToPx(protoImage.getWidth()), safeDpToPx(protoImage.getHeight()));
-            ImageSpan imgSpan = new ImageSpan(drawable);
-            builder.setSpan(
-                    imgSpan,
-                    startInclusive,
-                    endExclusive,
-                    android.text.Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
-        } catch (ExecutionException | InterruptedException e) {
-            Log.w(TAG, "Could not get drawable for image " + protoResourceId);
-        }
-    }
-
-    private View inflateSpannable(ViewGroup parent, Spannable spannable) {
-        TextView tv = new TextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
-        LayoutParams layoutParams = generateDefaultLayoutParams();
-
-        SpannableStringBuilder builder = new SpannableStringBuilder();
-
-        for (Span element : spannable.getSpansList()) {
-            switch (element.getInnerCase()) {
-                case IMAGE:
-                    SpanImage protoImage = element.getImage();
-                    builder = inflateImageInSpannable(builder, protoImage, tv);
-                    break;
-                case TEXT:
-                    SpanText protoText = element.getText();
-                    builder = inflateTextInSpannable(builder, protoText);
-                    break;
-                default:
-                    Log.w(TAG, "Unknown Span child type.");
-                    break;
-            }
-        }
-
-        tv.setEllipsize(textTruncationToEllipsize(spannable.getOverflow()));
-        tv.setGravity(horizontalAlignmentToGravity(spannable.getMultilineAlignment()));
-
-        if (spannable.hasMaxLines()) {
-            tv.setMaxLines(max(TEXT_MIN_LINES, spannable.getMaxLines().getValue()));
-        } else {
-            tv.setMaxLines(TEXT_MAX_LINES_DEFAULT);
-        }
-
-        if (spannable.hasLineSpacing()) {
-            float lineSpacing =
-                    TypedValue.applyDimension(
-                            TypedValue.COMPLEX_UNIT_SP,
-                            spannable.getLineSpacing().getValue(),
-                            mAppContext.getResources().getDisplayMetrics());
-            tv.setLineSpacing(lineSpacing, 1f);
-        }
-
-        tv.setText(builder);
-
-        View wrappedView = applyModifiers(tv, spannable.getModifiers());
-        parent.addView(applyModifiers(tv, spannable.getModifiers()), layoutParams);
-
-        return wrappedView;
-    }
-
-    @Nullable
-    private View inflateArcLayoutElement(ViewGroup parent, ArcLayoutElement element) {
-        View inflatedView = null;
-
-        switch (element.getInnerCase()) {
-            case ADAPTER:
-                // Fall back to the normal inflater.
-                inflatedView = inflateLayoutElement(parent, element.getAdapter().getContent());
-                break;
-
-            case SPACER:
-                inflatedView = inflateArcSpacer(parent, element.getSpacer());
-                break;
-
-            case LINE:
-                inflatedView = inflateArcLine(parent, element.getLine());
-                break;
-
-            case TEXT:
-                inflatedView = inflateArcText(parent, element.getText());
-                break;
-
-            case INNER_NOT_SET:
-                break;
-        }
-
-        if (inflatedView == null) {
-            // Covers null (returned when the childCase in the proto isn't known). Sadly, ProtoLite
-            // doesn't give us a way to access childCase's underlying tag, so we can't give any
-            // smarter
-            // error message here.
-            Log.w(TAG, "Unknown child type");
-        }
-
-        return inflatedView;
-    }
-
-    @Nullable
-    private View inflateLayoutElement(ViewGroup parent, LayoutElement element) {
-        // What is it?
-        View inflatedView = null;
-        switch (element.getInnerCase()) {
-            case COLUMN:
-                inflatedView = inflateColumn(parent, element.getColumn());
-                break;
-            case ROW:
-                inflatedView = inflateRow(parent, element.getRow());
-                break;
-            case BOX:
-                inflatedView = inflateBox(parent, element.getBox());
-                break;
-            case SPACER:
-                inflatedView = inflateSpacer(parent, element.getSpacer());
-                break;
-            case TEXT:
-                inflatedView = inflateText(parent, element.getText());
-                break;
-            case IMAGE:
-                inflatedView = inflateImage(parent, element.getImage());
-                break;
-            case ARC:
-                inflatedView = inflateArc(parent, element.getArc());
-                break;
-            case SPANNABLE:
-                inflatedView = inflateSpannable(parent, element.getSpannable());
-                break;
-            case INNER_NOT_SET:
-            default: // TODO(b/178359365): Remove default case
-                Log.w(TAG, "Unknown child type: " + element.getInnerCase().name());
-                break;
-        }
-
-        return inflatedView;
-    }
-
-    private boolean canMeasureContainer(
-            ContainerDimension containerWidth,
-            ContainerDimension containerHeight,
-            List<LayoutElement> elements) {
-        // We can't measure a container if it's set to wrap-contents but all of its contents are set
-        // to
-        // expand-to-parent. Such containers must not be displayed.
-        if (containerWidth.hasWrappedDimension() && !containsMeasurableWidth(elements)) {
-            return false;
-        }
-        if (containerHeight.hasWrappedDimension() && !containsMeasurableHeight(elements)) {
-            return false;
-        }
-        return true;
-    }
-
-    private boolean containsMeasurableWidth(List<LayoutElement> elements) {
-        for (LayoutElement element : elements) {
-            if (isWidthMeasurable(element)) {
-                // Enough to find a single element that is measurable.
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean containsMeasurableHeight(List<LayoutElement> elements) {
-        for (LayoutElement element : elements) {
-            if (isHeightMeasurable(element)) {
-                // Enough to find a single element that is measurable.
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean isWidthMeasurable(LayoutElement element) {
-        switch (element.getInnerCase()) {
-            case COLUMN:
-                return isMeasurable(element.getColumn().getWidth());
-            case ROW:
-                return isMeasurable(element.getRow().getWidth());
-            case BOX:
-                return isMeasurable(element.getBox().getWidth());
-            case SPACER:
-                return isMeasurable(element.getSpacer().getWidth());
-            case IMAGE:
-                return isMeasurable(element.getImage().getWidth());
-            case ARC:
-            case TEXT:
-            case SPANNABLE:
-                return true;
-            case INNER_NOT_SET:
-            default: // TODO(b/178359365): Remove default case
-                return false;
-        }
-    }
-
-    private boolean isHeightMeasurable(LayoutElement element) {
-        switch (element.getInnerCase()) {
-            case COLUMN:
-                return isMeasurable(element.getColumn().getHeight());
-            case ROW:
-                return isMeasurable(element.getRow().getHeight());
-            case BOX:
-                return isMeasurable(element.getBox().getHeight());
-            case SPACER:
-                return isMeasurable(element.getSpacer().getHeight());
-            case IMAGE:
-                return isMeasurable(element.getImage().getHeight());
-            case ARC:
-            case TEXT:
-            case SPANNABLE:
-                return true;
-            case INNER_NOT_SET:
-            default: // TODO(b/178359365): Remove default case
-                return false;
-        }
-    }
-
-    private boolean isMeasurable(ContainerDimension dimension) {
-        return dimensionToPx(dimension) != LayoutParams.MATCH_PARENT;
-    }
-
-    private static boolean isMeasurable(ImageDimension dimension) {
-        switch (dimension.getInnerCase()) {
-            case LINEAR_DIMENSION:
-            case PROPORTIONAL_DIMENSION:
-                return true;
-            case EXPANDED_DIMENSION:
-            case INNER_NOT_SET:
-                return false;
-        }
-        return false;
-    }
-
-    private static boolean isMeasurable(SpacerDimension dimension) {
-        switch (dimension.getInnerCase()) {
-            case LINEAR_DIMENSION:
-                return true;
-            case INNER_NOT_SET:
-                return false;
-        }
-        return false;
-    }
-
-    private void inflateLayoutElements(ViewGroup parent, List<LayoutElement> elements) {
-        for (LayoutElement element : elements) {
-            inflateLayoutElement(parent, element);
-        }
+        this.mRenderer = new TileRendererInternal(
+                appContext,
+                layout.toProto(),
+                StandardResourceAccessors.forLocalApp(appContext, resources).build(),
+                tilesTheme,
+                loadActionExecutor,
+                (s) -> loadActionListener.onClick(StateBuilders.State.fromProto(s)));
     }
 
     /**
@@ -1584,70 +112,6 @@
      */
     @Nullable
     public View inflate(@NonNull ViewGroup parent) {
-        // Go!
-        return inflateLayoutElement(parent, mLayout.getRoot());
-    }
-
-    private static void applyGravityToFrameLayoutChildren(FrameLayout parent, int gravity) {
-        for (int i = 0; i < parent.getChildCount(); i++) {
-            View child = parent.getChildAt(i);
-
-            // All children should have a LayoutParams already set...
-            if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) {
-                // This...shouldn't happen.
-                throw new IllegalStateException(
-                        "Layout params of child is not a descendant of FrameLayout.LayoutParams.");
-            }
-
-            // Children should grow out from the middle of the layout.
-            ((FrameLayout.LayoutParams) child.getLayoutParams()).gravity = gravity;
-        }
-    }
-
-    private static void applyAudibleParams(View view, String accessibilityLabel) {
-        view.setContentDescription(accessibilityLabel);
-        view.setFocusable(true);
-        view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-    }
-
-    /** Implementation of ClickableSpan for Tiles' Clickables. */
-    private class TilesClickableSpan extends ClickableSpan {
-        private final Clickable mClickable;
-
-        TilesClickableSpan(Clickable clickable) {
-            this.mClickable = clickable;
-        }
-
-        @Override
-        public void onClick(@NonNull View widget) {
-            Action action = mClickable.getOnClick();
-
-            switch (action.getValueCase()) {
-                case LAUNCH_ACTION:
-                    Intent i =
-                            buildLaunchActionIntent(action.getLaunchAction(), mClickable.getId());
-                    if (i != null) {
-                        if (i.resolveActivity(mAppContext.getPackageManager()) != null) {
-                            mAppContext.startActivity(i);
-                        }
-                    }
-                    break;
-                case LOAD_ACTION:
-                    mLoadActionExecutor.execute(
-                            () ->
-                                    mLoadActionListener.onClick(
-                                            buildState(
-                                                    action.getLoadAction(), mClickable.getId())));
-
-                    break;
-                case VALUE_NOT_SET:
-                    break;
-            }
-        }
-
-        @Override
-        public void updateDrawState(@NonNull TextPaint ds) {
-            // Don't change the underlying text appearance.
-        }
+        return mRenderer.inflate(parent);
     }
 }
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/AndroidResourceAccessor.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/AndroidResourceAccessor.java
similarity index 94%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/AndroidResourceAccessor.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/AndroidResourceAccessor.java
index 8b02a0b..2f46f99 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/AndroidResourceAccessor.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/AndroidResourceAccessor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import android.annotation.SuppressLint;
 import android.content.res.Resources;
@@ -22,7 +22,6 @@
 import android.graphics.drawable.Drawable;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
 import androidx.concurrent.futures.ResolvableFuture;
 import androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId;
 
@@ -30,10 +29,7 @@
 
 /**
  * Resource accessor for Android resources.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class AndroidResourceAccessor
         implements ResourceAccessors.AndroidImageResourceByResIdAccessor {
     private final Resources mAndroidResources;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/InlineResourceAccessor.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/InlineResourceAccessor.java
similarity index 96%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/InlineResourceAccessor.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/InlineResourceAccessor.java
index fc8f2d5..6a6964c 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/InlineResourceAccessor.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/InlineResourceAccessor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
@@ -25,7 +25,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import androidx.concurrent.futures.ResolvableFuture;
 import androidx.wear.tiles.proto.ResourceProto.ImageFormat;
 import androidx.wear.tiles.proto.ResourceProto.InlineImageResource;
@@ -36,10 +35,7 @@
 
 /**
  * Resource accessor for inline resources.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class InlineResourceAccessor implements ResourceAccessors.InlineImageResourceAccessor {
     private static final int RGB565_BYTES_PER_PX = 2;
 
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/LetterSpacingSpan.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/LetterSpacingSpan.java
similarity index 91%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/LetterSpacingSpan.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/LetterSpacingSpan.java
index 951f2c65..642be88 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/LetterSpacingSpan.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/LetterSpacingSpan.java
@@ -14,20 +14,16 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import android.text.TextPaint;
 import android.text.style.MetricAffectingSpan;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
 
 /**
  * LetterSpacingSpan class used to apply custom spacing between letters.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class LetterSpacingSpan extends MetricAffectingSpan {
     private final float mLetterSpacingEm;
 
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/RatioViewWrapper.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/RatioViewWrapper.java
similarity index 98%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/RatioViewWrapper.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/RatioViewWrapper.java
index 25e642e..2623851 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/RatioViewWrapper.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/RatioViewWrapper.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import android.content.Context;
 import android.util.AttributeSet;
@@ -24,7 +24,6 @@
 import androidx.annotation.AttrRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import androidx.annotation.StyleRes;
 
 /**
@@ -43,10 +42,7 @@
  *
  * <p>Note that if both axes are exact, this container does nothing; it will simply size the child
  * and itself according to the exact MeasureSpecs.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class RatioViewWrapper extends ViewGroup {
     /**
      * An undefined aspect ratio. If {@link #setAspectRatio} is called with this value, or never
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/ResourceAccessors.java
similarity index 98%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/ResourceAccessors.java
index 2f8ee24..b420042 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/ResourceAccessors.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import android.annotation.SuppressLint;
 import android.graphics.drawable.Drawable;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/StandardResourceAccessors.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/StandardResourceAccessors.java
similarity index 98%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/StandardResourceAccessors.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/StandardResourceAccessors.java
index 9e0b4a2..dad88b8 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/StandardResourceAccessors.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/StandardResourceAccessors.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import android.content.Context;
 import android.content.res.Resources;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
new file mode 100644
index 0000000..1637bca
--- /dev/null
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
@@ -0,0 +1,1633 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.renderer.internal;
+
+import static java.lang.Math.max;
+import static java.lang.Math.round;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils.TruncateAt;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.ClickableSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.Space;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import androidx.core.content.ContextCompat;
+import androidx.wear.tiles.TileProviderService;
+import androidx.wear.tiles.proto.ActionProto.Action;
+import androidx.wear.tiles.proto.ActionProto.AndroidActivity;
+import androidx.wear.tiles.proto.ActionProto.LaunchAction;
+import androidx.wear.tiles.proto.ActionProto.LoadAction;
+import androidx.wear.tiles.proto.DimensionProto.ContainerDimension;
+import androidx.wear.tiles.proto.DimensionProto.ContainerDimension.InnerCase;
+import androidx.wear.tiles.proto.DimensionProto.DpProp;
+import androidx.wear.tiles.proto.DimensionProto.ExpandedDimensionProp;
+import androidx.wear.tiles.proto.DimensionProto.ImageDimension;
+import androidx.wear.tiles.proto.DimensionProto.ProportionalDimensionProp;
+import androidx.wear.tiles.proto.DimensionProto.SpacerDimension;
+import androidx.wear.tiles.proto.DimensionProto.WrappedDimensionProp;
+import androidx.wear.tiles.proto.LayoutElementProto.Arc;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcAnchorTypeProp;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcLayoutElement;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcLine;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcSpacer;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcText;
+import androidx.wear.tiles.proto.LayoutElementProto.Box;
+import androidx.wear.tiles.proto.LayoutElementProto.Column;
+import androidx.wear.tiles.proto.LayoutElementProto.ContentScaleMode;
+import androidx.wear.tiles.proto.LayoutElementProto.FontStyle;
+import androidx.wear.tiles.proto.LayoutElementProto.HorizontalAlignmentProp;
+import androidx.wear.tiles.proto.LayoutElementProto.Image;
+import androidx.wear.tiles.proto.LayoutElementProto.Layout;
+import androidx.wear.tiles.proto.LayoutElementProto.LayoutElement;
+import androidx.wear.tiles.proto.LayoutElementProto.Row;
+import androidx.wear.tiles.proto.LayoutElementProto.Spacer;
+import androidx.wear.tiles.proto.LayoutElementProto.Span;
+import androidx.wear.tiles.proto.LayoutElementProto.SpanImage;
+import androidx.wear.tiles.proto.LayoutElementProto.SpanText;
+import androidx.wear.tiles.proto.LayoutElementProto.Spannable;
+import androidx.wear.tiles.proto.LayoutElementProto.Text;
+import androidx.wear.tiles.proto.LayoutElementProto.TextAlignmentProp;
+import androidx.wear.tiles.proto.LayoutElementProto.TextOverflowProp;
+import androidx.wear.tiles.proto.LayoutElementProto.VerticalAlignmentProp;
+import androidx.wear.tiles.proto.ModifiersProto.ArcModifiers;
+import androidx.wear.tiles.proto.ModifiersProto.Background;
+import androidx.wear.tiles.proto.ModifiersProto.Border;
+import androidx.wear.tiles.proto.ModifiersProto.Clickable;
+import androidx.wear.tiles.proto.ModifiersProto.Modifiers;
+import androidx.wear.tiles.proto.ModifiersProto.Padding;
+import androidx.wear.tiles.proto.ModifiersProto.SpanModifiers;
+import androidx.wear.tiles.proto.StateProto.State;
+import androidx.wear.tiles.renderer.R;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+
+/**
+ * Renderer for Wear Tiles.
+ *
+ * <p>This variant uses Android views to represent the contents of the Wear Tile.
+ */
+public final class TileRendererInternal {
+
+    private static final String TAG = "TileRenderer";
+
+    private static final int HALIGN_DEFAULT_GRAVITY = Gravity.CENTER_HORIZONTAL;
+    private static final int VALIGN_DEFAULT_GRAVITY = Gravity.CENTER_VERTICAL;
+    private static final int TEXT_ALIGN_DEFAULT = Gravity.CENTER_HORIZONTAL;
+    private static final ScaleType IMAGE_DEFAULT_SCALE_TYPE = ScaleType.FIT_CENTER;
+
+    @WearArcLayout.LayoutParams.VerticalAlignment
+    private static final int ARC_VALIGN_DEFAULT = WearArcLayout.LayoutParams.VALIGN_CENTER;
+
+    // This is pretty badly named; TruncateAt specifies where to place the ellipsis (or whether to
+    // marquee). Disabling truncation with null actually disables the _ellipsis_, but text will
+    // still be truncated.
+    @Nullable private static final TruncateAt TEXT_OVERFLOW_DEFAULT = null;
+
+    private static final int TEXT_COLOR_DEFAULT = 0xFFFFFFFF;
+    private static final int TEXT_MAX_LINES_DEFAULT = 1;
+    private static final int TEXT_MIN_LINES = 1;
+
+    private static final ContainerDimension CONTAINER_DIMENSION_DEFAULT =
+            ContainerDimension.newBuilder()
+                    .setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
+                    .build();
+
+    @WearArcLayout.AnchorType
+    private static final int ARC_ANCHOR_DEFAULT = WearArcLayout.ANCHOR_CENTER;
+
+    // White
+    private static final int LINE_COLOR_DEFAULT = 0xFFFFFFFF;
+
+    // Need to be package private so that TilesClickableSpan can see them.
+    final Context mAppContext;
+    final LoadActionListener mLoadActionListener;
+    final Executor mLoadActionExecutor;
+
+    private final Layout mLayout;
+    private final ResourceAccessors mResourceAccessors;
+
+    /**
+     * Listener for clicks on Clickable objects that have an Action to (re)load the contents of a
+     * tile.
+     */
+    public interface LoadActionListener {
+
+        /**
+         * Called when a Clickable that has a LoadAction is clicked.
+         *
+         * @param nextState The state that the next tile should be in.
+         */
+        void onClick(@NonNull State nextState);
+    }
+
+    /**
+     * Default constructor.
+     *
+     * @param appContext The application context.
+     * @param layout The portion of the Tile to render.
+     * @param resourceAccessors Accessors for the resources used for rendering this Tile.
+     * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
+     */
+    public TileRendererInternal(
+            @NonNull Context appContext,
+            @NonNull Layout layout,
+            @NonNull ResourceAccessors resourceAccessors,
+            @NonNull Executor loadActionExecutor,
+            @NonNull LoadActionListener loadActionListener) {
+        this(
+                appContext,
+                layout,
+                resourceAccessors,
+                /* tilesTheme= */ 0,
+                loadActionExecutor,
+                loadActionListener);
+    }
+
+    /**
+     * Default constructor.
+     *
+     * @param appContext The application context.
+     * @param layout The portion of the Tile to render.
+     * @param resourceAccessors Accessors for the resources used for rendering this Tile.
+     * @param tilesTheme The theme to use for this Tile instance. This can be used to customise
+     *     things like the default font family. Pass 0 to use the default theme.
+     * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
+     */
+    public TileRendererInternal(
+            @NonNull Context appContext,
+            @NonNull Layout layout,
+            @NonNull ResourceAccessors resourceAccessors,
+            @StyleRes int tilesTheme,
+            @NonNull Executor loadActionExecutor,
+            @NonNull LoadActionListener loadActionListener) {
+        if (tilesTheme == 0) {
+            tilesTheme = R.style.TilesBaseTheme;
+        }
+
+        this.mAppContext = new ContextThemeWrapper(appContext, tilesTheme);
+        this.mLayout = layout;
+        this.mResourceAccessors = resourceAccessors;
+        this.mLoadActionListener = loadActionListener;
+        this.mLoadActionExecutor = loadActionExecutor;
+    }
+
+    private int safeDpToPx(DpProp dpProp) {
+        return round(
+                max(0, dpProp.getValue()) * mAppContext.getResources().getDisplayMetrics().density);
+    }
+
+    @Nullable
+    private static Float safeAspectRatioOrNull(
+            ProportionalDimensionProp proportionalDimensionProp) {
+        final int dividend = proportionalDimensionProp.getAspectRatioWidth();
+        final int divisor = proportionalDimensionProp.getAspectRatioHeight();
+
+        if (dividend <= 0 || divisor <= 0) {
+            return null;
+        }
+        return (float) dividend / divisor;
+    }
+
+    /**
+     * Generates a generic LayoutParameters for use by all components. This just defaults to setting
+     * the width/height to WRAP_CONTENT.
+     *
+     * @return The default layout parameters.
+     */
+    private static ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+    }
+
+    private LayoutParams updateLayoutParamsInLinearLayout(
+            LinearLayout parent,
+            LayoutParams layoutParams,
+            ContainerDimension width,
+            ContainerDimension height) {
+        // This is a little bit fun. Tiles' semantics is that dimension = expand should eat all
+        // remaining space in that dimension, but not grow the parent. This is easy for standard
+        // containers, but a little trickier in rows and columns on Android.
+        //
+        // A Row (LinearLayout) supports this with width=0 and weight>0. After doing a layout pass,
+        // it will assign all remaining space to elements with width=0 and weight>0, biased by
+        // the weight. This causes problems if there are two (or more) "expand" elements in a
+        // row, which is itself set to WRAP_CONTENTS, and one of those elements has a measured
+        // width (e.g. Text). In that case, the LinearLayout will measure the text, then ensure
+        // that all elements with a weight set have their widths set according to the weight. For
+        // us, that means that _all_ elements with expand=true will size themselves to the same
+        // width as the Text, pushing out the bounds of the parent row. This happens on columns
+        // too, but of course regarding height.
+        //
+        // To get around this, if an element with expand=true is added to a row that is WRAP_CONTENT
+        // (e.g. a row with no explicit width, that is not expanded), we ignore the expand=true, and
+        // set the inner element's width to WRAP_CONTENT too.
+
+        LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(layoutParams);
+
+        // Handle the width
+        if (parent.getOrientation() == LinearLayout.HORIZONTAL
+                && width.getInnerCase() == InnerCase.EXPANDED_DIMENSION) {
+            // If the parent container would not normally have "remaining space", ignore the
+            // expand=true.
+            if (parent.getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
+                linearLayoutParams.width = LayoutParams.WRAP_CONTENT;
+            } else {
+                linearLayoutParams.width = 0;
+                linearLayoutParams.weight = 1;
+            }
+        } else {
+            linearLayoutParams.width = dimensionToPx(width);
+        }
+
+        // And the height
+        if (parent.getOrientation() == LinearLayout.VERTICAL
+                && height.getInnerCase() == InnerCase.EXPANDED_DIMENSION) {
+            // If the parent container would not normally have "remaining space", ignore the
+            // expand=true.
+            if (parent.getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
+                linearLayoutParams.height = LayoutParams.WRAP_CONTENT;
+            } else {
+                linearLayoutParams.height = 0;
+                linearLayoutParams.weight = 1;
+            }
+        } else {
+            linearLayoutParams.height = dimensionToPx(height);
+        }
+
+        return linearLayoutParams;
+    }
+
+    private LayoutParams updateLayoutParams(
+            ViewGroup parent,
+            LayoutParams layoutParams,
+            ContainerDimension width,
+            ContainerDimension height) {
+        if (parent instanceof LinearLayout) {
+            // LinearLayouts have a bunch of messy caveats in Tile when their children can be
+            // expanded; factor that case out to keep this clean.
+            return updateLayoutParamsInLinearLayout(
+                    (LinearLayout) parent, layoutParams, width, height);
+        } else {
+            layoutParams.width = dimensionToPx(width);
+            layoutParams.height = dimensionToPx(height);
+        }
+
+        return layoutParams;
+    }
+
+    private static int horizontalAlignmentToGravity(HorizontalAlignmentProp alignment) {
+        switch (alignment.getValue()) {
+            case HALIGN_START:
+                return Gravity.START;
+            case HALIGN_CENTER:
+                return Gravity.CENTER_HORIZONTAL;
+            case HALIGN_END:
+                return Gravity.END;
+            case HALIGN_LEFT:
+                return Gravity.LEFT;
+            case HALIGN_RIGHT:
+                return Gravity.RIGHT;
+            case UNRECOGNIZED:
+            case HALIGN_UNDEFINED:
+                return HALIGN_DEFAULT_GRAVITY;
+        }
+
+        return HALIGN_DEFAULT_GRAVITY;
+    }
+
+    private static int verticalAlignmentToGravity(VerticalAlignmentProp alignment) {
+        switch (alignment.getValue()) {
+            case VALIGN_TOP:
+                return Gravity.TOP;
+            case VALIGN_CENTER:
+                return Gravity.CENTER_VERTICAL;
+            case VALIGN_BOTTOM:
+                return Gravity.BOTTOM;
+            case UNRECOGNIZED:
+            case VALIGN_UNDEFINED:
+                return VALIGN_DEFAULT_GRAVITY;
+        }
+
+        return VALIGN_DEFAULT_GRAVITY;
+    }
+
+    @WearArcLayout.LayoutParams.VerticalAlignment
+    private static int verticalAlignmentToArcVAlign(VerticalAlignmentProp alignment) {
+        switch (alignment.getValue()) {
+            case VALIGN_TOP:
+                return WearArcLayout.LayoutParams.VALIGN_OUTER;
+            case VALIGN_CENTER:
+                return WearArcLayout.LayoutParams.VALIGN_CENTER;
+            case VALIGN_BOTTOM:
+                return WearArcLayout.LayoutParams.VALIGN_INNER;
+            case UNRECOGNIZED:
+            case VALIGN_UNDEFINED:
+                return ARC_VALIGN_DEFAULT;
+        }
+
+        return ARC_VALIGN_DEFAULT;
+    }
+
+    private static ScaleType contentScaleModeToScaleType(ContentScaleMode contentScaleMode) {
+        switch (contentScaleMode) {
+            case CONTENT_SCALE_MODE_FIT:
+                return ScaleType.FIT_CENTER;
+            case CONTENT_SCALE_MODE_CROP:
+                return ScaleType.CENTER_CROP;
+            case CONTENT_SCALE_MODE_FILL_BOUNDS:
+                return ScaleType.FIT_XY;
+            case CONTENT_SCALE_MODE_UNDEFINED:
+            case UNRECOGNIZED:
+                return IMAGE_DEFAULT_SCALE_TYPE;
+        }
+
+        return IMAGE_DEFAULT_SCALE_TYPE;
+    }
+
+    private static boolean isBold(FontStyle fontStyle) {
+        // Although this method could be a simple equality check against FONT_WEIGHT_BOLD, we list
+        // all current cases here so that this will become a compile time error as soon as a new
+        // FontWeight value is added to the schema. If this fails to build, then this means that
+        // an int typeface style is no longer enough to represent all FontWeight values and a
+        // customizable, per-weight text style must be introduced to TileRenderer to handle this.
+        // See b/176980535
+        switch (fontStyle.getWeight().getValue()) {
+            case FONT_WEIGHT_BOLD:
+                return true;
+            case FONT_WEIGHT_NORMAL:
+            case FONT_WEIGHT_UNDEFINED:
+            case UNRECOGNIZED:
+                return false;
+        }
+
+        return false;
+    }
+
+    private static int fontStyleToTypefaceStyle(FontStyle fontStyle) {
+        final boolean isBold = isBold(fontStyle);
+        final boolean isItalic = fontStyle.getItalic().getValue();
+
+        if (isBold && isItalic) {
+            return Typeface.BOLD_ITALIC;
+        } else if (isBold) {
+            return Typeface.BOLD;
+        } else if (isItalic) {
+            return Typeface.ITALIC;
+        } else {
+            return Typeface.NORMAL;
+        }
+    }
+
+    @SuppressWarnings("nullness")
+    private static Typeface createTypeface(
+            FontStyle fontStyle, @Nullable Typeface currentTypeface) {
+        return Typeface.create(currentTypeface, fontStyleToTypefaceStyle(fontStyle));
+    }
+
+    private static MetricAffectingSpan createTypefaceSpan(FontStyle fontStyle) {
+        return new StyleSpan(fontStyleToTypefaceStyle(fontStyle));
+    }
+
+    private static boolean hasDefaultTypeface(FontStyle fontStyle) {
+        return !fontStyle.getItalic().getValue() && !isBold(fontStyle);
+    }
+
+    private static void applyFontStyle(FontStyle style, TextView textView) {
+        Typeface currentTypeface = textView.getTypeface();
+
+        if (!hasDefaultTypeface(style)) {
+            // Need to supply typefaceStyle when creating the typeface (will select specialist
+            // bold/italic typefaces), *and* when setting the typeface (will set synthetic
+            // bold/italic flags in Paint if they're not supported by the given typeface).
+            textView.setTypeface(
+                    createTypeface(style, currentTypeface), fontStyleToTypefaceStyle(style));
+        }
+
+        int currentPaintFlags = textView.getPaintFlags();
+
+        // Remove the bits we're setting
+        currentPaintFlags &= ~Paint.UNDERLINE_TEXT_FLAG;
+
+        if (style.hasUnderline() && style.getUnderline().getValue()) {
+            currentPaintFlags |= Paint.UNDERLINE_TEXT_FLAG;
+        }
+
+        textView.setPaintFlags(currentPaintFlags);
+
+        if (style.hasSize()) {
+            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.getSize().getValue());
+        }
+
+        if (style.hasLetterSpacing()) {
+            textView.setLetterSpacing(style.getLetterSpacing().getValue());
+        }
+
+        textView.setTextColor(extractTextColorArgb(style));
+    }
+
+    private void applyClickable(View view, Clickable clickable) {
+        view.setTag(clickable.getId());
+
+        boolean hasAction = false;
+        switch (clickable.getOnClick().getValueCase()) {
+            case LAUNCH_ACTION:
+                Intent i =
+                        buildLaunchActionIntent(
+                                clickable.getOnClick().getLaunchAction(), clickable.getId());
+                if (i != null) {
+                    hasAction = true;
+                    view.setOnClickListener(
+                            v -> {
+                                if (i.resolveActivity(mAppContext.getPackageManager()) != null) {
+                                    mAppContext.startActivity(i);
+                                }
+                            });
+                }
+                break;
+            case LOAD_ACTION:
+                hasAction = true;
+                view.setOnClickListener(
+                        v ->
+                                mLoadActionListener.onClick(
+                                        buildState(
+                                                clickable.getOnClick().getLoadAction(),
+                                                clickable.getId())));
+                break;
+            case VALUE_NOT_SET:
+                break;
+        }
+
+        if (hasAction) {
+            // Apply ripple effect
+            TypedValue outValue = new TypedValue();
+            mAppContext
+                    .getTheme()
+                    .resolveAttribute(
+                            android.R.attr.selectableItemBackground,
+                            outValue,
+                            /* resolveRefs= */ true);
+            view.setForeground(mAppContext.getDrawable(outValue.resourceId));
+        }
+    }
+
+    private void applyPadding(View view, Padding padding) {
+        if (padding.getRtlAware().getValue()) {
+            view.setPaddingRelative(
+                    safeDpToPx(padding.getStart()),
+                    safeDpToPx(padding.getTop()),
+                    safeDpToPx(padding.getEnd()),
+                    safeDpToPx(padding.getBottom()));
+        } else {
+            view.setPadding(
+                    safeDpToPx(padding.getStart()),
+                    safeDpToPx(padding.getTop()),
+                    safeDpToPx(padding.getEnd()),
+                    safeDpToPx(padding.getBottom()));
+        }
+    }
+
+    private GradientDrawable applyBackground(
+            View view, Background background, @Nullable GradientDrawable drawable) {
+        if (drawable == null) {
+            drawable = new GradientDrawable();
+        }
+
+        if (background.hasColor()) {
+            drawable.setColor(background.getColor().getArgb());
+        }
+
+        if (background.hasCorner()) {
+            drawable.setCornerRadius(safeDpToPx(background.getCorner().getRadius()));
+            view.setClipToOutline(true);
+            view.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+        }
+
+        return drawable;
+    }
+
+    private GradientDrawable applyBorder(Border border, @Nullable GradientDrawable drawable) {
+        if (drawable == null) {
+            drawable = new GradientDrawable();
+        }
+
+        drawable.setStroke(safeDpToPx(border.getWidth()), border.getColor().getArgb());
+
+        return drawable;
+    }
+
+    private View applyModifiers(View view, Modifiers modifiers) {
+        if (modifiers.hasClickable()) {
+            applyClickable(view, modifiers.getClickable());
+        }
+
+        if (modifiers.hasSemantics()) {
+            applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
+        }
+
+        if (modifiers.hasPadding()) {
+            applyPadding(view, modifiers.getPadding());
+        }
+
+        GradientDrawable backgroundDrawable = null;
+
+        if (modifiers.hasBackground()) {
+            backgroundDrawable =
+                    applyBackground(view, modifiers.getBackground(), backgroundDrawable);
+        }
+
+        if (modifiers.hasBorder()) {
+            backgroundDrawable = applyBorder(modifiers.getBorder(), backgroundDrawable);
+        }
+
+        if (backgroundDrawable != null) {
+            view.setBackground(backgroundDrawable);
+        }
+
+        return view;
+    }
+
+    // This is a little nasty; ArcLayoutWidget is just an interface, so we have no guarantee that
+    // the instance also extends View (as it should). Instead, just take a View in and rename
+    // this, and check that it's an ArcLayoutWidget internally.
+    private View applyModifiersToArcLayoutView(View view, ArcModifiers modifiers) {
+        if (!(view instanceof WearArcLayout.ArcLayoutWidget)) {
+            Log.e(
+                    TAG,
+                    "applyModifiersToArcLayoutView should only be called with an ArcLayoutWidget");
+            return view;
+        }
+
+        if (modifiers.hasClickable()) {
+            applyClickable(view, modifiers.getClickable());
+        }
+
+        if (modifiers.hasSemantics()) {
+            applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
+        }
+
+        return view;
+    }
+
+    private void applyFontStyle(FontStyle style, WearCurvedTextView textView) {
+        Typeface currentTypeface = textView.getTypeface();
+
+        if (!hasDefaultTypeface(style)) {
+            // Need to supply typefaceStyle when creating the typeface (will select specialist
+            // bold/italic typefaces), *and* when setting the typeface (will set synthetic
+            // bold/italic flags in Paint if they're not supported by the given typeface).
+            textView.setTypeface(
+                    createTypeface(style, currentTypeface), fontStyleToTypefaceStyle(style));
+        }
+
+        int currentPaintFlags = textView.getPaintFlags();
+
+        // Remove the bits we're setting
+        currentPaintFlags &= ~Paint.UNDERLINE_TEXT_FLAG;
+
+        if (style.hasUnderline() && style.getUnderline().getValue()) {
+            currentPaintFlags |= Paint.UNDERLINE_TEXT_FLAG;
+        }
+
+        textView.setPaintFlags(currentPaintFlags);
+
+        if (style.hasSize()) {
+            textView.setTextSize(
+                    TypedValue.applyDimension(
+                            TypedValue.COMPLEX_UNIT_SP,
+                            style.getSize().getValue(),
+                            mAppContext.getResources().getDisplayMetrics()));
+        }
+    }
+
+    private static int textAlignToAndroidGravity(TextAlignmentProp alignment) {
+        switch (alignment.getValue()) {
+            case TEXT_ALIGN_START:
+                return Gravity.START;
+            case TEXT_ALIGN_CENTER:
+                return Gravity.CENTER_HORIZONTAL;
+            case TEXT_ALIGN_END:
+                return Gravity.END;
+            case TEXT_ALIGN_UNDEFINED:
+            case UNRECOGNIZED:
+                return TEXT_ALIGN_DEFAULT;
+        }
+
+        return TEXT_ALIGN_DEFAULT;
+    }
+
+    @Nullable
+    private static TruncateAt textTruncationToEllipsize(TextOverflowProp type) {
+        switch (type.getValue()) {
+            case TEXT_OVERFLOW_TRUNCATE:
+                // A null TruncateAt disables adding an ellipsis.
+                return null;
+            case TEXT_OVERFLOW_ELLIPSIZE_END:
+                return TruncateAt.END;
+            case TEXT_OVERFLOW_UNDEFINED:
+            case UNRECOGNIZED:
+                return TEXT_OVERFLOW_DEFAULT;
+        }
+
+        return TEXT_OVERFLOW_DEFAULT;
+    }
+
+    @WearArcLayout.AnchorType
+    private static int anchorTypeToAnchorPos(ArcAnchorTypeProp type) {
+        switch (type.getValue()) {
+            case ARC_ANCHOR_START:
+                return WearArcLayout.ANCHOR_START;
+            case ARC_ANCHOR_CENTER:
+                return WearArcLayout.ANCHOR_CENTER;
+            case ARC_ANCHOR_END:
+                return WearArcLayout.ANCHOR_END;
+            case ARC_ANCHOR_UNDEFINED:
+            case UNRECOGNIZED:
+                return ARC_ANCHOR_DEFAULT;
+        }
+
+        return ARC_ANCHOR_DEFAULT;
+    }
+
+    private int dimensionToPx(ContainerDimension containerDimension) {
+        switch (containerDimension.getInnerCase()) {
+            case LINEAR_DIMENSION:
+                return safeDpToPx(containerDimension.getLinearDimension());
+            case EXPANDED_DIMENSION:
+                return LayoutParams.MATCH_PARENT;
+            case WRAPPED_DIMENSION:
+                return LayoutParams.WRAP_CONTENT;
+            case INNER_NOT_SET:
+                return dimensionToPx(CONTAINER_DIMENSION_DEFAULT);
+        }
+
+        return dimensionToPx(CONTAINER_DIMENSION_DEFAULT);
+    }
+
+    private static int extractTextColorArgb(FontStyle fontStyle) {
+        if (fontStyle.hasColor()) {
+            return fontStyle.getColor().getArgb();
+        } else {
+            return TEXT_COLOR_DEFAULT;
+        }
+    }
+
+    /**
+     * Returns an Android {@link Intent} that can perform the action defined in the given tile
+     * {@link LaunchAction}.
+     */
+    @Nullable
+    static Intent buildLaunchActionIntent(
+            @NonNull LaunchAction launchAction, @NonNull String clickableId) {
+        if (launchAction.hasAndroidActivity()) {
+            AndroidActivity activity = launchAction.getAndroidActivity();
+            Intent i =
+                    new Intent().setClassName(activity.getPackageName(), activity.getClassName());
+            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+            if (!clickableId.isEmpty()) {
+                i.putExtra(TileProviderService.EXTRA_CLICKABLE_ID, clickableId);
+            }
+
+            return i;
+        }
+
+        return null;
+    }
+
+    static State buildState(LoadAction loadAction, String clickableId) {
+        // Get the state specified by the provider and add the last clicked clickable's ID to it.
+        return loadAction.getRequestState().toBuilder().setLastClickableId(clickableId).build();
+    }
+
+    @Nullable
+    private View inflateColumn(ViewGroup parent, Column column) {
+        ContainerDimension width =
+                column.hasWidth() ? column.getWidth() : CONTAINER_DIMENSION_DEFAULT;
+        ContainerDimension height =
+                column.hasHeight() ? column.getHeight() : CONTAINER_DIMENSION_DEFAULT;
+
+        if (!canMeasureContainer(width, height, column.getContentsList())) {
+            Log.w(TAG, "Column set to wrap but contents are unmeasurable. Ignoring.");
+            return null;
+        }
+
+        LinearLayout linearLayout = new LinearLayout(mAppContext);
+        linearLayout.setOrientation(LinearLayout.VERTICAL);
+
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+
+        linearLayout.setGravity(horizontalAlignmentToGravity(column.getHorizontalAlignment()));
+
+        layoutParams = updateLayoutParams(parent, layoutParams, width, height);
+
+        View wrappedView = applyModifiers(linearLayout, column.getModifiers());
+
+        parent.addView(wrappedView, layoutParams);
+        inflateLayoutElements(linearLayout, column.getContentsList());
+
+        return wrappedView;
+    }
+
+    @Nullable
+    private View inflateRow(ViewGroup parent, Row row) {
+        ContainerDimension width = row.hasWidth() ? row.getWidth() : CONTAINER_DIMENSION_DEFAULT;
+        ContainerDimension height = row.hasHeight() ? row.getHeight() : CONTAINER_DIMENSION_DEFAULT;
+
+        if (!canMeasureContainer(width, height, row.getContentsList())) {
+            Log.w(TAG, "Row set to wrap but contents are unmeasurable. Ignoring.");
+            return null;
+        }
+
+        LinearLayout linearLayout = new LinearLayout(mAppContext);
+        linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+
+        linearLayout.setGravity(verticalAlignmentToGravity(row.getVerticalAlignment()));
+
+        layoutParams = updateLayoutParams(parent, layoutParams, width, height);
+
+        View wrappedView = applyModifiers(linearLayout, row.getModifiers());
+
+        parent.addView(wrappedView, layoutParams);
+        inflateLayoutElements(linearLayout, row.getContentsList());
+
+        return wrappedView;
+    }
+
+    @Nullable
+    private View inflateBox(ViewGroup parent, Box box) {
+        ContainerDimension width = box.hasWidth() ? box.getWidth() : CONTAINER_DIMENSION_DEFAULT;
+        ContainerDimension height = box.hasHeight() ? box.getHeight() : CONTAINER_DIMENSION_DEFAULT;
+
+        if (!canMeasureContainer(width, height, box.getContentsList())) {
+            Log.w(TAG, "Box set to wrap but contents are unmeasurable. Ignoring.");
+            return null;
+        }
+
+        FrameLayout frame = new FrameLayout(mAppContext);
+
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+
+        layoutParams = updateLayoutParams(parent, layoutParams, width, height);
+
+        int gravity =
+                horizontalAlignmentToGravity(box.getHorizontalAlignment())
+                        | verticalAlignmentToGravity(box.getVerticalAlignment());
+
+        View wrappedView = applyModifiers(frame, box.getModifiers());
+
+        parent.addView(wrappedView, layoutParams);
+        inflateLayoutElements(frame, box.getContentsList());
+
+        // We can't set layout gravity to a FrameLayout ahead of time (and foregroundGravity only
+        // sets the gravity of the foreground Drawable). Go and apply gravity to the child.
+        applyGravityToFrameLayoutChildren(frame, gravity);
+
+        return wrappedView;
+    }
+
+    @Nullable
+    private View inflateSpacer(ViewGroup parent, Spacer spacer) {
+        int widthPx = safeDpToPx(spacer.getWidth().getLinearDimension());
+        int heightPx = safeDpToPx(spacer.getHeight().getLinearDimension());
+
+        if (widthPx == 0 && heightPx == 0) {
+            return null;
+        }
+
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+
+        // Modifiers cannot be applied to android's Space, so use a plain View if this Spacer has
+        // modifiers.
+        View view;
+        if (spacer.hasModifiers()) {
+            view = applyModifiers(new View(mAppContext), spacer.getModifiers());
+            layoutParams =
+                    updateLayoutParams(
+                            parent,
+                            layoutParams,
+                            spacerDimensionToContainerDimension(spacer.getWidth()),
+                            spacerDimensionToContainerDimension(spacer.getHeight()));
+        } else {
+            view = new Space(mAppContext);
+            view.setMinimumWidth(widthPx);
+            view.setMinimumHeight(heightPx);
+        }
+
+        parent.addView(view, layoutParams);
+
+        return view;
+    }
+
+    @Nullable
+    private View inflateArcSpacer(ViewGroup parent, ArcSpacer spacer) {
+        float lengthDegrees = max(0, spacer.getLength().getValue());
+        int thicknessPx = safeDpToPx(spacer.getThickness());
+
+        if (lengthDegrees == 0 && thicknessPx == 0) {
+            return null;
+        }
+
+        WearCurvedSpacer space = new WearCurvedSpacer(mAppContext);
+
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+
+        space.setSweepAngleDegrees(lengthDegrees);
+        space.setThicknessPx(thicknessPx);
+
+        View wrappedView = applyModifiersToArcLayoutView(space, spacer.getModifiers());
+        parent.addView(wrappedView, layoutParams);
+
+        return wrappedView;
+    }
+
+    private View inflateText(ViewGroup parent, Text text) {
+        TextView textView =
+                new TextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
+
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+
+        textView.setText(text.getText().getValue());
+
+        textView.setEllipsize(textTruncationToEllipsize(text.getOverflow()));
+        textView.setGravity(textAlignToAndroidGravity(text.getMultilineAlignment()));
+
+        if (text.hasMaxLines()) {
+            textView.setMaxLines(max(TEXT_MIN_LINES, text.getMaxLines().getValue()));
+        } else {
+            textView.setMaxLines(TEXT_MAX_LINES_DEFAULT);
+        }
+
+        // Setting colours **must** go after setting the Text Appearance, otherwise it will get
+        // immediately overridden.
+        if (text.hasFontStyle()) {
+            applyFontStyle(text.getFontStyle(), textView);
+        } else {
+            applyFontStyle(FontStyle.getDefaultInstance(), textView);
+        }
+
+        if (text.hasLineHeight()) {
+            float lineHeight =
+                    TypedValue.applyDimension(
+                            TypedValue.COMPLEX_UNIT_SP,
+                            text.getLineHeight().getValue(),
+                            mAppContext.getResources().getDisplayMetrics());
+            final float fontHeight = textView.getPaint().getFontSpacing();
+            if (lineHeight != fontHeight) {
+                textView.setLineSpacing(lineHeight - fontHeight, 1f);
+            }
+        }
+
+        View wrappedView = applyModifiers(textView, text.getModifiers());
+        parent.addView(wrappedView, layoutParams);
+
+        // We don't want the text to be screen-reader focusable, unless wrapped in a Audible. This
+        // prevents automatically reading out partial text (e.g. text in a row) etc.
+        textView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+
+        return wrappedView;
+    }
+
+    private View inflateArcText(ViewGroup parent, ArcText text) {
+        WearCurvedTextView textView =
+                new WearCurvedTextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
+
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+        layoutParams.width = LayoutParams.MATCH_PARENT;
+        layoutParams.height = LayoutParams.MATCH_PARENT;
+
+        textView.setText(text.getText().getValue());
+
+        if (text.hasFontStyle()) {
+            applyFontStyle(text.getFontStyle(), textView);
+        }
+
+        textView.setTextColor(extractTextColorArgb(text.getFontStyle()));
+
+        View wrappedView = applyModifiersToArcLayoutView(textView, text.getModifiers());
+        parent.addView(wrappedView, layoutParams);
+
+        return wrappedView;
+    }
+
+    private static boolean isZeroLengthImageDimension(ImageDimension dimension) {
+        return dimension.getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION
+                && dimension.getLinearDimension().getValue() == 0;
+    }
+
+    private static ContainerDimension imageDimensionToContainerDimension(ImageDimension dimension) {
+        switch (dimension.getInnerCase()) {
+            case LINEAR_DIMENSION:
+                return ContainerDimension.newBuilder()
+                        .setLinearDimension(dimension.getLinearDimension())
+                        .build();
+            case EXPANDED_DIMENSION:
+                return ContainerDimension.newBuilder()
+                        .setExpandedDimension(ExpandedDimensionProp.getDefaultInstance())
+                        .build();
+            case PROPORTIONAL_DIMENSION:
+                // A ratio size should be translated to a WRAP_CONTENT; the RatioViewWrapper will
+                // deal with the sizing of that.
+                return ContainerDimension.newBuilder()
+                        .setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
+                        .build();
+            case INNER_NOT_SET:
+                break;
+        }
+        // Caller should have already checked for this.
+        throw new IllegalArgumentException(
+                "ImageDimension has an unknown dimension type: " + dimension.getInnerCase().name());
+    }
+
+    private static ContainerDimension spacerDimensionToContainerDimension(
+            SpacerDimension dimension) {
+        switch (dimension.getInnerCase()) {
+            case LINEAR_DIMENSION:
+                return ContainerDimension.newBuilder()
+                        .setLinearDimension(dimension.getLinearDimension())
+                        .build();
+            case INNER_NOT_SET:
+                // A spacer is allowed to have missing dimension and this should be considered as
+                // 0dp.
+                return ContainerDimension.newBuilder()
+                        .setLinearDimension(DpProp.getDefaultInstance())
+                        .build();
+        }
+        // Caller should have already checked for this.
+        throw new IllegalArgumentException(
+                "SpacerDimension has an unknown dimension type: "
+                        + dimension.getInnerCase().name());
+    }
+
+    @SuppressWarnings("ExecutorTaskName")
+    @Nullable
+    private View inflateImage(ViewGroup parent, Image image) {
+        String protoResId = image.getResourceId().getValue();
+
+        // If either width or height isn't set, abort.
+        if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.INNER_NOT_SET
+                || image.getHeight().getInnerCase() == ImageDimension.InnerCase.INNER_NOT_SET) {
+            Log.w(TAG, "One of width and height not set on image " + protoResId);
+            return null;
+        }
+
+        // The image must occupy _some_ space.
+        if (isZeroLengthImageDimension(image.getWidth())
+                || isZeroLengthImageDimension(image.getHeight())) {
+            Log.w(TAG, "One of width and height was zero on image " + protoResId);
+            return null;
+        }
+
+        // Both dimensions can't be ratios.
+        if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION
+                && image.getHeight().getInnerCase()
+                == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
+            Log.w(TAG, "Both width and height were proportional for image " + protoResId);
+            return null;
+        }
+
+        // Pull the ratio for the RatioViewWrapper. Was either argument a proportional dimension?
+        @Nullable Float ratio = RatioViewWrapper.UNDEFINED_ASPECT_RATIO;
+
+        if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
+            ratio = safeAspectRatioOrNull(image.getWidth().getProportionalDimension());
+        }
+
+        if (image.getHeight().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
+            ratio = safeAspectRatioOrNull(image.getHeight().getProportionalDimension());
+        }
+
+        if (ratio == null) {
+            Log.w(TAG, "Invalid aspect ratio for image " + protoResId);
+            return null;
+        }
+
+        ImageView imageView = new ImageView(mAppContext);
+
+        if (image.hasContentScaleMode()) {
+            imageView.setScaleType(
+                    contentScaleModeToScaleType(image.getContentScaleMode().getValue()));
+        }
+
+        if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION) {
+            imageView.setMinimumWidth(safeDpToPx(image.getWidth().getLinearDimension()));
+        }
+
+        if (image.getHeight().getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION) {
+            imageView.setMinimumHeight(safeDpToPx(image.getHeight().getLinearDimension()));
+        }
+
+        // We need to sort out the sizing of the widget now, so we can pass the correct params to
+        // RatioViewWrapper. First, translate the ImageSize to a ContainerSize. A ratio size should
+        // be translated to a WRAP_CONTENT; the RatioViewWrapper will deal with the sizing of that.
+        LayoutParams ratioWrapperLayoutParams = generateDefaultLayoutParams();
+        ratioWrapperLayoutParams =
+                updateLayoutParams(
+                        parent,
+                        ratioWrapperLayoutParams,
+                        imageDimensionToContainerDimension(image.getWidth()),
+                        imageDimensionToContainerDimension(image.getHeight()));
+
+        RatioViewWrapper ratioViewWrapper = new RatioViewWrapper(mAppContext);
+        ratioViewWrapper.setAspectRatio(ratio);
+        ratioViewWrapper.addView(imageView);
+
+        // Finally, wrap the image in any modifiers...
+        View wrappedView = applyModifiers(ratioViewWrapper, image.getModifiers());
+
+        parent.addView(wrappedView, ratioWrapperLayoutParams);
+
+        ListenableFuture<Drawable> drawableFuture = mResourceAccessors.getDrawable(protoResId);
+        if (drawableFuture.isDone()) {
+            // If the future is done, immediately draw.
+            setImageDrawable(imageView, drawableFuture, protoResId);
+        } else {
+            // Otherwise, handle the result on the UI thread.
+            drawableFuture.addListener(
+                    () -> setImageDrawable(imageView, drawableFuture, protoResId),
+                    ContextCompat.getMainExecutor(mAppContext));
+        }
+
+        return wrappedView;
+    }
+
+    private static void setImageDrawable(
+            ImageView imageView, Future<Drawable> drawableFuture, String protoResId) {
+        try {
+            imageView.setImageDrawable(drawableFuture.get());
+        } catch (ExecutionException | InterruptedException e) {
+            Log.w(TAG, "Could not get drawable for image " + protoResId);
+        }
+    }
+
+    @Nullable
+    private View inflateArcLine(ViewGroup parent, ArcLine line) {
+        float lengthDegrees = max(0, line.getLength().getValue());
+        int thicknessPx = safeDpToPx(line.getThickness());
+
+        if (lengthDegrees == 0 && thicknessPx == 0) {
+            return null;
+        }
+
+        WearCurvedLineView lineView = new WearCurvedLineView(mAppContext);
+
+        // A ArcLineView must always be the same width/height as its parent, so it can draw the line
+        // properly inside of those bounds.
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+        layoutParams.width = LayoutParams.MATCH_PARENT;
+        layoutParams.height = LayoutParams.MATCH_PARENT;
+
+        int lineColor = LINE_COLOR_DEFAULT;
+        if (line.hasColor()) {
+            lineColor = line.getColor().getArgb();
+        }
+
+        lineView.setThicknessPx(thicknessPx);
+        lineView.setSweepAngleDegrees(lengthDegrees);
+        lineView.setColor(lineColor);
+
+        View wrappedView = applyModifiersToArcLayoutView(lineView, line.getModifiers());
+        parent.addView(wrappedView, layoutParams);
+
+        return wrappedView;
+    }
+
+    @Nullable
+    private View inflateArc(ViewGroup parent, Arc arc) {
+        WearArcLayout arcLayout = new WearArcLayout(mAppContext);
+
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+        layoutParams.width = LayoutParams.MATCH_PARENT;
+        layoutParams.height = LayoutParams.MATCH_PARENT;
+
+        arcLayout.setAnchorAngleDegrees(arc.getAnchorAngle().getValue());
+        arcLayout.setAnchorType(anchorTypeToAnchorPos(arc.getAnchorType()));
+
+        // Add all children.
+        for (ArcLayoutElement child : arc.getContentsList()) {
+            @Nullable View childView = inflateArcLayoutElement(arcLayout, child);
+            if (childView != null) {
+                WearArcLayout.LayoutParams childLayoutParams =
+                        (WearArcLayout.LayoutParams) childView.getLayoutParams();
+                boolean rotate = false;
+                if (child.hasAdapter()) {
+                    rotate = child.getAdapter().getRotateContents().getValue();
+                }
+
+                // Apply rotation and gravity.
+                childLayoutParams.setRotate(rotate);
+                childLayoutParams.setVerticalAlignment(
+                        verticalAlignmentToArcVAlign(arc.getVerticalAlign()));
+            }
+        }
+
+        View wrappedView = applyModifiers(arcLayout, arc.getModifiers());
+        parent.addView(wrappedView, layoutParams);
+
+        return wrappedView;
+    }
+
+    private void applyStylesToSpan(
+            SpannableStringBuilder builder, int start, int end, FontStyle fontStyle) {
+        if (fontStyle.hasSize()) {
+            float fontSize =
+                    TypedValue.applyDimension(
+                            TypedValue.COMPLEX_UNIT_SP,
+                            fontStyle.getSize().getValue(),
+                            mAppContext.getResources().getDisplayMetrics());
+
+            AbsoluteSizeSpan span = new AbsoluteSizeSpan(round(fontSize));
+            builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
+        }
+
+        if (!hasDefaultTypeface(fontStyle)) {
+            MetricAffectingSpan span = createTypefaceSpan(fontStyle);
+            builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
+        }
+
+        if (fontStyle.getUnderline().getValue()) {
+            UnderlineSpan span = new UnderlineSpan();
+            builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
+        }
+
+        if (fontStyle.hasLetterSpacing()) {
+            LetterSpacingSpan span = new LetterSpacingSpan(fontStyle.getLetterSpacing().getValue());
+            builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
+        }
+
+        ForegroundColorSpan colorSpan;
+
+        colorSpan = new ForegroundColorSpan(extractTextColorArgb(fontStyle));
+
+        builder.setSpan(colorSpan, start, end, Spanned.SPAN_MARK_MARK);
+    }
+
+    private void applyModifiersToSpan(
+            SpannableStringBuilder builder, int start, int end, SpanModifiers modifiers) {
+        if (modifiers.hasClickable()) {
+            ClickableSpan clickableSpan = new TilesClickableSpan(modifiers.getClickable());
+
+            builder.setSpan(clickableSpan, start, end, Spanned.SPAN_MARK_MARK);
+        }
+    }
+
+    private SpannableStringBuilder inflateTextInSpannable(
+            SpannableStringBuilder builder, SpanText text) {
+        int currentPos = builder.length();
+        int lastPos = currentPos + text.getText().getValue().length();
+
+        builder.append(text.getText().getValue());
+
+        applyStylesToSpan(builder, currentPos, lastPos, text.getFontStyle());
+        applyModifiersToSpan(builder, currentPos, lastPos, text.getModifiers());
+
+        return builder;
+    }
+
+    @SuppressWarnings("ExecutorTaskName")
+    private SpannableStringBuilder inflateImageInSpannable(
+            SpannableStringBuilder builder, SpanImage protoImage, TextView textView) {
+        String protoResId = protoImage.getResourceId().getValue();
+
+        if (protoImage.getWidth().getValue() == 0 || protoImage.getHeight().getValue() == 0) {
+            Log.w(TAG, "One of width and height was zero on image " + protoResId);
+            return builder;
+        }
+
+        ListenableFuture<Drawable> drawableFuture = mResourceAccessors.getDrawable(protoResId);
+        if (drawableFuture.isDone()) {
+            // If the future is done, immediately add drawable to builder.
+            try {
+                Drawable drawable = drawableFuture.get();
+                appendSpanDrawable(builder, drawable, protoImage);
+            } catch (ExecutionException | InterruptedException e) {
+                Log.w(
+                        TAG,
+                        "Could not get drawable for image "
+                                + protoImage.getResourceId().getValue());
+            }
+        } else {
+            // If the future is not done, add an empty drawable to builder as a placeholder.
+            Drawable emptyDrawable = new ColorDrawable(Color.TRANSPARENT);
+            int startInclusive = builder.length();
+            ImageSpan emptyDrawableSpan = appendSpanDrawable(builder, emptyDrawable, protoImage);
+            int endExclusive = builder.length();
+
+            // When the future is done, replace the empty drawable with the received one.
+            drawableFuture.addListener(
+                    () -> {
+                        // Remove the placeholder. This should be safe, even with other modifiers
+                        // applied. This just removes the single drawable span, and should leave
+                        // other spans in place.
+                        builder.removeSpan(emptyDrawableSpan);
+                        // Add the new drawable to the same range.
+                        setSpanDrawable(
+                                builder, drawableFuture, startInclusive, endExclusive, protoImage);
+                        // Update the TextView.
+                        textView.setText(builder);
+                    },
+                    ContextCompat.getMainExecutor(mAppContext));
+        }
+
+        return builder;
+    }
+
+    private ImageSpan appendSpanDrawable(
+            SpannableStringBuilder builder, Drawable drawable, SpanImage protoImage) {
+        drawable.setBounds(
+                0, 0, safeDpToPx(protoImage.getWidth()), safeDpToPx(protoImage.getHeight()));
+        ImageSpan imgSpan = new ImageSpan(drawable);
+
+        int startPos = builder.length();
+        builder.append(" ", imgSpan, Spanned.SPAN_MARK_MARK);
+        int endPos = builder.length();
+
+        applyModifiersToSpan(builder, startPos, endPos, protoImage.getModifiers());
+
+        return imgSpan;
+    }
+
+    private void setSpanDrawable(
+            SpannableStringBuilder builder,
+            ListenableFuture<Drawable> drawableFuture,
+            int startInclusive,
+            int endExclusive,
+            SpanImage protoImage) {
+        final String protoResourceId = protoImage.getResourceId().getValue();
+
+        try {
+            // Add the image span to the same range occupied by the placeholder.
+            Drawable drawable = drawableFuture.get();
+            drawable.setBounds(
+                    0, 0, safeDpToPx(protoImage.getWidth()), safeDpToPx(protoImage.getHeight()));
+            ImageSpan imgSpan = new ImageSpan(drawable);
+            builder.setSpan(
+                    imgSpan,
+                    startInclusive,
+                    endExclusive,
+                    android.text.Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+        } catch (ExecutionException | InterruptedException e) {
+            Log.w(TAG, "Could not get drawable for image " + protoResourceId);
+        }
+    }
+
+    private View inflateSpannable(ViewGroup parent, Spannable spannable) {
+        TextView tv = new TextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
+        LayoutParams layoutParams = generateDefaultLayoutParams();
+
+        SpannableStringBuilder builder = new SpannableStringBuilder();
+
+        for (Span element : spannable.getSpansList()) {
+            switch (element.getInnerCase()) {
+                case IMAGE:
+                    SpanImage protoImage = element.getImage();
+                    builder = inflateImageInSpannable(builder, protoImage, tv);
+                    break;
+                case TEXT:
+                    SpanText protoText = element.getText();
+                    builder = inflateTextInSpannable(builder, protoText);
+                    break;
+                default:
+                    Log.w(TAG, "Unknown Span child type.");
+                    break;
+            }
+        }
+
+        tv.setEllipsize(textTruncationToEllipsize(spannable.getOverflow()));
+        tv.setGravity(horizontalAlignmentToGravity(spannable.getMultilineAlignment()));
+
+        if (spannable.hasMaxLines()) {
+            tv.setMaxLines(max(TEXT_MIN_LINES, spannable.getMaxLines().getValue()));
+        } else {
+            tv.setMaxLines(TEXT_MAX_LINES_DEFAULT);
+        }
+
+        if (spannable.hasLineSpacing()) {
+            float lineSpacing =
+                    TypedValue.applyDimension(
+                            TypedValue.COMPLEX_UNIT_SP,
+                            spannable.getLineSpacing().getValue(),
+                            mAppContext.getResources().getDisplayMetrics());
+            tv.setLineSpacing(lineSpacing, 1f);
+        }
+
+        tv.setText(builder);
+
+        View wrappedView = applyModifiers(tv, spannable.getModifiers());
+        parent.addView(applyModifiers(tv, spannable.getModifiers()), layoutParams);
+
+        return wrappedView;
+    }
+
+    @Nullable
+    private View inflateArcLayoutElement(ViewGroup parent, ArcLayoutElement element) {
+        View inflatedView = null;
+
+        switch (element.getInnerCase()) {
+            case ADAPTER:
+                // Fall back to the normal inflater.
+                inflatedView = inflateLayoutElement(parent, element.getAdapter().getContent());
+                break;
+
+            case SPACER:
+                inflatedView = inflateArcSpacer(parent, element.getSpacer());
+                break;
+
+            case LINE:
+                inflatedView = inflateArcLine(parent, element.getLine());
+                break;
+
+            case TEXT:
+                inflatedView = inflateArcText(parent, element.getText());
+                break;
+
+            case INNER_NOT_SET:
+                break;
+        }
+
+        if (inflatedView == null) {
+            // Covers null (returned when the childCase in the proto isn't known). Sadly, ProtoLite
+            // doesn't give us a way to access childCase's underlying tag, so we can't give any
+            // smarter error message here.
+            Log.w(TAG, "Unknown child type");
+        }
+
+        return inflatedView;
+    }
+
+    @Nullable
+    private View inflateLayoutElement(ViewGroup parent, LayoutElement element) {
+        // What is it?
+        View inflatedView = null;
+        switch (element.getInnerCase()) {
+            case COLUMN:
+                inflatedView = inflateColumn(parent, element.getColumn());
+                break;
+            case ROW:
+                inflatedView = inflateRow(parent, element.getRow());
+                break;
+            case BOX:
+                inflatedView = inflateBox(parent, element.getBox());
+                break;
+            case SPACER:
+                inflatedView = inflateSpacer(parent, element.getSpacer());
+                break;
+            case TEXT:
+                inflatedView = inflateText(parent, element.getText());
+                break;
+            case IMAGE:
+                inflatedView = inflateImage(parent, element.getImage());
+                break;
+            case ARC:
+                inflatedView = inflateArc(parent, element.getArc());
+                break;
+            case SPANNABLE:
+                inflatedView = inflateSpannable(parent, element.getSpannable());
+                break;
+            case INNER_NOT_SET:
+            default: // TODO(b/178359365): Remove default case
+                Log.w(TAG, "Unknown child type: " + element.getInnerCase().name());
+                break;
+        }
+
+        return inflatedView;
+    }
+
+    private boolean canMeasureContainer(
+            ContainerDimension containerWidth,
+            ContainerDimension containerHeight,
+            List<LayoutElement> elements) {
+        // We can't measure a container if it's set to wrap-contents but all of its contents are set
+        // to expand-to-parent. Such containers must not be displayed.
+        if (containerWidth.hasWrappedDimension() && !containsMeasurableWidth(elements)) {
+            return false;
+        }
+        if (containerHeight.hasWrappedDimension() && !containsMeasurableHeight(elements)) {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean containsMeasurableWidth(List<LayoutElement> elements) {
+        for (LayoutElement element : elements) {
+            if (isWidthMeasurable(element)) {
+                // Enough to find a single element that is measurable.
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean containsMeasurableHeight(List<LayoutElement> elements) {
+        for (LayoutElement element : elements) {
+            if (isHeightMeasurable(element)) {
+                // Enough to find a single element that is measurable.
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isWidthMeasurable(LayoutElement element) {
+        switch (element.getInnerCase()) {
+            case COLUMN:
+                return isMeasurable(element.getColumn().getWidth());
+            case ROW:
+                return isMeasurable(element.getRow().getWidth());
+            case BOX:
+                return isMeasurable(element.getBox().getWidth());
+            case SPACER:
+                return isMeasurable(element.getSpacer().getWidth());
+            case IMAGE:
+                return isMeasurable(element.getImage().getWidth());
+            case ARC:
+            case TEXT:
+            case SPANNABLE:
+                return true;
+            case INNER_NOT_SET:
+            default: // TODO(b/178359365): Remove default case
+                return false;
+        }
+    }
+
+    private boolean isHeightMeasurable(LayoutElement element) {
+        switch (element.getInnerCase()) {
+            case COLUMN:
+                return isMeasurable(element.getColumn().getHeight());
+            case ROW:
+                return isMeasurable(element.getRow().getHeight());
+            case BOX:
+                return isMeasurable(element.getBox().getHeight());
+            case SPACER:
+                return isMeasurable(element.getSpacer().getHeight());
+            case IMAGE:
+                return isMeasurable(element.getImage().getHeight());
+            case ARC:
+            case TEXT:
+            case SPANNABLE:
+                return true;
+            case INNER_NOT_SET:
+            default: // TODO(b/178359365): Remove default case
+                return false;
+        }
+    }
+
+    private boolean isMeasurable(ContainerDimension dimension) {
+        return dimensionToPx(dimension) != LayoutParams.MATCH_PARENT;
+    }
+
+    private static boolean isMeasurable(ImageDimension dimension) {
+        switch (dimension.getInnerCase()) {
+            case LINEAR_DIMENSION:
+            case PROPORTIONAL_DIMENSION:
+                return true;
+            case EXPANDED_DIMENSION:
+            case INNER_NOT_SET:
+                return false;
+        }
+        return false;
+    }
+
+    private static boolean isMeasurable(SpacerDimension dimension) {
+        switch (dimension.getInnerCase()) {
+            case LINEAR_DIMENSION:
+                return true;
+            case INNER_NOT_SET:
+                return false;
+        }
+        return false;
+    }
+
+    private void inflateLayoutElements(ViewGroup parent, List<LayoutElement> elements) {
+        for (LayoutElement element : elements) {
+            inflateLayoutElement(parent, element);
+        }
+    }
+
+    /**
+     * Inflates a Tile into {@code parent}.
+     *
+     * @param parent The view to attach the tile into.
+     * @return The first child that was inflated. This may be null if the proto is empty the
+     *     top-level LayoutElement has no inner set, or the top-level LayoutElement contains an
+     *     unsupported inner type.
+     */
+    @Nullable
+    public View inflate(@NonNull ViewGroup parent) {
+        // Go!
+        return inflateLayoutElement(parent, mLayout.getRoot());
+    }
+
+    private static void applyGravityToFrameLayoutChildren(FrameLayout parent, int gravity) {
+        for (int i = 0; i < parent.getChildCount(); i++) {
+            View child = parent.getChildAt(i);
+
+            // All children should have a LayoutParams already set...
+            if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) {
+                // This...shouldn't happen.
+                throw new IllegalStateException(
+                        "Layout params of child is not a descendant of FrameLayout.LayoutParams.");
+            }
+
+            // Children should grow out from the middle of the layout.
+            ((FrameLayout.LayoutParams) child.getLayoutParams()).gravity = gravity;
+        }
+    }
+
+    private static void applyAudibleParams(View view, String accessibilityLabel) {
+        view.setContentDescription(accessibilityLabel);
+        view.setFocusable(true);
+        view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+    }
+
+    /** Implementation of ClickableSpan for Tiles' Clickables. */
+    private class TilesClickableSpan extends ClickableSpan {
+        private final Clickable mClickable;
+
+        TilesClickableSpan(Clickable clickable) {
+            this.mClickable = clickable;
+        }
+
+        @Override
+        public void onClick(@NonNull View widget) {
+            Action action = mClickable.getOnClick();
+
+            switch (action.getValueCase()) {
+                case LAUNCH_ACTION:
+                    Intent i =
+                            buildLaunchActionIntent(action.getLaunchAction(), mClickable.getId());
+                    if (i != null) {
+                        if (i.resolveActivity(mAppContext.getPackageManager()) != null) {
+                            mAppContext.startActivity(i);
+                        }
+                    }
+                    break;
+                case LOAD_ACTION:
+                    mLoadActionExecutor.execute(
+                            () ->
+                                    mLoadActionListener.onClick(
+                                            buildState(
+                                                    action.getLoadAction(), mClickable.getId())));
+
+                    break;
+                case VALUE_NOT_SET:
+                    break;
+            }
+        }
+
+        @Override
+        public void updateDrawState(@NonNull TextPaint ds) {
+            // Don't change the underlying text appearance.
+        }
+    }
+}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearArcLayout.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearArcLayout.java
similarity index 99%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearArcLayout.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearArcLayout.java
index 303e12f..5173a09 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearArcLayout.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearArcLayout.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import static java.lang.Math.asin;
 import static java.lang.Math.max;
@@ -36,6 +36,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.UiThread;
+import androidx.wear.tiles.renderer.R;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -70,13 +71,10 @@
  *
  * <p>An example of a widget which implements this interface is {@link WearCurvedTextView}, which
  * will lay itself out along the arc.
- *
- * @hide
  */
 // TODO(b/174649543): Replace this with the actual androidx.wear.widget.WearArcLayout when
 // we've reconciled the two.
 @UiThread
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class WearArcLayout extends ViewGroup {
 
     /**
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedLineView.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedLineView.java
similarity index 97%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedLineView.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedLineView.java
index 7a3fe9f..10a2fbb 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedLineView.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedLineView.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import static java.lang.Math.min;
 
@@ -32,7 +32,7 @@
 import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
+import androidx.wear.tiles.renderer.R;
 
 /**
  * A line, drawn inside an arc.
@@ -40,10 +40,7 @@
  * <p>This widget takes three parameters, the thickness of the line to draw, its sweep angle, and
  * the color to draw with. This widget will then draw an arc, with the specified thickness, around
  * its parent arc. The sweep angle is specified in degrees, clockwise.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class WearCurvedLineView extends View implements WearArcLayout.ArcLayoutWidget {
     private static final int DEFAULT_THICKNESS_PX = 0;
     private static final float DEFAULT_SWEEP_ANGLE_DEGREES = 0;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedSpacer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedSpacer.java
similarity index 95%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedSpacer.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedSpacer.java
index 16e937d2..dd29df2 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedSpacer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedSpacer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import android.content.Context;
 import android.content.res.TypedArray;
@@ -23,16 +23,13 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
+import androidx.wear.tiles.renderer.R;
 
 /**
  * A lightweight curved widget that represents space between elements inside an Arc. This does no
  * rendering; it simply causes the parent {@link WearArcLayout} to advance by {@code
  * sweepAngleDegrees}.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class WearCurvedSpacer extends View implements WearArcLayout.ArcLayoutWidget {
 
     private static final float DEFAULT_SWEEP_ANGLE_DEGREES = 0f;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedTextView.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedTextView.java
similarity index 99%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedTextView.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedTextView.java
index 35c0d42..da1d4b8 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedTextView.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedTextView.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
 
 import static java.lang.Math.cos;
 import static java.lang.Math.max;
@@ -45,16 +45,13 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
+import androidx.wear.tiles.renderer.R;
 
 /**
  * A WearCurvedTextView is a component allowing developers to easily write curved text following the
  * curvature of the largest circle that can be inscribed in the view. WearArcLayout could be used to
  * concatenate multiple curved texts, also layout together with other widgets such as icons.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 // TODO(b/174649543): Replace this with the actual androidx.wear.widget.WearCurvedTextView when
 // we've reconciled the two.
 public final class WearCurvedTextView extends View implements WearArcLayout.ArcLayoutWidget {
diff --git a/work/workmanager/src/main/java/androidx/work/impl/package-info.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/package-info.java
similarity index 77%
copy from work/workmanager/src/main/java/androidx/work/impl/package-info.java
copy to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/package-info.java
index 3f9ab0d..d5db66f 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/package-info.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,9 +15,11 @@
  */
 
 /**
+ * Internal implementation for Wear Tiles.
+ *
  * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.work.impl;
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.wear.tiles.renderer.internal;
 
 import androidx.annotation.RestrictTo;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineCache.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineCache.java
index 16fe3b2..829af88 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineCache.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineCache.java
@@ -20,19 +20,18 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.wear.tiles.builders.TimelineBuilders;
-import androidx.wear.tiles.proto.TimelineProto.TimeInterval;
-import androidx.wear.tiles.proto.TimelineProto.Timeline;
 import androidx.wear.tiles.proto.TimelineProto.TimelineEntry;
+import androidx.wear.tiles.timeline.internal.TilesTimelineCacheInternal;
 
 /**
  * Timeline cache for Wear Tiles. This will take in a full timeline, and return the appropriate
  * entry for the given time from {@code findTimelineEntryForTime}.
  */
 public final class TilesTimelineCache {
-    private final Timeline mTimeline;
+    private final TilesTimelineCacheInternal mCache;
 
-    public TilesTimelineCache(@NonNull TimelineBuilders.Timeline tile) {
-        this.mTimeline = tile.toProto();
+    public TilesTimelineCache(@NonNull TimelineBuilders.Timeline timeline) {
+        mCache = new TilesTimelineCacheInternal(timeline.toProto());
     }
 
     /**
@@ -48,41 +47,13 @@
     @MainThread
     @Nullable
     public TimelineBuilders.TimelineEntry findTimelineEntryForTime(long timeMillis) {
-        TimelineEntry currentEntry = null;
-        long currentEntryLength = Long.MAX_VALUE;
+        TimelineEntry entry = mCache.findTimelineEntryForTime(timeMillis);
 
-        // Iterate through, finding the _shortest_ valid timeline entry.
-        for (TimelineEntry entry : mTimeline.getTimelineEntriesList()) {
-            if (!entry.hasValidity()) {
-                // Only override a default if there's no more specific entry found.
-                if (currentEntryLength == Long.MAX_VALUE) {
-                    // Let's treat an entry with no validity as being a "default", as long as we
-                    // haven't found
-                    // any other valid entries
-                    currentEntry = entry;
-                }
-            } else {
-                TimeInterval validity = entry.getValidity();
-
-                long validityLength = validity.getEndMillis() - validity.getStartMillis();
-
-                if (validityLength > currentEntryLength) {
-                    continue;
-                }
-
-                if (validity.getStartMillis() <= timeMillis
-                        && timeMillis < validity.getEndMillis()) {
-                    currentEntry = entry;
-                    currentEntryLength = validityLength;
-                }
-            }
-        }
-
-        if (currentEntry == null) {
+        if (entry == null) {
             return null;
         }
 
-        return TimelineBuilders.TimelineEntry.fromProto(currentEntry);
+        return TimelineBuilders.TimelineEntry.fromProto(entry);
     }
 
     /**
@@ -101,47 +72,13 @@
     @MainThread
     @Nullable
     public TimelineBuilders.TimelineEntry findClosestTimelineEntry(long timeMillis) {
-        long currentEntryError = Long.MAX_VALUE;
-        TimelineEntry currentEntry = null;
+        TimelineEntry entry = mCache.findClosestTimelineEntry(timeMillis);
 
-        for (TimelineEntry entry : mTimeline.getTimelineEntriesList()) {
-            if (!entry.hasValidity()) {
-                // It's a default. This shouldn't happen if we've been called. Skip it.
-                continue;
-            }
-
-            TimeInterval validity = entry.getValidity();
-
-            if (!isTimeIntervalValid(validity)) {
-                continue;
-            }
-
-            // It's valid in this time interval. Shouldn't happen. Skip anyway.
-            if (validity.getStartMillis() <= timeMillis && timeMillis < validity.getEndMillis()) {
-                continue;
-            }
-
-            long error;
-
-            // It's in the future.
-            if (validity.getStartMillis() > timeMillis) {
-                error = validity.getStartMillis() - timeMillis;
-            } else {
-                // It's in the past.
-                error = timeMillis - validity.getEndMillis();
-            }
-
-            if (error < currentEntryError) {
-                currentEntry = entry;
-                currentEntryError = error;
-            }
-        }
-
-        if (currentEntry == null) {
+        if (entry == null) {
             return null;
         }
 
-        return TimelineBuilders.TimelineEntry.fromProto(currentEntry);
+        return TimelineBuilders.TimelineEntry.fromProto(entry);
     }
 
     /**
@@ -157,91 +94,6 @@
      */
     public long findCurrentTimelineEntryExpiry(
             @NonNull TimelineBuilders.TimelineEntry entry, long fromTimeMillis) {
-        long currentSmallestExpiry = Long.MAX_VALUE;
-        long entryValidityLength = Long.MAX_VALUE;
-
-        TimelineEntry protoEntry = entry.toProto();
-
-        if (protoEntry.hasValidity() && protoEntry.getValidity().getEndMillis() > fromTimeMillis) {
-            currentSmallestExpiry = protoEntry.getValidity().getEndMillis();
-            entryValidityLength =
-                    protoEntry.getValidity().getEndMillis()
-                            - protoEntry.getValidity().getStartMillis();
-        }
-
-        // Search for the starting edge of an overlapping period (i.e. one with startTime between
-        // entry.startTime and entry.endTime), with a validity period shorter than the one currently
-        // being considered.
-        for (TimelineEntry nextEntry : mTimeline.getTimelineEntriesList()) {
-            // The entry can't invalidate itself
-            if (nextEntry.equals(entry.toProto())) {
-                continue;
-            }
-
-            // Discard if nextEntry doesn't have a validity period. In this case, it's a default (so
-            // would potentially be used at entry.end_millis anyway).
-            if (!nextEntry.hasValidity()) {
-                continue;
-            }
-
-            TimeInterval nextEntryValidity = nextEntry.getValidity();
-
-            // Discard if the validity period is flat out invalid.
-            if (!isTimeIntervalValid(nextEntryValidity)) {
-                continue;
-            }
-
-            // Discard if the start time of nextEntry doesn't fall in the current period (it can't
-            // interrupt this entry, so this entry's expiry should be used).
-            if (protoEntry.hasValidity()) {
-                if (nextEntryValidity.getStartMillis() > protoEntry.getValidity().getEndMillis()
-                        || nextEntryValidity.getStartMillis()
-                                < protoEntry.getValidity().getStartMillis()) {
-                    continue;
-                }
-            }
-
-            // Discard if its start time is greater than the current smallest one we've found. In
-            // that
-            // case, the entry that gave us currentSmallestExpiry would be shown next.
-            if (nextEntryValidity.getStartMillis() > currentSmallestExpiry) {
-                continue;
-            }
-
-            // Discard if it's less than "fromTime". This prevents accidentally returning valid
-            // times in
-            // the past.
-            if (nextEntryValidity.getStartMillis() < fromTimeMillis) {
-                continue;
-            }
-
-            // Finally, consider whether the length of the validity period is shorter than the
-            // current
-            // one. If this doesn't hold, the current entry would be shown instead (the timeline
-            // entry
-            // with the shortest validity period is always shown if overlapping).
-            //
-            // We don't need to deal with the case of shortest validity between this entry, and an
-            // already
-            // chosen candidate time, as if we've got here, the start time of nextEntry is lower
-            // than
-            // the entry that is driving currentSmallestExpiry, so nextEntry would be shown
-            // regardless.
-            long nextEntryValidityLength =
-                    nextEntryValidity.getEndMillis() - nextEntryValidity.getStartMillis();
-
-            if (nextEntryValidityLength < entryValidityLength) {
-                // It's valid!
-                currentSmallestExpiry = nextEntryValidity.getStartMillis();
-            }
-        }
-
-        return currentSmallestExpiry;
-    }
-
-    private static boolean isTimeIntervalValid(TimeInterval timeInterval) {
-        // Zero-width (and "negative width") validity periods are not valid, and should never be
-        // considered.
-        return timeInterval.getEndMillis() > timeInterval.getStartMillis();
+        return mCache.findCurrentTimelineEntryExpiry(entry.toProto(), fromTimeMillis);
     }
 }
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineManager.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineManager.java
index b159cdb..7246d0d 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineManager.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineManager.java
@@ -16,20 +16,15 @@
 
 package androidx.wear.tiles.timeline;
 
-import static java.lang.Math.max;
-
 import android.app.AlarmManager;
-import android.app.AlarmManager.OnAlarmListener;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.wear.tiles.builders.LayoutElementBuilders;
 import androidx.wear.tiles.builders.TimelineBuilders;
+import androidx.wear.tiles.timeline.internal.TilesTimelineManagerInternal;
 
 import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
 
 /**
  * Manager for a single Wear Tiles timeline.
@@ -38,9 +33,9 @@
  * correct alarms to detect when a layout should be updated, and dispatch it to its listener.
  */
 public class TilesTimelineManager {
-    // 1 minute min delay between tiles.
     @VisibleForTesting
-    static final long MIN_TILE_UPDATE_DELAY_MILLIS = TimeUnit.MINUTES.toMillis(1);
+    static final long MIN_TILE_UPDATE_DELAY_MILLIS =
+            TilesTimelineManagerInternal.MIN_TILE_UPDATE_DELAY_MILLIS;
 
     /** Interface so this manager can retrieve the current time. */
     public interface Clock {
@@ -60,15 +55,7 @@
         void onLayoutUpdate(int token, @NonNull LayoutElementBuilders.Layout layout);
     }
 
-    private static final String TAG = "TimelineManager";
-
-    private final AlarmManager mAlarmManager;
-    private final Clock mClock;
-    private final TilesTimelineCache mCache;
-    private final Executor mListenerExecutor;
-    private final Listener mListener;
-    private final int mToken;
-    @Nullable private OnAlarmListener mAlarmListener = null;
+    private final TilesTimelineManagerInternal mManager;
 
     /**
      * Default constructor.
@@ -86,13 +73,17 @@
             @NonNull TimelineBuilders.Timeline timeline,
             int token,
             @NonNull Executor listenerExecutor,
-            @NonNull Listener listener) {
-        this.mAlarmManager = alarmManager;
-        this.mClock = clock;
-        this.mCache = new TilesTimelineCache(timeline);
-        this.mToken = token;
-        this.mListenerExecutor = listenerExecutor;
-        this.mListener = listener;
+            @NonNull Listener listener
+    ) {
+        mManager = new TilesTimelineManagerInternal(
+                alarmManager,
+                () -> clock.getCurrentTimeMillis(),
+                timeline.toProto(),
+                token,
+                listenerExecutor,
+                (t, entry) -> listener.onLayoutUpdate(t,
+                        LayoutElementBuilders.Layout.fromProto(entry.getLayout()))
+        );
     }
 
     /**
@@ -100,63 +91,11 @@
      * layout, and set its first alarm.
      */
     public void init() {
-        dispatchNextLayout();
+        mManager.init();
     }
 
     /** Tears down this Timeline Manager. This will ensure any set alarms are cleared up. */
     public void deInit() {
-        if (mAlarmListener != null) {
-            mAlarmManager.cancel(mAlarmListener);
-            mAlarmListener = null;
-        }
-    }
-
-    void dispatchNextLayout() {
-        if (mAlarmListener != null) {
-            mAlarmManager.cancel(mAlarmListener);
-            mAlarmListener = null;
-        }
-
-        long now = mClock.getCurrentTimeMillis();
-        TimelineBuilders.TimelineEntry entry = mCache.findTimelineEntryForTime(now);
-
-        if (entry == null) {
-            Log.d(TAG, "Could not find absolute timeline entry for time " + now);
-
-            entry = mCache.findClosestTimelineEntry(now);
-
-            if (entry == null) {
-                Log.w(TAG, "Could not find any timeline entry for time " + now);
-                return;
-            }
-        }
-
-        // Find when this entry should expire, and set a rollover alarm.
-        long expiryTime = mCache.findCurrentTimelineEntryExpiry(entry, now);
-
-        expiryTime = max(expiryTime, now + MIN_TILE_UPDATE_DELAY_MILLIS);
-
-        if (expiryTime != Long.MAX_VALUE) {
-            // This **has** to be an instantiation like this, in order for AlarmManager#cancel to
-            // work
-            // correctly (it doesn't work on method references).
-            mAlarmListener =
-                    new OnAlarmListener() {
-                        @Override
-                        public void onAlarm() {
-                            dispatchNextLayout();
-                        }
-                    };
-
-            // Run on the main thread (targetHandler = null). The update has to be on the main
-            // thread so
-            // it can mutate the layout, so we might as well just do everything there.
-            mAlarmManager.set(
-                    AlarmManager.RTC, expiryTime, TAG, mAlarmListener, /* targetHandler= */ null);
-        }
-
-        final LayoutElementBuilders.Layout layout = LayoutElementBuilders.Layout.fromProto(
-                entry.toProto().getLayout());
-        mListenerExecutor.execute(() -> mListener.onLayoutUpdate(mToken, layout));
+        mManager.deInit();
     }
 }
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineCacheInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineCacheInternal.java
new file mode 100644
index 0000000..b7e8029
--- /dev/null
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineCacheInternal.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.timeline.internal;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.wear.tiles.proto.TimelineProto.TimeInterval;
+import androidx.wear.tiles.proto.TimelineProto.Timeline;
+import androidx.wear.tiles.proto.TimelineProto.TimelineEntry;
+import androidx.wear.tiles.timeline.TilesTimelineCache;
+
+/**
+ * Timeline cache for Wear Tiles. This will take in a full timeline, and return the appropriate
+ * entry for the given time from {@code findTimelineEntryForTime}.
+ */
+public final class TilesTimelineCacheInternal {
+    private final Timeline mTimeline;
+
+    public TilesTimelineCacheInternal(@NonNull Timeline timeline) {
+        this.mTimeline = timeline;
+    }
+
+    /**
+     * Finds the entry which should be active at the given time. This will return the entry which
+     * has the _shortest_ validity period at the current time, if validity periods overlap. Note
+     * that an entry which has no validity period set will be considered a "default" and will be
+     * used if no other entries are suitable.
+     *
+     * @param timeMillis The time to base the search on, in milliseconds.
+     * @return The timeline entry which should be active at the given time. Returns {@code null} if
+     *     none are valid.
+     */
+    @MainThread
+    @Nullable
+    public TimelineEntry findTimelineEntryForTime(long timeMillis) {
+        TimelineEntry currentEntry = null;
+        long currentEntryLength = Long.MAX_VALUE;
+
+        // Iterate through, finding the _shortest_ valid timeline entry.
+        for (TimelineEntry entry : mTimeline.getTimelineEntriesList()) {
+            if (!entry.hasValidity()) {
+                // Only override a default if there's no more specific entry found.
+                if (currentEntryLength == Long.MAX_VALUE) {
+                    // Let's treat an entry with no validity as being a "default", as long as we
+                    // haven't found
+                    // any other valid entries
+                    currentEntry = entry;
+                }
+            } else {
+                TimeInterval validity = entry.getValidity();
+
+                long validityLength = validity.getEndMillis() - validity.getStartMillis();
+
+                if (validityLength > currentEntryLength) {
+                    continue;
+                }
+
+                if (validity.getStartMillis() <= timeMillis
+                        && timeMillis < validity.getEndMillis()) {
+                    currentEntry = entry;
+                    currentEntryLength = validityLength;
+                }
+            }
+        }
+
+        return currentEntry;
+    }
+
+    /**
+     * A (very) inexact version of {@link TilesTimelineCache#findTimelineEntryForTime(long)} which
+     * finds the closest timeline entry to the current time, regardless of validity. This should
+     * only used as a fallback if {@code findTimelineEntryForTime} fails, so it can attempt to at
+     * least show something.
+     *
+     * <p>By this point, we're technically in an error state, so just show _something_. Note that
+     * calling this if {@code findTimelineEntryForTime} returns a valid entry is invalid, and may
+     * lead to incorrect results.
+     *
+     * @param timeMillis The time to search from, in milliseconds.
+     * @return The timeline entry with validity period closest to {@code timeMillis}.
+     */
+    @MainThread
+    @Nullable
+    public TimelineEntry findClosestTimelineEntry(long timeMillis) {
+        long currentEntryError = Long.MAX_VALUE;
+        TimelineEntry currentEntry = null;
+
+        for (TimelineEntry entry : mTimeline.getTimelineEntriesList()) {
+            if (!entry.hasValidity()) {
+                // It's a default. This shouldn't happen if we've been called. Skip it.
+                continue;
+            }
+
+            TimeInterval validity = entry.getValidity();
+
+            if (!isTimeIntervalValid(validity)) {
+                continue;
+            }
+
+            // It's valid in this time interval. Shouldn't happen. Skip anyway.
+            if (validity.getStartMillis() <= timeMillis && timeMillis < validity.getEndMillis()) {
+                continue;
+            }
+
+            long error;
+
+            // It's in the future.
+            if (validity.getStartMillis() > timeMillis) {
+                error = validity.getStartMillis() - timeMillis;
+            } else {
+                // It's in the past.
+                error = timeMillis - validity.getEndMillis();
+            }
+
+            if (error < currentEntryError) {
+                currentEntry = entry;
+                currentEntryError = error;
+            }
+        }
+
+        return currentEntry;
+    }
+
+    /**
+     * Finds when the timeline entry {@code entry} should be considered "expired". This is either
+     * when it is no longer valid (i.e. end_millis), or when another entry should be presented
+     * instead.
+     *
+     * @param entry The entry to find the expiry time of.
+     * @param fromTimeMillis The time to start searching from. The returned time will never be lower
+     *     than the value passed here.
+     * @return The time in millis that {@code entry} should be considered to be expired. This value
+     *     will be {@link Long#MAX_VALUE} if {@code entry} does not expire.
+     */
+    public long findCurrentTimelineEntryExpiry(
+            @NonNull TimelineEntry entry, long fromTimeMillis) {
+        long currentSmallestExpiry = Long.MAX_VALUE;
+        long entryValidityLength = Long.MAX_VALUE;
+
+        if (entry.hasValidity() && entry.getValidity().getEndMillis() > fromTimeMillis) {
+            currentSmallestExpiry = entry.getValidity().getEndMillis();
+            entryValidityLength =
+                    entry.getValidity().getEndMillis()
+                            - entry.getValidity().getStartMillis();
+        }
+
+        // Search for the starting edge of an overlapping period (i.e. one with startTime between
+        // entry.startTime and entry.endTime), with a validity period shorter than the one currently
+        // being considered.
+        for (TimelineEntry nextEntry : mTimeline.getTimelineEntriesList()) {
+            // The entry can't invalidate itself
+            if (nextEntry.equals(entry)) {
+                continue;
+            }
+
+            // Discard if nextEntry doesn't have a validity period. In this case, it's a default (so
+            // would potentially be used at entry.end_millis anyway).
+            if (!nextEntry.hasValidity()) {
+                continue;
+            }
+
+            TimeInterval nextEntryValidity = nextEntry.getValidity();
+
+            // Discard if the validity period is flat out invalid.
+            if (!isTimeIntervalValid(nextEntryValidity)) {
+                continue;
+            }
+
+            // Discard if the start time of nextEntry doesn't fall in the current period (it can't
+            // interrupt this entry, so this entry's expiry should be used).
+            if (entry.hasValidity()) {
+                if (nextEntryValidity.getStartMillis() > entry.getValidity().getEndMillis()
+                        || nextEntryValidity.getStartMillis()
+                        < entry.getValidity().getStartMillis()) {
+                    continue;
+                }
+            }
+
+            // Discard if its start time is greater than the current smallest one we've found. In
+            // that
+            // case, the entry that gave us currentSmallestExpiry would be shown next.
+            if (nextEntryValidity.getStartMillis() > currentSmallestExpiry) {
+                continue;
+            }
+
+            // Discard if it's less than "fromTime". This prevents accidentally returning valid
+            // times in
+            // the past.
+            if (nextEntryValidity.getStartMillis() < fromTimeMillis) {
+                continue;
+            }
+
+            // Finally, consider whether the length of the validity period is shorter than the
+            // current
+            // one. If this doesn't hold, the current entry would be shown instead (the timeline
+            // entry
+            // with the shortest validity period is always shown if overlapping).
+            //
+            // We don't need to deal with the case of shortest validity between this entry, and an
+            // already
+            // chosen candidate time, as if we've got here, the start time of nextEntry is lower
+            // than
+            // the entry that is driving currentSmallestExpiry, so nextEntry would be shown
+            // regardless.
+            long nextEntryValidityLength =
+                    nextEntryValidity.getEndMillis() - nextEntryValidity.getStartMillis();
+
+            if (nextEntryValidityLength < entryValidityLength) {
+                // It's valid!
+                currentSmallestExpiry = nextEntryValidity.getStartMillis();
+            }
+        }
+
+        return currentSmallestExpiry;
+    }
+
+    private static boolean isTimeIntervalValid(TimeInterval timeInterval) {
+        // Zero-width (and "negative width") validity periods are not valid, and should never be
+        // considered.
+        return timeInterval.getEndMillis() > timeInterval.getStartMillis();
+    }
+}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineManagerInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineManagerInternal.java
new file mode 100644
index 0000000..abf1505
--- /dev/null
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineManagerInternal.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.timeline.internal;
+
+import static java.lang.Math.max;
+
+import android.app.AlarmManager;
+import android.app.AlarmManager.OnAlarmListener;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.wear.tiles.proto.TimelineProto;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Manager for a single Wear Tiles timeline.
+ *
+ * <p>This handles the dispatching of single Tile layouts from a full timeline. It will set the
+ * correct alarms to detect when a layout should be updated, and dispatch it to its listener.
+ */
+public class TilesTimelineManagerInternal {
+    // 1 minute min delay between tiles.
+    @VisibleForTesting
+    public static final long MIN_TILE_UPDATE_DELAY_MILLIS = TimeUnit.MINUTES.toMillis(1);
+
+    /** Interface so this manager can retrieve the current time. */
+    public interface Clock {
+        /** Get the current wall-clock time in millis. */
+        long getCurrentTimeMillis();
+    }
+
+    /** Type to listen for layout updates from a given timeline. */
+    public interface Listener {
+
+        /**
+         * Called when a timeline has a new layout to be displayed.
+         *
+         * @param token The token originally passed to {@link TilesTimelineManagerInternal}.
+         * @param entry The new timeline entry to use.
+         */
+        void onLayoutUpdate(int token, @NonNull TimelineProto.TimelineEntry entry);
+    }
+
+    private static final String TAG = "TimelineManager";
+
+    private final AlarmManager mAlarmManager;
+    private final Clock mClock;
+    private final TilesTimelineCacheInternal mCache;
+    private final Executor mListenerExecutor;
+    private final Listener mListener;
+    private final int mToken;
+    @Nullable private OnAlarmListener mAlarmListener = null;
+
+    /**
+     * Default constructor.
+     *
+     * @param alarmManager An AlarmManager instance suitable for setting RTC alarms on.
+     * @param clock A Clock to use to ascertain the current time (and hence which tile to show).
+     *     This should be synchronized to the same clock as used by {@code alarmManager}
+     * @param timeline The Tiles timeline to use.
+     * @param token A token, which will be passed to {@code listener}'s callback.
+     * @param listener A listener instance, called when a new timeline entry is available.
+     */
+    public TilesTimelineManagerInternal(
+            @NonNull AlarmManager alarmManager,
+            @NonNull Clock clock,
+            @NonNull TimelineProto.Timeline timeline,
+            int token,
+            @NonNull Executor listenerExecutor,
+            @NonNull Listener listener) {
+        this.mAlarmManager = alarmManager;
+        this.mClock = clock;
+        this.mCache = new TilesTimelineCacheInternal(timeline);
+        this.mToken = token;
+        this.mListenerExecutor = listenerExecutor;
+        this.mListener = listener;
+    }
+
+    /**
+     * Sets up this Timeline Manager. This will cause the timeline manager to dispatch the first
+     * layout, and set its first alarm.
+     */
+    public void init() {
+        dispatchNextLayout();
+    }
+
+    /** Tears down this Timeline Manager. This will ensure any set alarms are cleared up. */
+    public void deInit() {
+        if (mAlarmListener != null) {
+            mAlarmManager.cancel(mAlarmListener);
+            mAlarmListener = null;
+        }
+    }
+
+    void dispatchNextLayout() {
+        if (mAlarmListener != null) {
+            mAlarmManager.cancel(mAlarmListener);
+            mAlarmListener = null;
+        }
+
+        long now = mClock.getCurrentTimeMillis();
+        TimelineProto.TimelineEntry entry = mCache.findTimelineEntryForTime(now);
+
+        if (entry == null) {
+            Log.d(TAG, "Could not find absolute timeline entry for time " + now);
+
+            entry = mCache.findClosestTimelineEntry(now);
+
+            if (entry == null) {
+                Log.w(TAG, "Could not find any timeline entry for time " + now);
+                return;
+            }
+        }
+
+        // Find when this entry should expire, and set a rollover alarm.
+        long expiryTime = mCache.findCurrentTimelineEntryExpiry(entry, now);
+
+        expiryTime = max(expiryTime, now + MIN_TILE_UPDATE_DELAY_MILLIS);
+
+        if (expiryTime != Long.MAX_VALUE) {
+            // This **has** to be an instantiation like this, in order for AlarmManager#cancel to
+            // work
+            // correctly (it doesn't work on method references).
+            mAlarmListener =
+                    new OnAlarmListener() {
+                        @Override
+                        public void onAlarm() {
+                            dispatchNextLayout();
+                        }
+                    };
+
+            // Run on the main thread (targetHandler = null). The update has to be on the main
+            // thread so
+            // it can mutate the layout, so we might as well just do everything there.
+            mAlarmManager.set(
+                    AlarmManager.RTC, expiryTime, TAG, mAlarmListener, /* targetHandler= */ null);
+        }
+
+        final TimelineProto.TimelineEntry entryToDispatch = entry;
+        mListenerExecutor.execute(() -> mListener.onLayoutUpdate(mToken, entryToDispatch));
+    }
+}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/package-info.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/package-info.java
similarity index 77%
rename from work/workmanager/src/main/java/androidx/work/impl/package-info.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/package-info.java
index 3f9ab0d..92063a4 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/package-info.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,9 +15,11 @@
  */
 
 /**
+ * Internal implementation for Wear Tiles.
+ *
  * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.work.impl;
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.wear.tiles.timeline.internal;
 
 import androidx.annotation.RestrictTo;
diff --git a/wear/wear-complications-data/api/public_plus_experimental_current.txt b/wear/wear-complications-data/api/public_plus_experimental_current.txt
index 40ba553..8829abf 100644
--- a/wear/wear-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/wear-complications-data/api/public_plus_experimental_current.txt
@@ -243,7 +243,7 @@
   }
 
   public final class DataKt {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData asApiComplicationData(android.support.wearable.complications.ComplicationData);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData toApiComplicationData(android.support.wearable.complications.ComplicationData);
   }
 
   public final class EmptyComplicationData extends androidx.wear.complications.data.ComplicationData {
diff --git a/wear/wear-complications-data/api/restricted_current.txt b/wear/wear-complications-data/api/restricted_current.txt
index a315bbc..2c16c7d 100644
--- a/wear/wear-complications-data/api/restricted_current.txt
+++ b/wear/wear-complications-data/api/restricted_current.txt
@@ -259,11 +259,11 @@
   }
 
   public interface ComplicationText {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public android.support.wearable.complications.ComplicationText asWireComplicationText();
     method public long getNextChangeTime(long fromDateTimeMillis);
     method public CharSequence getTextAt(android.content.res.Resources resources, long dateTimeMillis);
     method public boolean isAlwaysEmpty();
     method public boolean returnsSameText(long firstDateTimeMillis, long secondDateTimeMillis);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public android.support.wearable.complications.ComplicationText toWireComplicationText();
   }
 
   public enum ComplicationType {
@@ -301,7 +301,7 @@
   }
 
   public final class DataKt {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData asApiComplicationData(android.support.wearable.complications.ComplicationData);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData toApiComplicationData(android.support.wearable.complications.ComplicationData);
   }
 
   public final class EmptyComplicationData extends androidx.wear.complications.data.ComplicationData {
@@ -535,6 +535,7 @@
   }
 
   public final class TextKt {
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationText toApiComplicationText(android.support.wearable.complications.TimeDependentText);
   }
 
   public final class TimeDifferenceComplicationText implements androidx.wear.complications.data.ComplicationText {
@@ -594,8 +595,8 @@
   }
 
   public final class TypeKt {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationType![] asApiComplicationTypes(int[]);
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static int[] asWireTypes(java.util.Collection<? extends androidx.wear.complications.data.ComplicationType>);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationType![] toApiComplicationTypes(int[]);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static int[] toWireTypes(java.util.Collection<? extends androidx.wear.complications.data.ComplicationType>);
   }
 
 }
diff --git a/wear/wear-complications-data/build.gradle b/wear/wear-complications-data/build.gradle
index dc18da2..d004c40 100644
--- a/wear/wear-complications-data/build.gradle
+++ b/wear/wear-complications-data/build.gradle
@@ -64,7 +64,7 @@
     name = "Android Wear Complications"
     publish = Publish.SNAPSHOT_AND_RELEASE
     mavenGroup = LibraryGroups.WEAR
-    mavenVersion = LibraryVersions.WEAR_COMPLICATIONS
+    mavenVersion = LibraryVersions.WEAR_COMPLICATIONS_DATA
     inceptionYear = "2020"
     description = "Android Wear Complications"
 }
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
index c74a757..a0a7b71 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
@@ -231,7 +231,7 @@
         int[] wireSupportedTypes = new int[supportedTypes.size()];
         int i = 0;
         for (ComplicationType supportedType : supportedTypes) {
-            wireSupportedTypes[i++] = supportedType.asWireComplicationType();
+            wireSupportedTypes[i++] = supportedType.toWireComplicationType();
         }
         intent.putExtra(ProviderChooserIntent.EXTRA_SUPPORTED_TYPES, wireSupportedTypes);
         return intent;
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
index 0ab5a45..70dea0a 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
@@ -30,7 +30,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.complications.data.ComplicationType
-import androidx.wear.complications.data.asApiComplicationData
+import androidx.wear.complications.data.toApiComplicationData
 import androidx.wear.utility.TraceEvent
 import kotlinx.coroutines.CompletableDeferred
 
@@ -152,13 +152,13 @@
         service.asBinder().linkToDeath(deathObserver, 0)
         if (!service.requestPreviewComplicationData(
                 providerComponent,
-                complicationType.asWireComplicationType(),
+                complicationType.toWireComplicationType(),
                 object : IPreviewComplicationDataCallback.Stub() {
                     override fun updateComplicationData(
                         data: android.support.wearable.complications.ComplicationData?
                     ) {
                         service.asBinder().unlinkToDeath(deathObserver, 0)
-                        result.complete(data?.asApiComplicationData())
+                        result.complete(data?.toApiComplicationData())
                     }
                 }
             )
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
index b701dbe..14a7de5 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
@@ -54,7 +54,7 @@
     internal fun createWireComplicationDataBuilder(): WireComplicationDataBuilder =
         cachedWireComplicationData?.let {
             WireComplicationDataBuilder(it)
-        } ?: WireComplicationDataBuilder(type.asWireComplicationType())
+        } ?: WireComplicationDataBuilder(type.toWireComplicationType())
 }
 
 /**
@@ -200,9 +200,9 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     override fun asWireComplicationData(): WireComplicationData =
         createWireComplicationDataBuilder().apply {
-            setShortText(text.asWireComplicationText())
-            setShortTitle(title?.asWireComplicationText())
-            setContentDescription(contentDescription?.asWireComplicationText())
+            setShortText(text.toWireComplicationText())
+            setShortTitle(title?.toWireComplicationText())
+            setContentDescription(contentDescription?.toWireComplicationText())
             monochromaticImage?.addToWireComplicationData(this)
             setTapAction(tapAction)
             setValidTimeRange(validTimeRange, this)
@@ -308,12 +308,12 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     override fun asWireComplicationData(): WireComplicationData =
         createWireComplicationDataBuilder().apply {
-            setLongText(text.asWireComplicationText())
-            setLongTitle(title?.asWireComplicationText())
+            setLongText(text.toWireComplicationText())
+            setLongTitle(title?.toWireComplicationText())
             monochromaticImage?.addToWireComplicationData(this)
             smallImage?.addToWireComplicationData(this)
             setTapAction(tapAction)
-            setContentDescription(contentDescription?.asWireComplicationText())
+            setContentDescription(contentDescription?.toWireComplicationText())
             setValidTimeRange(validTimeRange, this)
         }.build().also { cachedWireComplicationData = it }
 
@@ -429,10 +429,10 @@
             setRangedMinValue(min)
             setRangedMaxValue(max)
             monochromaticImage?.addToWireComplicationData(this)
-            setShortText(text?.asWireComplicationText())
-            setShortTitle(title?.asWireComplicationText())
+            setShortText(text?.toWireComplicationText())
+            setShortTitle(title?.toWireComplicationText())
             setTapAction(tapAction)
-            setContentDescription(contentDescription?.asWireComplicationText())
+            setContentDescription(contentDescription?.toWireComplicationText())
             setValidTimeRange(validTimeRange, this)
         }.build().also { cachedWireComplicationData = it }
 
@@ -512,7 +512,7 @@
     override fun asWireComplicationData(): WireComplicationData =
         createWireComplicationDataBuilder().apply {
             monochromaticImage.addToWireComplicationData(this)
-            setContentDescription(contentDescription?.asWireComplicationText())
+            setContentDescription(contentDescription?.toWireComplicationText())
             setTapAction(tapAction)
             setValidTimeRange(validTimeRange, this)
         }.build().also { cachedWireComplicationData = it }
@@ -593,7 +593,7 @@
     override fun asWireComplicationData(): WireComplicationData =
         createWireComplicationDataBuilder().apply {
             smallImage.addToWireComplicationData(this)
-            setContentDescription(contentDescription?.asWireComplicationText())
+            setContentDescription(contentDescription?.toWireComplicationText())
             setTapAction(tapAction)
             setValidTimeRange(validTimeRange, this)
         }.build().also { cachedWireComplicationData = it }
@@ -680,7 +680,7 @@
     override fun asWireComplicationData(): WireComplicationData =
         createWireComplicationDataBuilder().apply {
             setLargeImage(photoImage)
-            setContentDescription(contentDescription?.asWireComplicationText())
+            setContentDescription(contentDescription?.toWireComplicationText())
             setValidTimeRange(validTimeRange, this)
         }.build().also { cachedWireComplicationData = it }
 
@@ -757,8 +757,8 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     override fun asWireComplicationData(): WireComplicationData =
         createWireComplicationDataBuilder().apply {
-            setShortText(text?.asWireComplicationText())
-            setShortTitle(title?.asWireComplicationText())
+            setShortText(text?.toWireComplicationText())
+            setShortTitle(title?.toWireComplicationText())
             monochromaticImage?.addToWireComplicationData(this)
         }.build().also { cachedWireComplicationData = it }
 
@@ -771,38 +771,38 @@
 }
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun WireComplicationData.asApiComplicationData(): ComplicationData {
+public fun WireComplicationData.toApiComplicationData(): ComplicationData {
     val wireComplicationData = this
     return when (type) {
-        NoDataComplicationData.TYPE.asWireComplicationType() -> NoDataComplicationData()
+        NoDataComplicationData.TYPE.toWireComplicationType() -> NoDataComplicationData()
 
-        EmptyComplicationData.TYPE.asWireComplicationType() -> EmptyComplicationData()
+        EmptyComplicationData.TYPE.toWireComplicationType() -> EmptyComplicationData()
 
-        NotConfiguredComplicationData.TYPE.asWireComplicationType() ->
+        NotConfiguredComplicationData.TYPE.toWireComplicationType() ->
             NotConfiguredComplicationData()
 
-        ShortTextComplicationData.TYPE.asWireComplicationType() ->
-            ShortTextComplicationData.Builder(shortText!!.asApiComplicationText()).apply {
+        ShortTextComplicationData.TYPE.toWireComplicationType() ->
+            ShortTextComplicationData.Builder(shortText!!.toApiComplicationText()).apply {
                 setTapAction(tapAction)
                 setValidTimeRange(parseTimeRange())
-                setTitle(shortTitle?.asApiComplicationText())
+                setTitle(shortTitle?.toApiComplicationText())
                 setMonochromaticImage(parseIcon())
-                setContentDescription(contentDescription?.asApiComplicationText())
+                setContentDescription(contentDescription?.toApiComplicationText())
                 setCachedWireComplicationData(wireComplicationData)
             }.build()
 
-        LongTextComplicationData.TYPE.asWireComplicationType() ->
-            LongTextComplicationData.Builder(longText!!.asApiComplicationText()).apply {
+        LongTextComplicationData.TYPE.toWireComplicationType() ->
+            LongTextComplicationData.Builder(longText!!.toApiComplicationText()).apply {
                 setTapAction(tapAction)
                 setValidTimeRange(parseTimeRange())
-                setTitle(longTitle?.asApiComplicationText())
+                setTitle(longTitle?.toApiComplicationText())
                 setMonochromaticImage(parseIcon())
                 setSmallImage(parseSmallImage())
-                setContentDescription(contentDescription?.asApiComplicationText())
+                setContentDescription(contentDescription?.toApiComplicationText())
                 setCachedWireComplicationData(wireComplicationData)
             }.build()
 
-        RangedValueComplicationData.TYPE.asWireComplicationType() ->
+        RangedValueComplicationData.TYPE.toWireComplicationType() ->
             RangedValueComplicationData.Builder(
                 value = rangedValue, min = rangedMinValue,
                 max = rangedMaxValue
@@ -810,40 +810,40 @@
                 setTapAction(tapAction)
                 setValidTimeRange(parseTimeRange())
                 setMonochromaticImage(parseIcon())
-                setTitle(shortTitle?.asApiComplicationText())
-                setText(shortText?.asApiComplicationText())
-                setContentDescription(contentDescription?.asApiComplicationText())
+                setTitle(shortTitle?.toApiComplicationText())
+                setText(shortText?.toApiComplicationText())
+                setContentDescription(contentDescription?.toApiComplicationText())
                 setCachedWireComplicationData(wireComplicationData)
             }.build()
 
-        MonochromaticImageComplicationData.TYPE.asWireComplicationType() ->
+        MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
             MonochromaticImageComplicationData.Builder(parseIcon()!!).apply {
                 setTapAction(tapAction)
                 setValidTimeRange(parseTimeRange())
-                setContentDescription(contentDescription?.asApiComplicationText())
+                setContentDescription(contentDescription?.toApiComplicationText())
                 setCachedWireComplicationData(wireComplicationData)
             }.build()
 
-        SmallImageComplicationData.TYPE.asWireComplicationType() ->
+        SmallImageComplicationData.TYPE.toWireComplicationType() ->
             SmallImageComplicationData.Builder(parseSmallImage()!!).apply {
                 setTapAction(tapAction)
                 setValidTimeRange(parseTimeRange())
-                setContentDescription(contentDescription?.asApiComplicationText())
+                setContentDescription(contentDescription?.toApiComplicationText())
                 setCachedWireComplicationData(wireComplicationData)
             }.build()
 
-        PhotoImageComplicationData.TYPE.asWireComplicationType() ->
+        PhotoImageComplicationData.TYPE.toWireComplicationType() ->
             PhotoImageComplicationData.Builder(largeImage!!).apply {
                 setValidTimeRange(parseTimeRange())
-                setContentDescription(contentDescription?.asApiComplicationText())
+                setContentDescription(contentDescription?.toApiComplicationText())
                 setCachedWireComplicationData(wireComplicationData)
             }.build()
 
-        NoPermissionComplicationData.TYPE.asWireComplicationType() ->
+        NoPermissionComplicationData.TYPE.toWireComplicationType() ->
             NoPermissionComplicationData.Builder().apply {
                 setMonochromaticImage(parseIcon())
-                setTitle(shortTitle?.asApiComplicationText())
-                setText(shortText?.asApiComplicationText())
+                setTitle(shortTitle?.toApiComplicationText())
+                setText(shortText?.toApiComplicationText())
                 setCachedWireComplicationData(wireComplicationData)
             }.build()
 
@@ -879,7 +879,7 @@
 
 /** Some of the types, do not have any fields. This method provides a shorthard for that case. */
 internal fun asPlainWireComplicationData(type: ComplicationType) =
-    WireComplicationDataBuilder(type.asWireComplicationType()).build()
+    WireComplicationDataBuilder(type.toWireComplicationType()).build()
 
 internal fun setValidTimeRange(validTimeRange: TimeRange?, data: WireComplicationDataBuilder) {
     validTimeRange?.let {
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
index c05db28..fd88d45 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
@@ -30,6 +30,8 @@
 private typealias WireComplicationTextTimeFormatBuilder =
     android.support.wearable.complications.ComplicationText.TimeFormatBuilder
 
+private typealias WireTimeDependentText = android.support.wearable.complications.TimeDependentText
+
 /**
  * The text within a complication.
  *
@@ -69,7 +71,7 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public fun asWireComplicationText(): WireComplicationText
+    public fun toWireComplicationText(): WireComplicationText
 }
 
 /** A [ComplicationText] that contains plain text. */
@@ -351,13 +353,42 @@
 
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationText() = delegate
+    override fun toWireComplicationText() = delegate
 }
 
 /** Converts a [WireComplicationText] into an equivalent [ComplicationText] instead. */
-internal fun WireComplicationText.asApiComplicationText(): ComplicationText =
+internal fun WireComplicationText.toApiComplicationText(): ComplicationText =
     DelegatingComplicationText(this)
 
 /** Converts a [TimeZone] into an equivalent [java.util.TimeZone]. */
 internal fun TimeZone.asJavaTimeZone(): java.util.TimeZone =
     java.util.TimeZone.getTimeZone(this.id)
+
+/** [ComplicationText] implementation that delegates to a [WireTimeDependentText] instance. */
+private class DelegatingTimeDependentText(
+    private val delegate: WireTimeDependentText
+) : ComplicationText {
+    override fun getTextAt(resources: Resources, dateTimeMillis: Long) =
+        delegate.getTextAt(resources, dateTimeMillis)
+
+    override fun returnsSameText(firstDateTimeMillis: Long, secondDateTimeMillis: Long) =
+        delegate.returnsSameText(firstDateTimeMillis, secondDateTimeMillis)
+
+    override fun getNextChangeTime(fromDateTimeMillis: Long) =
+        delegate.getNextChangeTime(fromDateTimeMillis)
+
+    override fun isAlwaysEmpty() = false
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    override fun toWireComplicationText(): WireComplicationText {
+        throw UnsupportedOperationException(
+            "DelegatingTimeDependentText doesn't support asWireComplicationText"
+        )
+    }
+}
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun WireTimeDependentText.toApiComplicationText(): ComplicationText =
+    DelegatingTimeDependentText(this)
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Type.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Type.kt
index 4c9eb33..97d4dfc 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Type.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Type.kt
@@ -39,7 +39,7 @@
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
-    public fun asWireComplicationType(): Int = wireType
+    public fun toWireComplicationType(): Int = wireType
 
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -80,7 +80,7 @@
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @JvmStatic
-        public fun toWireTypes(types: Collection<ComplicationType>): IntArray = types.asWireTypes()
+        public fun toWireTypes(types: Collection<ComplicationType>): IntArray = types.toWireTypes()
 
         /**
          * Converts an array of integer values used for serialization into the corresponding array
@@ -95,7 +95,7 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @JvmStatic
         public fun fromWireTypes(types: IntArray): Array<ComplicationType> =
-            types.asApiComplicationTypes()
+            types.toApiComplicationTypes()
 
         /**
          * Converts an array of integer values used for serialization into the corresponding list
@@ -119,8 +119,8 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun Collection<ComplicationType>.asWireTypes(): IntArray =
-    this.map { it.asWireComplicationType() }.toIntArray()
+public fun Collection<ComplicationType>.toWireTypes(): IntArray =
+    this.map { it.toWireComplicationType() }.toIntArray()
 
 /**
  * Converts an array of integer values uses for serialization into the corresponding array
@@ -131,5 +131,5 @@
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun IntArray.asApiComplicationTypes(): Array<ComplicationType> =
+public fun IntArray.toApiComplicationTypes(): Array<ComplicationType> =
     this.map { ComplicationType.fromWireType(it) }.toTypedArray()
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
index d0da8608..6c7fac6 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
@@ -75,7 +75,7 @@
                 true
             }.`when`(mockService).requestPreviewComplicationData(
                 eq(component),
-                eq(type.asWireComplicationType()),
+                eq(type.toWireComplicationType()),
                 any()
             )
 
@@ -104,7 +104,7 @@
                 true
             }.`when`(mockService).requestPreviewComplicationData(
                 eq(component),
-                eq(type.asWireComplicationType()),
+                eq(type.toWireComplicationType()),
                 any()
             )
 
@@ -137,7 +137,7 @@
                 false
             }.`when`(mockService).requestPreviewComplicationData(
                 eq(component),
-                eq(type.asWireComplicationType()),
+                eq(type.toWireComplicationType()),
                 any()
             )
 
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
index 9d93102..7495485 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
@@ -177,7 +177,7 @@
     private fun testRoundTripConversions(data: ComplicationData) {
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                data.asWireComplicationData().asApiComplicationData().asWireComplicationData()
+                data.asWireComplicationData().toApiComplicationData().asWireComplicationData()
             )
     }
 }
@@ -294,7 +294,7 @@
     }
 
     private fun assertRoundtrip(wireData: WireComplicationData, type: ComplicationType) {
-        val data = wireData.asApiComplicationData()
+        val data = wireData.toApiComplicationData()
         assertThat(data.type).isEqualTo(type)
         ParcelableSubject.assertThat(data.asWireComplicationData()).hasSameSerializationAs(wireData)
     }
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
index aa1c765..7d3843c 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
@@ -38,9 +38,9 @@
     @Test
     public fun plainText() {
         val text = PlainComplicationText.Builder("abc").build()
-        ParcelableSubject.assertThat(text.asWireComplicationText())
+        ParcelableSubject.assertThat(text.toWireComplicationText())
             .hasSameSerializationAs(WireComplicationText.plainText("abc"))
-        ParcelableSubject.assertThat(text.asWireComplicationText())
+        ParcelableSubject.assertThat(text.toWireComplicationText())
             .hasDifferentSerializationAs(WireComplicationText.plainText("abc1"))
     }
 
@@ -56,7 +56,7 @@
             .setMinimumUnit(TimeUnit.SECONDS)
             .build()
 
-        ParcelableSubject.assertThat(text.asWireComplicationText())
+        ParcelableSubject.assertThat(text.toWireComplicationText())
             .hasSameSerializationAs(
                 WireTimeDifferenceBuilder()
                     .setStyle(WireComplicationText.DIFFERENCE_STYLE_STOPWATCH)
@@ -85,7 +85,7 @@
             .setMinimumUnit(TimeUnit.SECONDS)
             .build()
 
-        ParcelableSubject.assertThat(text.asWireComplicationText())
+        ParcelableSubject.assertThat(text.toWireComplicationText())
             .hasSameSerializationAs(
                 WireTimeDifferenceBuilder()
                     .setStyle(WireComplicationText.DIFFERENCE_STYLE_STOPWATCH)
@@ -110,7 +110,7 @@
             .setTimeZone(TimeZone.getTimeZone("Europe/London"))
             .build()
 
-        ParcelableSubject.assertThat(text.asWireComplicationText())
+        ParcelableSubject.assertThat(text.toWireComplicationText())
             .hasSameSerializationAs(
                 WireTimeFormatBuilder()
                     .setFormat("h:m")
@@ -129,7 +129,7 @@
     @Test
     public fun plainText() {
         val wireText = WireComplicationText.plainText("abc")
-        val text = wireText.asApiComplicationText()
+        val text = wireText.toApiComplicationText()
 
         assertThat(text.getTextAt(getResource(), 0)).isEqualTo("abc")
         assertThat(text.getNextChangeTime(0)).isEqualTo(Long.MAX_VALUE)
@@ -148,7 +148,7 @@
             .setReferencePeriodEndMillis(startPointMillis)
             .build()
 
-        val text = wireText.asApiComplicationText()
+        val text = wireText.toApiComplicationText()
 
         val twoMinutesThreeSecondAfter = startPointMillis + 2.minutes + 3.seconds
         assertThat(
@@ -173,7 +173,7 @@
             .setTimeZone(java.util.TimeZone.getTimeZone("Europe/London"))
             .build()
 
-        val text = wireText.asApiComplicationText()
+        val text = wireText.toApiComplicationText()
 
         assertThat(text.getTextAt(getResource(), dateTimeMillis).toString())
             .isEqualTo("10:15 in London")
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TypeTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TypeTest.kt
index 76e215b..75c5f30 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TypeTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TypeTest.kt
@@ -47,7 +47,7 @@
     }
 
     private fun assertThatIsWireType(type: ComplicationType, wireType: Int) {
-        assertThat(type.asWireComplicationType()).isEqualTo(wireType)
+        assertThat(type.toWireComplicationType()).isEqualTo(wireType)
     }
 
     @Test
diff --git a/wear/wear-complications-provider/build.gradle b/wear/wear-complications-provider/build.gradle
index b36a93a..60d0420 100644
--- a/wear/wear-complications-provider/build.gradle
+++ b/wear/wear-complications-provider/build.gradle
@@ -57,7 +57,7 @@
     name = "Android Wear Complications"
     publish = Publish.SNAPSHOT_AND_RELEASE
     mavenGroup = LibraryGroups.WEAR
-    mavenVersion = LibraryVersions.WEAR_COMPLICATIONS
+    mavenVersion = LibraryVersions.WEAR_COMPLICATIONS_PROVIDER
     inceptionYear = "2020"
     description = "Android Wear Complications"
 }
diff --git a/wear/wear-complications-provider/src/test/java/androidx/wear/complications/ComplicationProviderServiceTest.java b/wear/wear-complications-provider/src/test/java/androidx/wear/complications/ComplicationProviderServiceTest.java
index c7a459f..3ddb44d 100644
--- a/wear/wear-complications-provider/src/test/java/androidx/wear/complications/ComplicationProviderServiceTest.java
+++ b/wear/wear-complications-provider/src/test/java/androidx/wear/complications/ComplicationProviderServiceTest.java
@@ -135,7 +135,7 @@
     public void testOnComplicationUpdate() throws Exception {
         int id = 123;
         mComplicationProvider.onUpdate(
-                id, ComplicationType.LONG_TEXT.asWireComplicationType(), mLocalManager);
+                id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
         ShadowLooper.runUiThreadTasks();
 
         ArgumentCaptor<android.support.wearable.complications.ComplicationData> data =
@@ -151,7 +151,7 @@
     public void testOnComplicationUpdateNoUpdateRequired() throws Exception {
         int id = 123;
         mNoUpdateComplicationProvider.onUpdate(
-                id, ComplicationType.LONG_TEXT.asWireComplicationType(), mLocalManager);
+                id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
         ShadowLooper.runUiThreadTasks();
 
         ArgumentCaptor<android.support.wearable.complications.ComplicationData> data =
@@ -164,7 +164,7 @@
     @Test
     public void testGetComplicationPreviewData() throws Exception {
         assertThat(mComplicationProvider.getComplicationPreviewData(
-                ComplicationType.LONG_TEXT.asWireComplicationType()
+                ComplicationType.LONG_TEXT.toWireComplicationType()
         ).getLongText().getTextAt(null, 0)).isEqualTo("hello preview");
     }
 
@@ -172,7 +172,7 @@
     public void testGetComplicationPreviewDataReturnsNull() throws Exception {
         // The ComplicationProvider doesn't support PHOTO_IMAGE so null should be returned.
         assertNull(mComplicationProvider.getComplicationPreviewData(
-                ComplicationType.PHOTO_IMAGE.asWireComplicationType())
+                ComplicationType.PHOTO_IMAGE.toWireComplicationType())
         );
     }
 }
diff --git a/wear/wear-input-testing/build.gradle b/wear/wear-input-testing/build.gradle
index 28d856d..bcd9ccc 100644
--- a/wear/wear-input-testing/build.gradle
+++ b/wear/wear-input-testing/build.gradle
@@ -38,7 +38,7 @@
     name = "Android Wear Support Input Testing Helpers"
     publish = Publish.SNAPSHOT_AND_RELEASE
     mavenGroup = LibraryGroups.WEAR
-    mavenVersion = LibraryVersions.WEAR_INPUT
+    mavenVersion = LibraryVersions.WEAR_INPUT_TESTING
     inceptionYear = "2020"
     description = "Android Wear Support Input  Testing Helpers"
 }
diff --git a/wear/wear-phone-interactions/api/current.txt b/wear/wear-phone-interactions/api/current.txt
index 0863cad..310f3a1 100644
--- a/wear/wear-phone-interactions/api/current.txt
+++ b/wear/wear-phone-interactions/api/current.txt
@@ -66,9 +66,9 @@
     method protected void finalize();
     method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
     field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
-    field public static final int ERROR_PHONE_UNAVAILABLE = 2; // 0x2
-    field public static final int ERROR_UNSUPPORTED = 1; // 0x1
-    field public static final int NO_ERROR = 0; // 0x0
+    field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+    field public static final int NO_ERROR = -1; // 0xffffffff
   }
 
   public abstract static class RemoteAuthClient.Callback {
diff --git a/wear/wear-phone-interactions/api/public_plus_experimental_current.txt b/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
index 0863cad..310f3a1 100644
--- a/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
+++ b/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
@@ -66,9 +66,9 @@
     method protected void finalize();
     method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
     field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
-    field public static final int ERROR_PHONE_UNAVAILABLE = 2; // 0x2
-    field public static final int ERROR_UNSUPPORTED = 1; // 0x1
-    field public static final int NO_ERROR = 0; // 0x0
+    field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+    field public static final int NO_ERROR = -1; // 0xffffffff
   }
 
   public abstract static class RemoteAuthClient.Callback {
diff --git a/wear/wear-phone-interactions/api/restricted_current.txt b/wear/wear-phone-interactions/api/restricted_current.txt
index 0863cad..310f3a1 100644
--- a/wear/wear-phone-interactions/api/restricted_current.txt
+++ b/wear/wear-phone-interactions/api/restricted_current.txt
@@ -66,9 +66,9 @@
     method protected void finalize();
     method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
     field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
-    field public static final int ERROR_PHONE_UNAVAILABLE = 2; // 0x2
-    field public static final int ERROR_UNSUPPORTED = 1; // 0x1
-    field public static final int NO_ERROR = 0; // 0x0
+    field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+    field public static final int NO_ERROR = -1; // 0xffffffff
   }
 
   public abstract static class RemoteAuthClient.Callback {
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
index e1c5d72..c547fc7 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
@@ -134,13 +134,13 @@
             "android.support.wearable.authentication.action.OAUTH"
 
         /** Indicates 3p authentication is finished without error  */
-        public const val NO_ERROR: Int = 0
+        public const val NO_ERROR: Int = -1
 
         /** Indicates 3p authentication isn't supported by Wear OS  */
-        public const val ERROR_UNSUPPORTED: Int = 1
+        public const val ERROR_UNSUPPORTED: Int = 0
 
         /** Indicates no phone is connected, or the phone connected doesn't support 3p auth */
-        public const val ERROR_PHONE_UNAVAILABLE: Int = 2
+        public const val ERROR_PHONE_UNAVAILABLE: Int = 1
 
         /** Errors returned in [.Callback.onAuthorizationError].  */
         @Retention(AnnotationRetention.SOURCE)
@@ -339,7 +339,7 @@
         }
 
         @SuppressLint("SyntheticAccessor")
-        internal fun onResult(response: OAuthResponse) {
+        private fun onResult(response: OAuthResponse) {
             @ErrorCode val error = response.getErrorCode()
             uiThreadExecutor.execute(
                 Runnable {
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
index 747bb43..dadd75b 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
@@ -27,6 +27,9 @@
 import android.os.RemoteException
 import android.support.wearable.authentication.IAuthenticationRequestCallback
 import android.support.wearable.authentication.IAuthenticationRequestService
+import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_ERROR_CODE
+import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_PACKAGE_NAME
+import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_RESPONSE_URL
 import java.security.SecureRandom
 
 /**
@@ -79,7 +82,7 @@
     public companion object {
         @JvmStatic
         private val callbacksByPackageNameAndRequestID:
-            MutableMap<Pair<String, Int>, RemoteAuthClient.RequestCallback> = HashMap()
+            MutableMap<Pair<String, Int>, IAuthenticationRequestCallback> = HashMap()
 
         /**
          * To be called by the child class to invoke the callback with Response
@@ -91,7 +94,9 @@
             packageNameAndRequestId: Pair<String, Int>
         ) {
             try {
-                callbacksByPackageNameAndRequestID[packageNameAndRequestId]?.onResult(response)
+                callbacksByPackageNameAndRequestID[packageNameAndRequestId]?.onResult(
+                    buildBundleFromResponse(response, packageNameAndRequestId.first)
+                )
                 callbacksByPackageNameAndRequestID.remove(packageNameAndRequestId)
             } catch (e: RemoteException) {
                 throw e.cause!!
@@ -99,8 +104,15 @@
         }
 
         internal fun getCallback(packageNameAndRequestId: Pair<String, Int>):
-            RemoteAuthClient.RequestCallback? =
+            IAuthenticationRequestCallback? =
                 callbacksByPackageNameAndRequestID[packageNameAndRequestId]
+
+        internal fun buildBundleFromResponse(response: OAuthResponse, packageName: String): Bundle =
+            Bundle().apply {
+                putParcelable(KEY_RESPONSE_URL, response.getResponseUrl())
+                putInt(KEY_ERROR_CODE, response.getErrorCode())
+                putString(KEY_PACKAGE_NAME, packageName)
+            }
     }
 
     private val secureRandom: SecureRandom = SecureRandom()
@@ -151,15 +163,15 @@
             request: Bundle,
             authenticationRequestCallback: IAuthenticationRequestCallback
         ) {
+            val packageName = request.getString(RemoteAuthClient.KEY_PACKAGE_NAME)
             if (remoteAuthRequestHandler.isAuthSupported()) {
-                val packageName = request.getString(RemoteAuthClient.KEY_PACKAGE_NAME)
                 if (!verifyPackageName(context, packageName)) {
                     throw SecurityException("Failed to verify the Requester's package name")
                 }
 
                 val packageNameAndRequestId = Pair(packageName!!, secureRandom.nextInt())
                 callbacksByPackageNameAndRequestID[packageNameAndRequestId] =
-                    authenticationRequestCallback as RemoteAuthClient.RequestCallback
+                    authenticationRequestCallback
 
                 val requestUrl: Uri? = request.getParcelable(RemoteAuthClient.KEY_REQUEST_URL)
                 remoteAuthRequestHandler.sendAuthRequest(
@@ -167,9 +179,8 @@
                     packageNameAndRequestId
                 )
             } else {
-                (authenticationRequestCallback as RemoteAuthClient.RequestCallback).onResult(
-                    OAuthResponse.Builder()
-                        .setErrorCode(RemoteAuthClient.ERROR_UNSUPPORTED).build()
+                authenticationRequestCallback.onResult(
+                    Bundle().apply { putInt(KEY_ERROR_CODE, RemoteAuthClient.ERROR_UNSUPPORTED) }
                 )
             }
         }
diff --git a/wear/wear-remote-interactions/api/current.txt b/wear/wear-remote-interactions/api/current.txt
index 0a4a745..367672c 100644
--- a/wear/wear-remote-interactions/api/current.txt
+++ b/wear/wear-remote-interactions/api/current.txt
@@ -2,13 +2,17 @@
 package androidx.wear.remote.interactions {
 
   @RequiresApi(android.os.Build.VERSION_CODES.N) public final class PlayStoreAvailability {
-    method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
-    field public static final androidx.wear.remote.interactions.PlayStoreAvailability INSTANCE;
+    method public static int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+    field public static final androidx.wear.remote.interactions.PlayStoreAvailability.Companion Companion;
     field public static final int PLAY_STORE_AVAILABLE = 1; // 0x1
     field public static final int PLAY_STORE_ERROR_UNKNOWN = 0; // 0x0
     field public static final int PLAY_STORE_UNAVAILABLE = 2; // 0x2
   }
 
+  public static final class PlayStoreAvailability.Companion {
+    method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+  }
+
   public final class WatchFaceConfigIntentHelper {
     method public static String? getPeerIdExtra(android.content.Intent watchFaceIntent);
     method public static android.content.ComponentName? getWatchFaceComponentExtra(android.content.Intent watchFaceIntent);
diff --git a/wear/wear-remote-interactions/api/public_plus_experimental_current.txt b/wear/wear-remote-interactions/api/public_plus_experimental_current.txt
index 0a4a745..367672c 100644
--- a/wear/wear-remote-interactions/api/public_plus_experimental_current.txt
+++ b/wear/wear-remote-interactions/api/public_plus_experimental_current.txt
@@ -2,13 +2,17 @@
 package androidx.wear.remote.interactions {
 
   @RequiresApi(android.os.Build.VERSION_CODES.N) public final class PlayStoreAvailability {
-    method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
-    field public static final androidx.wear.remote.interactions.PlayStoreAvailability INSTANCE;
+    method public static int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+    field public static final androidx.wear.remote.interactions.PlayStoreAvailability.Companion Companion;
     field public static final int PLAY_STORE_AVAILABLE = 1; // 0x1
     field public static final int PLAY_STORE_ERROR_UNKNOWN = 0; // 0x0
     field public static final int PLAY_STORE_UNAVAILABLE = 2; // 0x2
   }
 
+  public static final class PlayStoreAvailability.Companion {
+    method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+  }
+
   public final class WatchFaceConfigIntentHelper {
     method public static String? getPeerIdExtra(android.content.Intent watchFaceIntent);
     method public static android.content.ComponentName? getWatchFaceComponentExtra(android.content.Intent watchFaceIntent);
diff --git a/wear/wear-remote-interactions/api/restricted_current.txt b/wear/wear-remote-interactions/api/restricted_current.txt
index 0a4a745..367672c 100644
--- a/wear/wear-remote-interactions/api/restricted_current.txt
+++ b/wear/wear-remote-interactions/api/restricted_current.txt
@@ -2,13 +2,17 @@
 package androidx.wear.remote.interactions {
 
   @RequiresApi(android.os.Build.VERSION_CODES.N) public final class PlayStoreAvailability {
-    method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
-    field public static final androidx.wear.remote.interactions.PlayStoreAvailability INSTANCE;
+    method public static int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+    field public static final androidx.wear.remote.interactions.PlayStoreAvailability.Companion Companion;
     field public static final int PLAY_STORE_AVAILABLE = 1; // 0x1
     field public static final int PLAY_STORE_ERROR_UNKNOWN = 0; // 0x0
     field public static final int PLAY_STORE_UNAVAILABLE = 2; // 0x2
   }
 
+  public static final class PlayStoreAvailability.Companion {
+    method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+  }
+
   public final class WatchFaceConfigIntentHelper {
     method public static String? getPeerIdExtra(android.content.Intent watchFaceIntent);
     method public static android.content.ComponentName? getWatchFaceComponentExtra(android.content.Intent watchFaceIntent);
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/PlayStoreAvailability.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/PlayStoreAvailability.kt
index 02f04f9..70765b7 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/PlayStoreAvailability.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/PlayStoreAvailability.kt
@@ -28,83 +28,87 @@
  * Helper class for checking whether the phone paired to a given Wear OS device has the Play Store.
  */
 @RequiresApi(Build.VERSION_CODES.N)
-public object PlayStoreAvailability {
-    /**
-     * This value means that there was an error in checking for whether the Play Store is available
-     * on the phone.
-     */
-    public const val PLAY_STORE_ERROR_UNKNOWN: Int = 0
+public class PlayStoreAvailability private constructor() {
+    public companion object {
+        /**
+         * This value means that there was an error in checking for whether the Play Store is
+         * available son the phone.
+         */
+        public const val PLAY_STORE_ERROR_UNKNOWN: Int = 0
 
-    /** This value means that the Play Store is available on the phone.  */
-    public const val PLAY_STORE_AVAILABLE: Int = 1
+        /** This value means that the Play Store is available on the phone.  */
+        public const val PLAY_STORE_AVAILABLE: Int = 1
 
-    /** This value means that the Play Store is not available on the phone.  */
-    public const val PLAY_STORE_UNAVAILABLE: Int = 2
+        /** This value means that the Play Store is not available on the phone.  */
+        public const val PLAY_STORE_UNAVAILABLE: Int = 2
 
-    private const val PLAY_STORE_AVAILABILITY_PATH = "play_store_availability"
-    internal const val SETTINGS_AUTHORITY_URI = "com.google.android.wearable.settings"
-    internal val PLAY_STORE_AVAILABILITY_URI = Uri.Builder()
-        .scheme("content")
-        .authority(SETTINGS_AUTHORITY_URI)
-        .path(PLAY_STORE_AVAILABILITY_PATH)
-        .build()
+        private const val PLAY_STORE_AVAILABILITY_PATH = "play_store_availability"
+        internal const val SETTINGS_AUTHORITY_URI = "com.google.android.wearable.settings"
+        internal val PLAY_STORE_AVAILABILITY_URI = Uri.Builder()
+            .scheme("content")
+            .authority(SETTINGS_AUTHORITY_URI)
+            .path(PLAY_STORE_AVAILABILITY_PATH)
+            .build()
 
-    // The name of the row which stores the play store availability setting in versions before R.
-    internal const val KEY_PLAY_STORE_AVAILABILITY = "play_store_availability"
+        // The name of the row which stores the play store availability setting in versions before
+        // R.
+        internal const val KEY_PLAY_STORE_AVAILABILITY = "play_store_availability"
 
-    // The name of the settings value which stores the play store availability setting in versions
-    // from R.
-    private const val SETTINGS_PLAY_STORE_AVAILABILITY = "phone_play_store_availability"
+        // The name of the settings value which stores the play store availability setting in
+        // versions from R.
+        private const val SETTINGS_PLAY_STORE_AVAILABILITY = "phone_play_store_availability"
 
-    internal const val SYSTEM_FEATURE_WATCH: String = "android.hardware.type.watch"
+        internal const val SYSTEM_FEATURE_WATCH: String = "android.hardware.type.watch"
 
-    /**
-     * Returns whether the Play Store is available on the Phone. If
-     * [PLAY_STORE_ERROR_UNKNOWN] is returned, the caller should try again later. This
-     * method should not be run on the main thread.
-     *
-     * @return One of three values: [PLAY_STORE_AVAILABLE],
-     * [PLAY_STORE_UNAVAILABLE], or [PLAY_STORE_ERROR_UNKNOWN].
-     */
-    @PlayStoreStatus
-    public fun getPlayStoreAvailabilityOnPhone(context: Context): Int {
-        val isCurrentDeviceAWatch = context.packageManager.hasSystemFeature(
-            SYSTEM_FEATURE_WATCH
-        )
+        /**
+         * Returns whether the Play Store is available on the Phone. If
+         * [PLAY_STORE_ERROR_UNKNOWN] is returned, the caller should try again later. This
+         * method should not be run on the main thread.
+         *
+         * @return One of three values: [PLAY_STORE_AVAILABLE],
+         * [PLAY_STORE_UNAVAILABLE], or [PLAY_STORE_ERROR_UNKNOWN].
+         */
+        @JvmStatic
+        @PlayStoreStatus
+        public fun getPlayStoreAvailabilityOnPhone(context: Context): Int {
+            val isCurrentDeviceAWatch = context.packageManager.hasSystemFeature(
+                SYSTEM_FEATURE_WATCH
+            )
 
-        if (!isCurrentDeviceAWatch) {
-            val isPlayServiceAvailable =
-                GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
-            return if (isPlayServiceAvailable == ConnectionResult.SUCCESS) PLAY_STORE_AVAILABLE
-            else PLAY_STORE_UNAVAILABLE
-        }
+            if (!isCurrentDeviceAWatch) {
+                val isPlayServiceAvailable =
+                    GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
+                return if (isPlayServiceAvailable == ConnectionResult.SUCCESS) PLAY_STORE_AVAILABLE
+                else PLAY_STORE_UNAVAILABLE
+            }
 
-        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
-            context.contentResolver.query(
-                PLAY_STORE_AVAILABILITY_URI, null, null, null,
-                null
-            )?.use { cursor ->
-                while (cursor.moveToNext()) {
-                    if (KEY_PLAY_STORE_AVAILABILITY == cursor.getString(0)) {
-                        return cursor.getInt(1)
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+                context.contentResolver.query(
+                    PLAY_STORE_AVAILABILITY_URI, null, null, null,
+                    null
+                )?.use { cursor ->
+                    while (cursor.moveToNext()) {
+                        if (KEY_PLAY_STORE_AVAILABILITY == cursor.getString(0)) {
+                            return cursor.getInt(1)
+                        }
                     }
                 }
+            } else {
+                return Settings.Global.getInt(
+                    context.contentResolver, SETTINGS_PLAY_STORE_AVAILABILITY,
+                    PLAY_STORE_ERROR_UNKNOWN
+                )
             }
-        } else {
-            return Settings.Global.getInt(
-                context.contentResolver, SETTINGS_PLAY_STORE_AVAILABILITY,
-                PLAY_STORE_ERROR_UNKNOWN
-            )
+            return PLAY_STORE_ERROR_UNKNOWN
         }
-        return PLAY_STORE_ERROR_UNKNOWN
-    }
 
-    /** @hide */
-    @IntDef(
-        PLAY_STORE_ERROR_UNKNOWN,
-        PLAY_STORE_AVAILABLE,
-        PLAY_STORE_UNAVAILABLE
-    )
-    @Retention(AnnotationRetention.SOURCE)
-    public annotation class PlayStoreStatus
+        /** @hide */
+        @IntDef(
+            PLAY_STORE_ERROR_UNKNOWN,
+            PLAY_STORE_AVAILABLE,
+            PLAY_STORE_UNAVAILABLE
+        )
+        @Retention(AnnotationRetention.SOURCE)
+        public annotation class PlayStoreStatus
+    }
 }
\ No newline at end of file
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
index 0e319da..2d669ae 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
@@ -32,32 +32,32 @@
  * activity. The following meta-data will register the `com.example.watchface.CONFIG_DIGITAL`
  * action to be started when configuring a watch face on the wearable device:
  * ```
- * &lt;meta-data
+ * <meta-data
  * android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
- * android:value="com.example.watchface.CONFIG_DIGITAL" /&gt;
+ * android:value="com.example.watchface.CONFIG_DIGITAL" />
  * ```
  *
  *
  * To register a configuration activity to be started on a companion phone, add the following
  * alternative meta-data entry to the watch face component:
  * ```
- * &lt;meta-data
+ * <meta-data
  * android:name="com.google.android.wearable.watchface.companionConfigurationAction"
- * android:value="com.example.watchface.CONFIG_DIGITAL" /&gt;
+ * android:value="com.example.watchface.CONFIG_DIGITAL" />
  * ```
  *
  *
  * The activity should have an intent filter which lists the action specified in the meta-data
  * block above, in addition to the two categories present in the following example:
  * ```
- * &lt;activity android:name=".MyWatchFaceConfigActivity"&gt;
- * &lt;intent-filter&gt;
- * &lt;action android:name="com.example.watchface.CONFIG_DIGITAL" /&gt;
- * &lt;category android:name=
- * "com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" /&gt;
- * &lt;category android:name="android.intent.category.DEFAULT" /&gt;
- * &lt;/intent-filter&gt;
- * &lt;/activity&gt;
+ * <activity android:name=".MyWatchFaceConfigActivity">
+ * <intent-filter>
+ * <action android:name="com.example.watchface.CONFIG_DIGITAL" />
+ * <category android:name=
+ * "com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+ * <category android:name="android.intent.category.DEFAULT" />
+ * </intent-filter>
+ * </activity>
  * ```
  *
  *
diff --git a/wear/wear-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index 88201d9..8137255 100644
--- a/wear/wear-watchface-client/api/current.txt
+++ b/wear/wear-watchface-client/api/current.txt
@@ -37,77 +37,97 @@
     property public final boolean hasLowBitAmbient;
   }
 
-  public interface EditorObserverListener {
-    method public void onEditorStateChange(androidx.wear.watchface.client.EditorState editorState);
+  public interface EditorListener {
+    method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
   }
 
   public interface EditorServiceClient {
+    method public void addListener(androidx.wear.watchface.client.EditorListener editorListener, java.util.concurrent.Executor listenerExecutor);
     method public void closeEditor();
-    method public void registerObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
-    method public void registerObserver(optional java.util.concurrent.Executor? observerCallbackExecutor, androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
-    method public void unregisterObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
+    method public void removeListener(androidx.wear.watchface.client.EditorListener editorListener);
   }
 
   public final class EditorState {
-    method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationData();
-    method public java.util.Map<java.lang.String,java.lang.String> getUserStyle();
-    method public String getWatchFaceInstanceId();
-    method public boolean hasCommitChanges();
-    property public final boolean commitChanges;
-    property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationData;
-    property public final java.util.Map<java.lang.String,java.lang.String> userStyle;
-    property public final String watchFaceInstanceId;
+    method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationsData();
+    method public androidx.wear.watchface.style.UserStyleData getUserStyle();
+    method public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
+    method public boolean shouldCommitChanges();
+    property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationsData;
+    property public final boolean shouldCommitChanges;
+    property public final androidx.wear.watchface.style.UserStyleData userStyle;
+    property public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
   }
 
   public final class EditorStateKt {
   }
 
   public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
+    method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+    method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
+    method @AnyThread public boolean isConnectionAlive();
+    method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
+    method @RequiresApi(27) public android.graphics.Bitmap? renderComplicationToBitmap(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public android.os.Bundle toBundle();
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+    field public static final String BINDER_KEY = "HeadlessWatchFaceClient";
     field public static final androidx.wear.watchface.client.HeadlessWatchFaceClient.Companion Companion;
   }
 
-  public static final class HeadlessWatchFaceClient.Companion {
-    method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
+  public static interface HeadlessWatchFaceClient.ClientDisconnectListener {
+    method public void onClientDisconnected();
   }
 
-  public interface InteractiveWatchFaceSysUiClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public default static androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
-    method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> getContentDescriptionLabels();
+  public static final class HeadlessWatchFaceClient.Companion {
+    method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+  }
+
+  public interface InteractiveWatchFaceClient extends java.lang.AutoCloseable {
+    method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+    method public void displayPressedAnimation(int complicationId);
+    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
+    method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> getContentDescriptionLabels();
     method public String getInstanceId();
     method public long getPreviewReferenceTimeMillis();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @AnyThread public boolean isConnectionAlive();
     method public void performAmbientTick();
-    method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
-    method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
-    property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
+    method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
+    method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void sendTouchEvent(@Px int xPosition, @Px int yPosition, @androidx.wear.watchface.TapType int tapType);
+    method public void setWatchUiState(androidx.wear.watchface.client.WatchUiState watchUiState);
+    method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
+    method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
+    method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyleData userStyle);
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
+    property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
-    field public static final androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.Companion Companion;
-    field public static final int TAP_TYPE_TAP = 2; // 0x2
-    field public static final int TAP_TYPE_TOUCH = 0; // 0x0
-    field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+    property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+    field public static final androidx.wear.watchface.client.InteractiveWatchFaceClient.Companion Companion;
+    field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+    field public static final int TAP_TYPE_DOWN = 0; // 0x0
+    field public static final int TAP_TYPE_UP = 2; // 0x2
   }
 
-  public static final class InteractiveWatchFaceSysUiClient.Companion {
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
-    field public static final int TAP_TYPE_TAP = 2; // 0x2
-    field public static final int TAP_TYPE_TOUCH = 0; // 0x0
-    field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+  public static interface InteractiveWatchFaceClient.ClientDisconnectListener {
+    method public void onClientDisconnected();
   }
 
-  public static final class InteractiveWatchFaceSysUiClient.ContentDescriptionLabel {
-    ctor public InteractiveWatchFaceSysUiClient.ContentDescriptionLabel(android.support.wearable.complications.TimeDependentText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
+  public static final class InteractiveWatchFaceClient.Companion {
+    field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+    field public static final int TAP_TYPE_DOWN = 0; // 0x0
+    field public static final int TAP_TYPE_UP = 2; // 0x2
+  }
+
+  public static final class InteractiveWatchFaceClient.ContentDescriptionLabel {
+    ctor public InteractiveWatchFaceClient.ContentDescriptionLabel(androidx.wear.complications.data.ComplicationText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
     method public android.graphics.Rect getBounds();
     method public android.app.PendingIntent? getTapAction();
     method public CharSequence getTextAt(android.content.res.Resources resources, long dateTimeMillis);
@@ -115,44 +135,12 @@
     property public final android.app.PendingIntent? tapAction;
   }
 
-  public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public void bringAttentionToComplication(int complicationId);
-    method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
-    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public String getInstanceId();
-    method public long getPreviewReferenceTimeMillis();
-    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
-    method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
-    method public void updateInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
-    method public void updateInstance(String newInstanceId, java.util.Map<java.lang.String,java.lang.String> userStyle);
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public abstract String instanceId;
-    property public abstract long previewReferenceTimeMillis;
-    property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
-    field public static final androidx.wear.watchface.client.InteractiveWatchFaceWcsClient.Companion Companion;
-  }
-
-  public static final class InteractiveWatchFaceWcsClient.Companion {
-    method public androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
-  }
-
-  public final class SystemState {
-    ctor public SystemState(boolean inAmbientMode, int interruptionFilter);
-    method public int getInterruptionFilter();
-    method public boolean inAmbientMode();
-    property public final boolean inAmbientMode;
-    property public final int interruptionFilter;
-  }
-
   public interface WatchFaceControlClient extends java.lang.AutoCloseable {
-    method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
+    method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, @Px int surfaceWidth, @Px int surfaceHeight);
     method public default static suspend Object? createWatchFaceControlClient(android.content.Context p, String context, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.WatchFaceControlClient> watchFacePackageName);
     method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
-    method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+    method public suspend Object? getOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> p);
     field public static final androidx.wear.watchface.client.WatchFaceControlClient.Companion Companion;
   }
 
@@ -168,5 +156,19 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException();
   }
 
+  public final class WatchFaceId {
+    ctor public WatchFaceId(String id);
+    method public String getId();
+    property public final String id;
+  }
+
+  public final class WatchUiState {
+    ctor public WatchUiState(boolean inAmbientMode, int interruptionFilter);
+    method public int getInterruptionFilter();
+    method public boolean inAmbientMode();
+    property public final boolean inAmbientMode;
+    property public final int interruptionFilter;
+  }
+
 }
 
diff --git a/wear/wear-watchface-client/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
index 07c20da..d04191e 100644
--- a/wear/wear-watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
@@ -37,77 +37,97 @@
     property public final boolean hasLowBitAmbient;
   }
 
-  public interface EditorObserverListener {
-    method public void onEditorStateChange(androidx.wear.watchface.client.EditorState editorState);
+  public interface EditorListener {
+    method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
   }
 
   public interface EditorServiceClient {
+    method public void addListener(androidx.wear.watchface.client.EditorListener editorListener, java.util.concurrent.Executor listenerExecutor);
     method public void closeEditor();
-    method public void registerObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
-    method public void registerObserver(optional java.util.concurrent.Executor? observerCallbackExecutor, androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
-    method public void unregisterObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
+    method public void removeListener(androidx.wear.watchface.client.EditorListener editorListener);
   }
 
   public final class EditorState {
-    method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationData();
-    method public java.util.Map<java.lang.String,java.lang.String> getUserStyle();
-    method public String getWatchFaceInstanceId();
-    method public boolean hasCommitChanges();
-    property public final boolean commitChanges;
-    property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationData;
-    property public final java.util.Map<java.lang.String,java.lang.String> userStyle;
-    property public final String watchFaceInstanceId;
+    method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationsData();
+    method public androidx.wear.watchface.style.UserStyleData getUserStyle();
+    method public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
+    method public boolean shouldCommitChanges();
+    property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationsData;
+    property public final boolean shouldCommitChanges;
+    property public final androidx.wear.watchface.style.UserStyleData userStyle;
+    property public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
   }
 
   public final class EditorStateKt {
   }
 
   public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
+    method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+    method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
+    method @AnyThread public boolean isConnectionAlive();
+    method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
+    method @RequiresApi(27) public android.graphics.Bitmap? renderComplicationToBitmap(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public android.os.Bundle toBundle();
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+    field public static final String BINDER_KEY = "HeadlessWatchFaceClient";
     field public static final androidx.wear.watchface.client.HeadlessWatchFaceClient.Companion Companion;
   }
 
-  public static final class HeadlessWatchFaceClient.Companion {
-    method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
+  public static interface HeadlessWatchFaceClient.ClientDisconnectListener {
+    method public void onClientDisconnected();
   }
 
-  public interface InteractiveWatchFaceSysUiClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public default static androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
-    method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> getContentDescriptionLabels();
+  public static final class HeadlessWatchFaceClient.Companion {
+    method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+  }
+
+  public interface InteractiveWatchFaceClient extends java.lang.AutoCloseable {
+    method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+    method public void displayPressedAnimation(int complicationId);
+    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
+    method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> getContentDescriptionLabels();
     method public String getInstanceId();
     method public long getPreviewReferenceTimeMillis();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @AnyThread public boolean isConnectionAlive();
     method public void performAmbientTick();
-    method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
-    method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
-    property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
+    method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
+    method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void sendTouchEvent(@Px int xPosition, @Px int yPosition, @androidx.wear.watchface.TapType int tapType);
+    method public void setWatchUiState(androidx.wear.watchface.client.WatchUiState watchUiState);
+    method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
+    method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
+    method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyleData userStyle);
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
+    property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
-    field public static final androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.Companion Companion;
-    field public static final int TAP_TYPE_TAP = 2; // 0x2
-    field public static final int TAP_TYPE_TOUCH = 0; // 0x0
-    field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+    property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+    field public static final androidx.wear.watchface.client.InteractiveWatchFaceClient.Companion Companion;
+    field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+    field public static final int TAP_TYPE_DOWN = 0; // 0x0
+    field public static final int TAP_TYPE_UP = 2; // 0x2
   }
 
-  public static final class InteractiveWatchFaceSysUiClient.Companion {
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
-    field public static final int TAP_TYPE_TAP = 2; // 0x2
-    field public static final int TAP_TYPE_TOUCH = 0; // 0x0
-    field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+  public static interface InteractiveWatchFaceClient.ClientDisconnectListener {
+    method public void onClientDisconnected();
   }
 
-  public static final class InteractiveWatchFaceSysUiClient.ContentDescriptionLabel {
-    ctor public InteractiveWatchFaceSysUiClient.ContentDescriptionLabel(android.support.wearable.complications.TimeDependentText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
+  public static final class InteractiveWatchFaceClient.Companion {
+    field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+    field public static final int TAP_TYPE_DOWN = 0; // 0x0
+    field public static final int TAP_TYPE_UP = 2; // 0x2
+  }
+
+  public static final class InteractiveWatchFaceClient.ContentDescriptionLabel {
+    ctor public InteractiveWatchFaceClient.ContentDescriptionLabel(androidx.wear.complications.data.ComplicationText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
     method public android.graphics.Rect getBounds();
     method public android.app.PendingIntent? getTapAction();
     method public CharSequence getTextAt(android.content.res.Resources resources, long dateTimeMillis);
@@ -115,44 +135,12 @@
     property public final android.app.PendingIntent? tapAction;
   }
 
-  public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public void bringAttentionToComplication(int complicationId);
-    method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
-    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public String getInstanceId();
-    method public long getPreviewReferenceTimeMillis();
-    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
-    method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
-    method public void updateInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
-    method public void updateInstance(String newInstanceId, java.util.Map<java.lang.String,java.lang.String> userStyle);
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public abstract String instanceId;
-    property public abstract long previewReferenceTimeMillis;
-    property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
-    field public static final androidx.wear.watchface.client.InteractiveWatchFaceWcsClient.Companion Companion;
-  }
-
-  public static final class InteractiveWatchFaceWcsClient.Companion {
-    method public androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
-  }
-
-  public final class SystemState {
-    ctor public SystemState(boolean inAmbientMode, int interruptionFilter);
-    method public int getInterruptionFilter();
-    method public boolean inAmbientMode();
-    property public final boolean inAmbientMode;
-    property public final int interruptionFilter;
-  }
-
   public interface WatchFaceControlClient extends java.lang.AutoCloseable {
-    method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
+    method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, @Px int surfaceWidth, @Px int surfaceHeight);
     method public default static suspend Object? createWatchFaceControlClient(android.content.Context p, String context, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.WatchFaceControlClient> watchFacePackageName);
     method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
-    method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+    method public suspend Object? getOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> p);
     field public static final androidx.wear.watchface.client.WatchFaceControlClient.Companion Companion;
   }
 
@@ -168,5 +156,19 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException();
   }
 
+  public final class WatchFaceId {
+    ctor public WatchFaceId(String id);
+    method public String getId();
+    property public final String id;
+  }
+
+  public final class WatchUiState {
+    ctor public WatchUiState(boolean inAmbientMode, int interruptionFilter);
+    method public int getInterruptionFilter();
+    method public boolean inAmbientMode();
+    property public final boolean inAmbientMode;
+    property public final int interruptionFilter;
+  }
+
 }
 
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index 604dab1..bce2031 100644
--- a/wear/wear-watchface-client/api/restricted_current.txt
+++ b/wear/wear-watchface-client/api/restricted_current.txt
@@ -38,26 +38,25 @@
     property public final boolean hasLowBitAmbient;
   }
 
-  public interface EditorObserverListener {
-    method public void onEditorStateChange(androidx.wear.watchface.client.EditorState editorState);
+  public interface EditorListener {
+    method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
   }
 
   public interface EditorServiceClient {
+    method public void addListener(androidx.wear.watchface.client.EditorListener editorListener, java.util.concurrent.Executor listenerExecutor);
     method public void closeEditor();
-    method public void registerObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
-    method public void registerObserver(optional java.util.concurrent.Executor? observerCallbackExecutor, androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
-    method public void unregisterObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
+    method public void removeListener(androidx.wear.watchface.client.EditorListener editorListener);
   }
 
   public final class EditorState {
-    method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationData();
-    method public java.util.Map<java.lang.String,java.lang.String> getUserStyle();
-    method public String getWatchFaceInstanceId();
-    method public boolean hasCommitChanges();
-    property public final boolean commitChanges;
-    property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationData;
-    property public final java.util.Map<java.lang.String,java.lang.String> userStyle;
-    property public final String watchFaceInstanceId;
+    method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationsData();
+    method public androidx.wear.watchface.style.UserStyleData getUserStyle();
+    method public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
+    method public boolean shouldCommitChanges();
+    property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationsData;
+    property public final boolean shouldCommitChanges;
+    property public final androidx.wear.watchface.style.UserStyleData userStyle;
+    property public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
   }
 
   public final class EditorStateKt {
@@ -65,51 +64,72 @@
   }
 
   public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
+    method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+    method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
+    method @AnyThread public boolean isConnectionAlive();
+    method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
+    method @RequiresApi(27) public android.graphics.Bitmap? renderComplicationToBitmap(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public android.os.Bundle toBundle();
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+    field public static final String BINDER_KEY = "HeadlessWatchFaceClient";
     field public static final androidx.wear.watchface.client.HeadlessWatchFaceClient.Companion Companion;
   }
 
-  public static final class HeadlessWatchFaceClient.Companion {
-    method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
+  public static interface HeadlessWatchFaceClient.ClientDisconnectListener {
+    method public void onClientDisconnected();
   }
 
-  public interface InteractiveWatchFaceSysUiClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public default static androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
-    method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> getContentDescriptionLabels();
+  public static final class HeadlessWatchFaceClient.Companion {
+    method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+  }
+
+  public interface InteractiveWatchFaceClient extends java.lang.AutoCloseable {
+    method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+    method public void displayPressedAnimation(int complicationId);
+    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
+    method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> getContentDescriptionLabels();
     method public String getInstanceId();
     method public long getPreviewReferenceTimeMillis();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @AnyThread public boolean isConnectionAlive();
     method public void performAmbientTick();
-    method public void sendTouchEvent(int xPosition, int yPosition, @androidx.wear.watchface.client.TapType int tapType);
-    method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
-    property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
+    method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
+    method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void sendTouchEvent(@Px int xPosition, @Px int yPosition, @androidx.wear.watchface.TapType int tapType);
+    method public void setWatchUiState(androidx.wear.watchface.client.WatchUiState watchUiState);
+    method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
+    method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
+    method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyleData userStyle);
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
+    property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
-    field public static final androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.Companion Companion;
-    field public static final int TAP_TYPE_TAP = 2; // 0x2
-    field public static final int TAP_TYPE_TOUCH = 0; // 0x0
-    field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+    property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+    field public static final androidx.wear.watchface.client.InteractiveWatchFaceClient.Companion Companion;
+    field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+    field public static final int TAP_TYPE_DOWN = 0; // 0x0
+    field public static final int TAP_TYPE_UP = 2; // 0x2
   }
 
-  public static final class InteractiveWatchFaceSysUiClient.Companion {
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
-    field public static final int TAP_TYPE_TAP = 2; // 0x2
-    field public static final int TAP_TYPE_TOUCH = 0; // 0x0
-    field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+  public static interface InteractiveWatchFaceClient.ClientDisconnectListener {
+    method public void onClientDisconnected();
   }
 
-  public static final class InteractiveWatchFaceSysUiClient.ContentDescriptionLabel {
-    ctor public InteractiveWatchFaceSysUiClient.ContentDescriptionLabel(android.support.wearable.complications.TimeDependentText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
+  public static final class InteractiveWatchFaceClient.Companion {
+    field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+    field public static final int TAP_TYPE_DOWN = 0; // 0x0
+    field public static final int TAP_TYPE_UP = 2; // 0x2
+  }
+
+  public static final class InteractiveWatchFaceClient.ContentDescriptionLabel {
+    ctor public InteractiveWatchFaceClient.ContentDescriptionLabel(androidx.wear.complications.data.ComplicationText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
     method public android.graphics.Rect getBounds();
     method public android.app.PendingIntent? getTapAction();
     method public CharSequence getTextAt(android.content.res.Resources resources, long dateTimeMillis);
@@ -117,47 +137,12 @@
     property public final android.app.PendingIntent? tapAction;
   }
 
-  public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
-    method public android.os.IBinder asBinder();
-    method public void bringAttentionToComplication(int complicationId);
-    method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
-    method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public String getInstanceId();
-    method public long getPreviewReferenceTimeMillis();
-    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
-    method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
-    method public void updateInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
-    method public void updateInstance(String newInstanceId, java.util.Map<java.lang.String,java.lang.String> userStyle);
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public abstract String instanceId;
-    property public abstract long previewReferenceTimeMillis;
-    property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
-    field public static final androidx.wear.watchface.client.InteractiveWatchFaceWcsClient.Companion Companion;
-  }
-
-  public static final class InteractiveWatchFaceWcsClient.Companion {
-    method public androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
-  }
-
-  public final class SystemState {
-    ctor public SystemState(boolean inAmbientMode, int interruptionFilter);
-    method public int getInterruptionFilter();
-    method public boolean inAmbientMode();
-    property public final boolean inAmbientMode;
-    property public final int interruptionFilter;
-  }
-
-  @IntDef({androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.TAP_TYPE_TOUCH, androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.TAP_TYPE_TOUCH_CANCEL, androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.TAP_TYPE_TAP}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public @interface TapType {
-  }
-
   public interface WatchFaceControlClient extends java.lang.AutoCloseable {
-    method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
+    method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, @Px int surfaceWidth, @Px int surfaceHeight);
     method public default static suspend Object? createWatchFaceControlClient(android.content.Context p, String context, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.WatchFaceControlClient> watchFacePackageName);
     method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
-    method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+    method public suspend Object? getOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> p);
     field public static final androidx.wear.watchface.client.WatchFaceControlClient.Companion Companion;
   }
 
@@ -173,5 +158,19 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException();
   }
 
+  public final class WatchFaceId {
+    ctor public WatchFaceId(String id);
+    method public String getId();
+    property public final String id;
+  }
+
+  public final class WatchUiState {
+    ctor public WatchUiState(boolean inAmbientMode, int interruptionFilter);
+    method public int getInterruptionFilter();
+    method public boolean inAmbientMode();
+    property public final boolean inAmbientMode;
+    property public final int interruptionFilter;
+  }
+
 }
 
diff --git a/wear/wear-watchface-client/guava/api/current.txt b/wear/wear-watchface-client/guava/api/current.txt
index 2636cf73..bf268ec 100644
--- a/wear/wear-watchface-client/guava/api/current.txt
+++ b/wear/wear-watchface-client/guava/api/current.txt
@@ -7,9 +7,9 @@
     method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
     method public static final com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.ListenableWatchFaceControlClient> createWatchFaceControlClient(android.content.Context context, String watchFacePackageName);
     method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
-    method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+    method public suspend Object? getOrCreateInteractiveWatchFaceClient(String p, androidx.wear.watchface.client.DeviceConfig p1, androidx.wear.watchface.client.WatchUiState p2, androidx.wear.watchface.style.UserStyleData? p3, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? p4, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> $completion);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceClient> listenableGetOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     field public static final androidx.wear.watchface.client.ListenableWatchFaceControlClient.Companion Companion;
   }
 
diff --git a/wear/wear-watchface-client/guava/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/guava/api/public_plus_experimental_current.txt
index 2636cf73..bf268ec 100644
--- a/wear/wear-watchface-client/guava/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/guava/api/public_plus_experimental_current.txt
@@ -7,9 +7,9 @@
     method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
     method public static final com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.ListenableWatchFaceControlClient> createWatchFaceControlClient(android.content.Context context, String watchFacePackageName);
     method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
-    method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+    method public suspend Object? getOrCreateInteractiveWatchFaceClient(String p, androidx.wear.watchface.client.DeviceConfig p1, androidx.wear.watchface.client.WatchUiState p2, androidx.wear.watchface.style.UserStyleData? p3, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? p4, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> $completion);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceClient> listenableGetOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     field public static final androidx.wear.watchface.client.ListenableWatchFaceControlClient.Companion Companion;
   }
 
diff --git a/wear/wear-watchface-client/guava/api/restricted_current.txt b/wear/wear-watchface-client/guava/api/restricted_current.txt
index 2636cf73..bf268ec 100644
--- a/wear/wear-watchface-client/guava/api/restricted_current.txt
+++ b/wear/wear-watchface-client/guava/api/restricted_current.txt
@@ -7,9 +7,9 @@
     method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
     method public static final com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.ListenableWatchFaceControlClient> createWatchFaceControlClient(android.content.Context context, String watchFacePackageName);
     method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
-    method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
-    method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
-    method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+    method public suspend Object? getOrCreateInteractiveWatchFaceClient(String p, androidx.wear.watchface.client.DeviceConfig p1, androidx.wear.watchface.client.WatchUiState p2, androidx.wear.watchface.style.UserStyleData? p3, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? p4, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> $completion);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceClient> listenableGetOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     field public static final androidx.wear.watchface.client.ListenableWatchFaceControlClient.Companion Companion;
   }
 
diff --git a/wear/wear-watchface-client/guava/build.gradle b/wear/wear-watchface-client/guava/build.gradle
index 43b465a..3c0a1a5 100644
--- a/wear/wear-watchface-client/guava/build.gradle
+++ b/wear/wear-watchface-client/guava/build.gradle
@@ -46,7 +46,7 @@
     name = "Android Wear Watchface Client Guava"
     type = LibraryType.PUBLISHED_LIBRARY
     mavenGroup = LibraryGroups.WEAR
-    mavenVersion = LibraryVersions.WEAR_WATCHFACE_CLIENT
+    mavenVersion = LibraryVersions.WEAR_WATCHFACE_CLIENT_GUAVA
     inceptionYear = "2021"
     description = "Guava wrappers for the Androidx Wear Watchface library"
 }
diff --git a/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt b/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
index 12aa604..fbb2ad3 100644
--- a/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
@@ -31,7 +31,7 @@
 import androidx.test.filters.MediumTest
 import androidx.wear.watchface.client.DeviceConfig
 import androidx.wear.watchface.client.ListenableWatchFaceControlClient
-import androidx.wear.watchface.client.SystemState
+import androidx.wear.watchface.client.WatchUiState
 import androidx.wear.watchface.control.WatchFaceControlServiceFactory
 import androidx.wear.watchface.samples.createExampleCanvasAnalogWatchFaceBuilder
 import com.google.common.truth.Truth.assertThat
@@ -56,7 +56,7 @@
     private lateinit var surface: Surface
 
     @Before
-    fun setUp() {
+    public fun setUp() {
         MockitoAnnotations.initMocks(this)
         Mockito.`when`(surfaceHolder.surfaceFrame)
             .thenReturn(Rect(0, 0, 400, 400))
@@ -83,7 +83,7 @@
             400
         )!!
 
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings.map { it.id })
+        assertThat(headlessInstance.userStyleSchema.userStyleSettings.map { it.id.value })
             .containsExactly(
                 "color_style_setting",
                 "draw_hour_pips_style_setting",
@@ -126,7 +126,7 @@
         ).get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
 
         val interactiveInstanceFuture =
-            client.listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+            client.listenableGetOrCreateInteractiveWatchFaceClient(
                 "listenableTestId",
                 DeviceConfig(
                     false,
@@ -134,7 +134,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 null,
                 null
             )
@@ -152,7 +152,7 @@
         )
 
         val interactiveInstance = interactiveInstanceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
-        assertThat(interactiveInstance.userStyleSchema.userStyleSettings.map { it.id })
+        assertThat(interactiveInstance.userStyleSchema.userStyleSettings.map { it.id.value })
             .containsExactly(
                 "color_style_setting",
                 "draw_hour_pips_style_setting",
@@ -221,7 +221,7 @@
         ).get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
 
         val interactiveInstanceFuture =
-            client.listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+            client.listenableGetOrCreateInteractiveWatchFaceClient(
                 "listenableTestId",
                 DeviceConfig(
                     false,
@@ -229,7 +229,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 null,
                 null
             )
@@ -271,7 +271,7 @@
             context.packageName
         ).get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
 
-        assertNull(client.getInteractiveWatchFaceSysUiClientInstance("I do not exist"))
+        assertNull(client.getInteractiveWatchFaceClientInstance("I do not exist"))
     }
 }
 
diff --git a/wear/wear-watchface-client/guava/src/main/java/androidx/wear/watchface/client/ListenableWatchFaceControlClient.kt b/wear/wear-watchface-client/guava/src/main/java/androidx/wear/watchface/client/ListenableWatchFaceControlClient.kt
index 3857f023..112669c 100644
--- a/wear/wear-watchface-client/guava/src/main/java/androidx/wear/watchface/client/ListenableWatchFaceControlClient.kt
+++ b/wear/wear-watchface-client/guava/src/main/java/androidx/wear/watchface/client/ListenableWatchFaceControlClient.kt
@@ -23,10 +23,12 @@
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.utility.AsyncTraceEvent
 import androidx.wear.watchface.client.WatchFaceControlClient.ServiceNotBoundException
+import androidx.wear.watchface.style.UserStyleData
 import com.google.common.util.concurrent.ListenableFuture
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.async
 import kotlinx.coroutines.guava.asListenableFuture
 import kotlinx.coroutines.launch
 import kotlin.coroutines.CoroutineContext
@@ -38,12 +40,20 @@
 public open class ListenableWatchFaceControlClient(
     private val watchFaceControlClient: WatchFaceControlClient
 ) : WatchFaceControlClient {
-    override fun getInteractiveWatchFaceSysUiClientInstance(
+    override fun getInteractiveWatchFaceClientInstance(
         instanceId: String
-    ): InteractiveWatchFaceSysUiClient? =
-        watchFaceControlClient.getInteractiveWatchFaceSysUiClientInstance(instanceId)
+    ): InteractiveWatchFaceClient? =
+        watchFaceControlClient.getInteractiveWatchFaceClientInstance(instanceId)
 
     public companion object {
+        private val immediateCoroutineScope = CoroutineScope(
+            object : CoroutineDispatcher() {
+                override fun dispatch(context: CoroutineContext, block: Runnable) {
+                    block.run()
+                }
+            }
+        )
+
         /**
          * Returns a [ListenableFuture] for a [ListenableWatchFaceControlClient] which attempts to
          * connect to a watch face in the android package [watchFacePackageName].
@@ -62,13 +72,7 @@
                 "ListenableWatchFaceControlClient.createWatchFaceControlClient"
             )
             val future = ResolvableFuture.create<ListenableWatchFaceControlClient>()
-            val coroutineScope =
-                CoroutineScope(object : CoroutineDispatcher() {
-                    override fun dispatch(context: CoroutineContext, block: Runnable) {
-                        block.run()
-                    }
-                })
-            coroutineScope.launch {
+            immediateCoroutineScope.launch {
                 try {
                     future.set(
                         ListenableWatchFaceControlClient(
@@ -103,35 +107,36 @@
 
     /**
      * [ListenableFuture] wrapper around
-     * [WatchFaceControlClient.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync].
+     * [WatchFaceControlClient.getOrCreateInteractiveWatchFaceClient].
      * This is open to allow mocking.
      */
-    public open fun listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+    public open fun listenableGetOrCreateInteractiveWatchFaceClient(
         id: String,
         deviceConfig: DeviceConfig,
-        systemState: SystemState,
-        userStyle: Map<String, String>?,
+        watchUiState: WatchUiState,
+        userStyle: UserStyleData?,
         idToComplicationData: Map<Int, ComplicationData>?
-    ): ListenableFuture<InteractiveWatchFaceWcsClient> =
-        watchFaceControlClient.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    ): ListenableFuture<InteractiveWatchFaceClient> = immediateCoroutineScope.async {
+        watchFaceControlClient.getOrCreateInteractiveWatchFaceClient(
             id,
             deviceConfig,
-            systemState,
+            watchUiState,
             userStyle,
             idToComplicationData
-        ).asListenableFuture()
+        )
+    }.asListenableFuture()
 
-    override fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    override suspend fun getOrCreateInteractiveWatchFaceClient(
         id: String,
         deviceConfig: DeviceConfig,
-        systemState: SystemState,
-        userStyle: Map<String, String>?,
+        watchUiState: WatchUiState,
+        userStyle: UserStyleData?,
         idToComplicationData: Map<Int, ComplicationData>?
-    ): Deferred<InteractiveWatchFaceWcsClient> =
-        watchFaceControlClient.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    ): InteractiveWatchFaceClient =
+        watchFaceControlClient.getOrCreateInteractiveWatchFaceClient(
             id,
             deviceConfig,
-            systemState,
+            watchUiState,
             userStyle,
             idToComplicationData
         )
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/EditorServiceClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/EditorServiceClientTest.kt
index 7e58fd9..cf35008 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/EditorServiceClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/EditorServiceClientTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.wear.watchface.client.EditorObserverListener
+import androidx.wear.watchface.client.EditorListener
 import androidx.wear.watchface.client.EditorServiceClientImpl
 import androidx.wear.watchface.client.EditorState
 import androidx.wear.watchface.editor.EditorService
@@ -37,27 +37,37 @@
     @Test
     fun registerObserver() {
         lateinit var observedEditorState: EditorState
-        val observer = object : EditorObserverListener {
-            override fun onEditorStateChange(editorState: EditorState) {
+        val observer = object : EditorListener {
+            override fun onEditorStateChanged(editorState: EditorState) {
                 observedEditorState = editorState
             }
         }
-        editorServiceClient.registerObserver(observer)
+        editorServiceClient.addListener(observer) { runnable -> runnable.run() }
 
         val watchFaceInstanceId = "id-1"
         EditorService.globalEditorService.broadcastEditorState(
             EditorStateWireFormat(
                 watchFaceInstanceId,
-                UserStyleWireFormat(mapOf("color" to "red", "size" to "small")),
+                UserStyleWireFormat(
+                    mapOf(
+                        "color" to "red".encodeToByteArray(),
+                        "size" to "small".encodeToByteArray()
+                    )
+                ),
                 emptyList(),
                 true
             )
         )
 
-        editorServiceClient.unregisterObserver(observer)
+        editorServiceClient.removeListener(observer)
 
-        assertThat(observedEditorState.watchFaceInstanceId).isEqualTo(watchFaceInstanceId)
+        assertThat(observedEditorState.watchFaceId.id).isEqualTo(watchFaceInstanceId)
         assertThat(observedEditorState.userStyle.toString()).isEqualTo("{color=red, size=small}")
-        assertTrue(observedEditorState.commitChanges)
+        assertTrue(observedEditorState.shouldCommitChanges)
+
+        val editorStateString = observedEditorState.toString()
+        assertThat(editorStateString).contains("watchFaceId: $watchFaceInstanceId")
+        assertThat(editorStateString).contains("{color=red, size=small}")
+        assertThat(editorStateString).contains("shouldCommitChanges: true")
     }
 }
\ No newline at end of file
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 6548a2e..bfb35dd 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -40,8 +40,9 @@
 import androidx.wear.watchface.LayerMode
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.client.DeviceConfig
-import androidx.wear.watchface.client.SystemState
+import androidx.wear.watchface.client.HeadlessWatchFaceClient
 import androidx.wear.watchface.client.WatchFaceControlClient
+import androidx.wear.watchface.client.WatchUiState
 import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.data.ComplicationBoundsType
 import androidx.wear.watchface.samples.BLUE_STYLE
@@ -55,7 +56,11 @@
 import androidx.wear.watchface.samples.NO_COMPLICATIONS
 import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING
 import androidx.wear.watchface.style.Layer
+import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption
+import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
+import androidx.wear.watchface.style.UserStyleData
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.async
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withTimeout
 import org.junit.After
@@ -76,7 +81,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
-class WatchFaceControlClientTest {
+public class WatchFaceControlClientTest {
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private val service = runBlocking {
         WatchFaceControlClient.createWatchFaceControlClientImpl(
@@ -89,6 +94,7 @@
 
     @Mock
     private lateinit var surfaceHolder: SurfaceHolder
+
     @Mock
     private lateinit var surface: Surface
     private lateinit var engine: WallpaperService.Engine
@@ -97,7 +103,7 @@
     private lateinit var wallpaperService: TestExampleCanvasAnalogWatchFaceService
 
     @Before
-    fun setUp() {
+    public fun setUp() {
         MockitoAnnotations.initMocks(this)
         wallpaperService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder)
 
@@ -107,7 +113,7 @@
     }
 
     @After
-    fun tearDown() {
+    public fun tearDown() {
         if (this::engine.isInitialized) {
             engine.onDestroy()
         }
@@ -115,7 +121,8 @@
     }
 
     @get:Rule
-    val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface-client")
+    public val screenshotRule: AndroidXScreenshotTestRule =
+        AndroidXScreenshotTestRule("wear/wear-watchface-client")
 
     private val exampleWatchFaceComponentName = ComponentName(
         "androidx.wear.watchface.samples.test",
@@ -129,7 +136,7 @@
         0
     )
 
-    private val systemState = SystemState(false, 0)
+    private val systemState = WatchUiState(false, 0)
 
     private val complications = mapOf(
         EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
@@ -157,7 +164,7 @@
     }
 
     @Test
-    fun headlessScreenshot() {
+    public fun headlessScreenshot() {
         val headlessInstance = service.createHeadlessWatchFaceClient(
             ComponentName(
                 "androidx.wear.watchface.samples.test",
@@ -172,7 +179,7 @@
             400,
             400
         )!!
-        val bitmap = headlessInstance.takeWatchFaceScreenshot(
+        val bitmap = headlessInstance.renderWatchFaceToBitmap(
             RenderParameters(
                 DrawMode.INTERACTIVE,
                 RenderParameters.DRAW_ALL_LAYERS,
@@ -190,7 +197,7 @@
     }
 
     @Test
-    fun yellowComplicationHighlights() {
+    public fun yellowComplicationHighlights() {
         val headlessInstance = service.createHeadlessWatchFaceClient(
             ComponentName(
                 "androidx.wear.watchface.samples.test",
@@ -205,13 +212,13 @@
             400,
             400
         )!!
-        val bitmap = headlessInstance.takeWatchFaceScreenshot(
+        val bitmap = headlessInstance.renderWatchFaceToBitmap(
             RenderParameters(
                 DrawMode.INTERACTIVE,
                 mapOf(
-                    Layer.BASE_LAYER to LayerMode.DRAW,
+                    Layer.BASE to LayerMode.DRAW,
                     Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
-                    Layer.TOP_LAYER to LayerMode.DRAW
+                    Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
                 ),
                 null,
                 Color.YELLOW
@@ -227,7 +234,7 @@
     }
 
     @Test
-    fun headlessComplicationDetails() {
+    public fun headlessComplicationDetails() {
         val headlessInstance = service.createHeadlessWatchFaceClient(
             exampleWatchFaceComponentName,
             deviceConfig,
@@ -235,9 +242,9 @@
             400
         )!!
 
-        assertThat(headlessInstance.complicationState.size).isEqualTo(2)
+        assertThat(headlessInstance.complicationsState.size).isEqualTo(2)
 
-        val leftComplicationDetails = headlessInstance.complicationState[
+        val leftComplicationDetails = headlessInstance.complicationsState[
             EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
         ]!!
         assertThat(leftComplicationDetails.bounds).isEqualTo(Rect(80, 160, 160, 240))
@@ -257,7 +264,7 @@
         )
         assertTrue(leftComplicationDetails.isEnabled)
 
-        val rightComplicationDetails = headlessInstance.complicationState[
+        val rightComplicationDetails = headlessInstance.complicationsState[
             EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
         ]!!
         assertThat(rightComplicationDetails.bounds).isEqualTo(Rect(240, 160, 320, 240))
@@ -281,7 +288,7 @@
     }
 
     @Test
-    fun headlessUserStyleSchema() {
+    public fun headlessUserStyleSchema() {
         val headlessInstance = service.createHeadlessWatchFaceClient(
             exampleWatchFaceComponentName,
             deviceConfig,
@@ -290,16 +297,16 @@
         )!!
 
         assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(4)
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[0].id).isEqualTo(
+        assertThat(headlessInstance.userStyleSchema.userStyleSettings[0].id.value).isEqualTo(
             "color_style_setting"
         )
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[1].id).isEqualTo(
+        assertThat(headlessInstance.userStyleSchema.userStyleSettings[1].id.value).isEqualTo(
             "draw_hour_pips_style_setting"
         )
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[2].id).isEqualTo(
+        assertThat(headlessInstance.userStyleSchema.userStyleSettings[2].id.value).isEqualTo(
             "watch_hand_length_style_setting"
         )
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[3].id).isEqualTo(
+        assertThat(headlessInstance.userStyleSchema.userStyleSettings[3].id.value).isEqualTo(
             "complications_style_setting"
         )
 
@@ -307,27 +314,39 @@
     }
 
     @Test
-    fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient() {
-        val deferredInteractiveInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public fun headlessToBundleAndCreateFromBundle() {
+        val headlessInstance = HeadlessWatchFaceClient.createFromBundle(
+            service.createHeadlessWatchFaceClient(
+                exampleWatchFaceComponentName,
+                deviceConfig,
+                400,
+                400
+            )!!.toBundle()
+        )
+
+        assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(4)
+    }
+
+    @Test
+    public fun getOrCreateInteractiveWatchFaceClient(): Unit = runBlocking {
+        val deferredInteractiveInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 null,
                 complications
             )
+        }
 
-        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
-        createEngine()
+        // Create the engine which triggers creation of InteractiveWatchFaceClient.
+        async { createEngine() }
 
-        val interactiveInstance =
-            runBlocking {
-                withTimeout(CONNECT_TIMEOUT_MILLIS) {
-                    deferredInteractiveInstance.await()
-                }
-            }
+        val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            deferredInteractiveInstance.await()
+        }
 
-        val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
             RenderParameters(
                 DrawMode.INTERACTIVE,
                 RenderParameters.DRAW_ALL_LAYERS,
@@ -347,32 +366,32 @@
     }
 
     @Test
-    fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient_initialStyle() {
-        val deferredInteractiveInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public fun getOrCreateInteractiveWatchFaceClient_initialStyle(): Unit = runBlocking {
+        val deferredInteractiveInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 // An incomplete map which is OK.
-                mapOf(
-                    "color_style_setting" to "green_style",
-                    "draw_hour_pips_style_setting" to "false",
-                    "watch_hand_length_style_setting" to "0.8"
+                UserStyleData(
+                    mapOf(
+                        "color_style_setting" to "green_style".encodeToByteArray(),
+                        "draw_hour_pips_style_setting" to BooleanOption(false).id.value,
+                        "watch_hand_length_style_setting" to DoubleRangeOption(0.8).id.value
+                    )
                 ),
                 complications
             )
+        }
 
-        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
-        createEngine()
+        // Create the engine which triggers creation of InteractiveWatchFaceClient.
+        async { createEngine() }
 
-        val interactiveInstance =
-            runBlocking {
-                withTimeout(CONNECT_TIMEOUT_MILLIS) {
-                    deferredInteractiveInstance.await()
-                }
-            }
+        val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            deferredInteractiveInstance.await()
+        }
 
-        val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
             RenderParameters(
                 DrawMode.INTERACTIVE,
                 RenderParameters.DRAW_ALL_LAYERS,
@@ -392,25 +411,23 @@
     }
 
     @Test
-    fun wallpaperServiceBackedInteractiveWatchFaceWcsClient_ComplicationDetails() {
-        val deferredInteractiveInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public fun interactiveWatchFaceClient_ComplicationDetails(): Unit = runBlocking {
+        val deferredInteractiveInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 null,
                 complications
             )
+        }
 
-        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
-        createEngine()
+        // Create the engine which triggers creation of InteractiveWatchFaceClient.
+        async { createEngine() }
 
-        val interactiveInstance =
-            runBlocking {
-                withTimeout(CONNECT_TIMEOUT_MILLIS) {
-                    deferredInteractiveInstance.await()
-                }
-            }
+        val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            deferredInteractiveInstance.await()
+        }
 
         interactiveInstance.updateComplicationData(
             mapOf(
@@ -425,9 +442,9 @@
             )
         )
 
-        assertThat(interactiveInstance.complicationState.size).isEqualTo(2)
+        assertThat(interactiveInstance.complicationsState.size).isEqualTo(2)
 
-        val leftComplicationDetails = interactiveInstance.complicationState[
+        val leftComplicationDetails = interactiveInstance.complicationsState[
             EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
         ]!!
         assertThat(leftComplicationDetails.bounds).isEqualTo(Rect(80, 160, 160, 240))
@@ -450,7 +467,7 @@
             ComplicationType.SHORT_TEXT
         )
 
-        val rightComplicationDetails = interactiveInstance.complicationState[
+        val rightComplicationDetails = interactiveInstance.complicationsState[
             EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
         ]!!
         assertThat(rightComplicationDetails.bounds).isEqualTo(Rect(240, 160, 320, 240))
@@ -477,72 +494,68 @@
     }
 
     @Test
-    fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient_existingOpenInstance() {
-        val deferredInteractiveInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance(): Unit = runBlocking {
+        val deferredInteractiveInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 null,
                 complications
             )
-
-        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
-        createEngine()
-
-        runBlocking {
-            withTimeout(CONNECT_TIMEOUT_MILLIS) {
-                deferredInteractiveInstance.await()
-            }
         }
 
-        val existingInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+        // Create the engine which triggers creation of InteractiveWatchFaceClient.
+        async { createEngine() }
+
+        withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            deferredInteractiveInstance.await()
+        }
+
+        withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 null,
                 complications
             )
-
-        assertTrue(existingInstance.isCompleted)
+        }
     }
 
     @Test
-    fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient_existingClosedInstance() {
-        val deferredInteractiveInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public fun getOrCreateInteractiveWatchFaceClient_existingClosedInstance(): Unit = runBlocking {
+        val deferredInteractiveInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 null,
                 complications
             )
+        }
 
-        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
-        createEngine()
+        // Create the engine which triggers creation of InteractiveWatchFaceClient.
+        async { createEngine() }
 
         // Wait for the instance to be created.
-        val interactiveInstance =
-            runBlocking {
-                withTimeout(CONNECT_TIMEOUT_MILLIS) {
-                    deferredInteractiveInstance.await()
-                }
-            }
-
+        val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            deferredInteractiveInstance.await()
+        }
         // Closing this interface means the subsequent
-        // getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient won't immediately return
+        // getOrCreateInteractiveWatchFaceClient won't immediately return
         // a resolved future.
         interactiveInstance.close()
 
-        val deferredExistingInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+        val deferredExistingInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 null,
                 complications
             )
+        }
 
         assertFalse(deferredExistingInstance.isCompleted)
 
@@ -556,36 +569,34 @@
                 surfaceHolder.surfaceFrame.height()
             )
         }
-        runBlocking {
-            withTimeout(CONNECT_TIMEOUT_MILLIS) {
-                deferredExistingInstance.await()
-            }
+
+        withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            deferredExistingInstance.await()
         }
     }
 
     @Test
-    fun getInteractiveWatchFaceInstanceSysUi() {
-        val deferredInteractiveInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public fun getInteractiveWatchFaceInstance(): Unit = runBlocking {
+        val deferredInteractiveInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 null,
                 complications
             )
+        }
 
-        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
-        createEngine()
+        // Create the engine which triggers creation of InteractiveWatchFaceClient.
+        async { createEngine() }
 
         // Wait for the instance to be created.
-        runBlocking {
-            withTimeout(CONNECT_TIMEOUT_MILLIS) {
-                deferredInteractiveInstance.await()
-            }
+        withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            deferredInteractiveInstance.await()
         }
 
         val sysUiInterface =
-            service.getInteractiveWatchFaceSysUiClientInstance("testId")!!
+            service.getInteractiveWatchFaceClientInstance("testId")!!
 
         val contentDescriptionLabels = sysUiInterface.contentDescriptionLabels
         assertThat(contentDescriptionLabels.size).isEqualTo(3)
@@ -608,23 +619,26 @@
     }
 
     @Test
-    fun updateInstance() {
-        val deferredInteractiveInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public fun updateInstance(): Unit = runBlocking {
+        val deferredInteractiveInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
-                mapOf(
-                    COLOR_STYLE_SETTING to GREEN_STYLE,
-                    WATCH_HAND_LENGTH_STYLE_SETTING to "0.25",
-                    DRAW_HOUR_PIPS_STYLE_SETTING to "false",
-                    COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS
+                UserStyleData(
+                    mapOf(
+                        COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray(),
+                        WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.25).id.value,
+                        DRAW_HOUR_PIPS_STYLE_SETTING to BooleanOption(false).id.value,
+                        COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS.encodeToByteArray()
+                    )
                 ),
                 complications
             )
+        }
 
-        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
-        createEngine()
+        // Create the engine which triggers creation of InteractiveWatchFaceClient.
+        async { createEngine() }
 
         // Wait for the instance to be created.
         val interactiveInstance = runBlocking {
@@ -637,11 +651,13 @@
 
         // Note this map doesn't include all the categories, which is fine the others will be set
         // to their defaults.
-        interactiveInstance.updateInstance(
+        interactiveInstance.updateWatchFaceInstance(
             "testId2",
-            mapOf(
-                COLOR_STYLE_SETTING to BLUE_STYLE,
-                WATCH_HAND_LENGTH_STYLE_SETTING to "0.9",
+            UserStyleData(
+                mapOf(
+                    COLOR_STYLE_SETTING to BLUE_STYLE.encodeToByteArray(),
+                    WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.9).id.value,
+                )
             )
         )
 
@@ -649,20 +665,20 @@
 
         // The complications should have been cleared.
         val leftComplication =
-            interactiveInstance.complicationState[EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID]!!
+            interactiveInstance.complicationsState[EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID]!!
         val rightComplication =
-            interactiveInstance.complicationState[EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID]!!
+            interactiveInstance.complicationsState[EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID]!!
         assertThat(leftComplication.currentType).isEqualTo(ComplicationType.NO_DATA)
         assertThat(rightComplication.currentType).isEqualTo(ComplicationType.NO_DATA)
 
-        // It should be possible to create a SysUI instance with the updated id.
-        val sysUiInterface =
-            service.getInteractiveWatchFaceSysUiClientInstance("testId2")
-        assertThat(sysUiInterface).isNotNull()
-        sysUiInterface?.close()
+        // It should be possible to create an instance with the updated id.
+        val instance =
+            service.getInteractiveWatchFaceClientInstance("testId2")
+        assertThat(instance).isNotNull()
+        instance?.close()
 
         interactiveInstance.updateComplicationData(complications)
-        val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
             RenderParameters(
                 DrawMode.INTERACTIVE,
                 RenderParameters.DRAW_ALL_LAYERS,
@@ -683,23 +699,22 @@
     }
 
     @Test
-    fun getComplicationIdAt() {
-        val deferredInteractiveInstance =
-            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public fun getComplicationIdAt(): Unit = runBlocking {
+        val deferredInteractiveInstance = async {
+            service.getOrCreateInteractiveWatchFaceClient(
                 "testId",
                 deviceConfig,
                 systemState,
                 null,
                 complications
             )
+        }
 
-        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
-        createEngine()
+        // Create the engine which triggers creation of InteractiveWatchFaceClient.
+        async { createEngine() }
 
-        val interactiveInstance = runBlocking {
-            withTimeout(CONNECT_TIMEOUT_MILLIS) {
-                deferredInteractiveInstance.await()
-            }
+        val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+            deferredInteractiveInstance.await()
         }
 
         assertNull(interactiveInstance.getComplicationIdAt(0, 0))
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
index 802461f..c48e5da 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
@@ -22,36 +22,30 @@
 import androidx.wear.watchface.editor.data.EditorStateWireFormat
 import java.util.concurrent.Executor
 
-/** Client for the watchface editor service. */
+/**
+ * Client for the watchface editor service, which observes
+ * [androidx.wear.watchface.editor.EditorSession]. This client can be reused to observe multiple
+ * editor sessions.
+ */
 public interface EditorServiceClient {
     /**
-     * Starts listening to [androidx.wear.watchface.editor.EditorSession] events, with the callback
-     * executed by an immediate executor on an undefined thread.
+     * Starts listening for [EditorState] which is sent when
+     * [androidx.wear.watchface.editor.EditorSession] closes. The
+     * [EditorListener.onEditorStateChanged] callback is run on the specified [listenerExecutor].
      */
-    public fun registerObserver(
-        editorObserverListener: EditorObserverListener
-    )
+    public fun addListener(editorListener: EditorListener, listenerExecutor: Executor)
 
-    /**
-     * Starts listening to [androidx.wear.watchface.editor.EditorSession] events with the callback
-     * run on the specified [Executor] by an immediate executor, on an undefined thread if `null`.
-     */
-    public fun registerObserver(
-        observerCallbackExecutor: Executor? = null,
-        editorObserverListener: EditorObserverListener
-    )
-
-    /** Unregisters an [EditorObserverListener] previously registered via [registerObserver].  */
-    public fun unregisterObserver(editorObserverListener: EditorObserverListener)
+    /** Unregisters an [EditorListener] previously registered via [addListener].  */
+    public fun removeListener(editorListener: EditorListener)
 
     /** Instructs any open editor to close. */
     public fun closeEditor()
 }
 
 /** Observes state changes in [androidx.wear.watchface.editor.EditorSession]. */
-public interface EditorObserverListener {
+public interface EditorListener {
     /** Called in response to [androidx.wear.watchface.editor.EditorSession.close] .*/
-    public fun onEditorStateChange(editorState: EditorState)
+    public fun onEditorStateChanged(editorState: EditorState)
 }
 
 /** @hide */
@@ -60,28 +54,18 @@
     private val iEditorService: IEditorService
 ) : EditorServiceClient {
     private val lock = Any()
-    private val editorMap = HashMap<EditorObserverListener, Int>()
+    private val editorMap = HashMap<EditorListener, Int>()
 
-    override fun registerObserver(editorObserverListener: EditorObserverListener) {
-        registerObserver(null, editorObserverListener)
-    }
-
-    override fun registerObserver(
-        observerCallbackExecutor: Executor?,
-        editorObserverListener: EditorObserverListener
+    override fun addListener(
+        editorListener: EditorListener,
+        listenerExecutor: Executor
     ) {
-        val executor = observerCallbackExecutor ?: object : Executor {
-            override fun execute(runnable: Runnable) {
-                runnable.run()
-            }
-        }
-
         val observer = object : IEditorObserver.Stub() {
             override fun getApiVersion() = IEditorObserver.API_VERSION
 
             override fun onEditorStateChange(editorStateWireFormat: EditorStateWireFormat) {
-                executor.execute {
-                    editorObserverListener.onEditorStateChange(
+                listenerExecutor.execute {
+                    editorListener.onEditorStateChanged(
                         editorStateWireFormat.asApiEditorState()
                     )
                 }
@@ -89,15 +73,15 @@
         }
 
         synchronized(lock) {
-            editorMap[editorObserverListener] = iEditorService.registerObserver(observer)
+            editorMap[editorListener] = iEditorService.registerObserver(observer)
         }
     }
 
-    override fun unregisterObserver(editorObserverListener: EditorObserverListener) {
+    override fun removeListener(editorListener: EditorListener) {
         synchronized(lock) {
-            editorMap[editorObserverListener]?.let {
+            editorMap[editorListener]?.let {
                 iEditorService.unregisterObserver(it)
-                editorMap.remove(editorObserverListener)
+                editorMap.remove(editorListener)
             }
         }
     }
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorState.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorState.kt
index 4d8b8fd..339e657 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorState.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorState.kt
@@ -20,42 +20,60 @@
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.wear.complications.data.ComplicationData
-import androidx.wear.complications.data.asApiComplicationData
+import androidx.wear.complications.data.toApiComplicationData
 import androidx.wear.watchface.editor.data.EditorStateWireFormat
 import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleData
+
+/**
+ * The system is responsible for the management and generation of these ids and they have no
+ * context outside of an instance of an EditorState and should not be stored or saved for later
+ * use by the WatchFace provider.
+ *
+ * @param id The system's id for a watch face being edited. This is passed in from
+ *     [androidx.wear.watchface.EditorRequest.watchFaceId].
+ */
+public class WatchFaceId(public val id: String)
 
 /**
  * The state of the editing session. See [androidx.wear.watchface.editor.EditorSession].
  *
- * @param watchFaceInstanceId Unique ID for the instance of the watch face being edited, only
- *     defined for Android R and beyond.
- * @param userStyle The current [UserStyle] encoded as a Map<String, String>.
- * @param previewComplicationData Preview [ComplicationData] needed for taking screenshots without
+ * @param watchFaceId Unique ID for the instance of the watch face being edited (see
+ *     [androidx.wear.watchface.editor.EditorRequest.watchFaceId]), only defined for
+ *     Android R and beyond.
+ * @param userStyle The current [UserStyle] encoded as a [UserStyleData].
+ * @param previewComplicationsData Preview [ComplicationData] needed for taking screenshots without
  *     live complication data.
- * @param commitChanges Whether or not this state should be committed (i.e. the user aborted the
- *     session). If it's not committed then any changes (E.g. complication provider changes)
+ * @param shouldCommitChanges Whether or not this state should be committed (i.e. the user aborted
+ *     the session). If it's not committed then any changes (E.g. complication provider changes)
  *     should be abandoned. There's no need to resend the style to the watchface because the
  *     library will have restored the previous style.
  */
 public class EditorState internal constructor(
     @RequiresApi(Build.VERSION_CODES.R)
-    public val watchFaceInstanceId: String,
-    public val userStyle: Map<String, String>,
-    public val previewComplicationData: Map<Int, ComplicationData>,
-    @get:JvmName("hasCommitChanges")
-    public val commitChanges: Boolean
-)
+    public val watchFaceId: WatchFaceId,
+    public val userStyle: UserStyleData,
+    public val previewComplicationsData: Map<Int, ComplicationData>,
+    @get:JvmName("shouldCommitChanges")
+    public val shouldCommitChanges: Boolean
+) {
+    override fun toString(): String =
+        "{watchFaceId: ${watchFaceId.id}, userStyle: $userStyle" +
+            ", previewComplicationsData: [" +
+            previewComplicationsData.map { "${it.key} -> ${it.value}" }.joinToString() +
+            "], shouldCommitChanges: $shouldCommitChanges}"
+}
 
 /** @hide */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public fun EditorStateWireFormat.asApiEditorState(): EditorState {
     return EditorState(
-        watchFaceInstanceId ?: "",
-        userStyle.mUserStyle,
+        WatchFaceId(watchFaceInstanceId ?: ""),
+        UserStyleData(userStyle.mUserStyle),
         previewComplicationData.associateBy(
             { it.id },
-            { it.complicationData.asApiComplicationData() }
+            { it.complicationData.toApiComplicationData() }
         ),
         commitChanges
     )
-}
\ No newline at end of file
+}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
index 4ea6283..3234b51 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
@@ -17,18 +17,23 @@
 package androidx.wear.watchface.client
 
 import android.graphics.Bitmap
-import android.os.IBinder
+import android.os.Bundle
 import android.support.wearable.watchface.SharedMemoryImage
+import androidx.annotation.AnyThread
 import androidx.annotation.RequiresApi
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.utility.TraceEvent
+import androidx.wear.watchface.Complication
+import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.control.IHeadlessWatchFace
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.ComplicationRenderParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
+import java.util.concurrent.Executor
 
 /**
  * Controls a stateless remote headless watch face.  This is mostly intended for use by watch face
@@ -38,6 +43,14 @@
  * Note clients should call [close] when finished.
  */
 public interface HeadlessWatchFaceClient : AutoCloseable {
+    public companion object {
+        internal const val BINDER_KEY = "HeadlessWatchFaceClient"
+
+        @JvmStatic
+        public fun createFromBundle(bundle: Bundle): HeadlessWatchFaceClient =
+            HeadlessWatchFaceClientImpl(bundle.getBinder(BINDER_KEY) as IHeadlessWatchFace)
+    }
+
     /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
     public val previewReferenceTimeMillis: Long
 
@@ -45,21 +58,16 @@
     public val userStyleSchema: UserStyleSchema
 
     /**
-     * Map of complication ids to [ComplicationState] for each complication slot. Note this can
-     * change, typically in response to styling.
+     * Map of complication ids to [ComplicationState] for each [Complication] registered with the
+     * watch face's [ComplicationsManager]. The ComplicationState is based on the initial state of
+     * each Complication plus any overrides from the default style's
+     * [ComplicationsUserStyleSetting]. Because the style can't change, ComplicationState is
+     * immutable for a headless watch face.
      */
-    public val complicationState: Map<Int, ComplicationState>
-
-    public companion object {
-        /** Constructs a [HeadlessWatchFaceClient] from an [IBinder]. */
-        @JvmStatic
-        public fun createFromBinder(binder: IBinder): HeadlessWatchFaceClient =
-            HeadlessWatchFaceClientImpl(binder)
-    }
+    public val complicationsState: Map<Int, ComplicationState>
 
     /**
-     * Requests a shared memory backed [Bitmap] containing a screenshot of the watch face with the
-     * given settings.
+     * Renders the watchface to a shared memory backed [Bitmap] with the given settings.
      *
      * @param renderParameters The [RenderParameters] to draw with.
      * @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with.
@@ -70,7 +78,7 @@
      *     given settings.
      */
     @RequiresApi(27)
-    public fun takeWatchFaceScreenshot(
+    public fun renderWatchFaceToBitmap(
         renderParameters: RenderParameters,
         calendarTimeMillis: Long,
         userStyle: UserStyle?,
@@ -78,8 +86,7 @@
     ): Bitmap
 
     /**
-     * Requests a shared memory backed [Bitmap] containing a screenshot of the complication with the
-     * given settings.
+     * Renders the complication to a shared memory backed [Bitmap] with the given settings.
      *
      * @param complicationId The id of the complication to render
      * @param renderParameters The [RenderParameters] to draw with
@@ -90,7 +97,7 @@
      *     given settings, or `null` if [complicationId] is unrecognized.
      */
     @RequiresApi(27)
-    public fun takeComplicationScreenshot(
+    public fun renderComplicationToBitmap(
         complicationId: Int,
         renderParameters: RenderParameters,
         calendarTimeMillis: Long,
@@ -98,15 +105,59 @@
         userStyle: UserStyle?,
     ): Bitmap?
 
-    /** Returns the associated [IBinder]. Allows this interface to be passed over AIDL. */
-    public fun asBinder(): IBinder
+    /** Callback that observes when the client disconnects. */
+    public interface ClientDisconnectListener {
+        /**
+         * The client disconnected, typically due to the server side crashing. Note this is not
+         * called in response to [close] being called on [HeadlessWatchFaceClient].
+         */
+        public fun onClientDisconnected()
+    }
+
+    /** Registers a [ClientDisconnectListener]. */
+    @AnyThread
+    public fun addClientDisconnectListener(listener: ClientDisconnectListener, executor: Executor)
+
+    /**
+     * Removes a [ClientDisconnectListener] previously registered by [addClientDisconnectListener].
+     */
+    @AnyThread
+    public fun removeClientDisconnectListener(listener: ClientDisconnectListener)
+
+    /** Returns true if the connection to the server side is alive. */
+    @AnyThread
+    public fun isConnectionAlive(): Boolean
+
+    /** Stores the underlying connection in a [Bundle]. */
+    public fun toBundle(): Bundle
 }
 
 internal class HeadlessWatchFaceClientImpl internal constructor(
     private val iHeadlessWatchFace: IHeadlessWatchFace
 ) : HeadlessWatchFaceClient {
 
-    constructor(binder: IBinder) : this(IHeadlessWatchFace.Stub.asInterface(binder))
+    private val lock = Any()
+    private val listeners = HashMap<HeadlessWatchFaceClient.ClientDisconnectListener, Executor>()
+
+    init {
+        iHeadlessWatchFace.asBinder().linkToDeath(
+            {
+                var listenerCopy:
+                    HashMap<HeadlessWatchFaceClient.ClientDisconnectListener, Executor>
+
+                synchronized(lock) {
+                    listenerCopy = HashMap(listeners)
+                }
+
+                for ((listener, executor) in listenerCopy) {
+                    executor.execute {
+                        listener.onClientDisconnected()
+                    }
+                }
+            },
+            0
+        )
+    }
 
     override val previewReferenceTimeMillis: Long
         get() = iHeadlessWatchFace.previewReferenceTimeMillis
@@ -114,22 +165,22 @@
     override val userStyleSchema: UserStyleSchema
         get() = UserStyleSchema(iHeadlessWatchFace.userStyleSchema)
 
-    override val complicationState: Map<Int, ComplicationState>
+    override val complicationsState: Map<Int, ComplicationState>
         get() = iHeadlessWatchFace.complicationState.associateBy(
             { it.id },
             { ComplicationState(it.complicationState) }
         )
 
     @RequiresApi(27)
-    override fun takeWatchFaceScreenshot(
+    override fun renderWatchFaceToBitmap(
         renderParameters: RenderParameters,
         calendarTimeMillis: Long,
         userStyle: UserStyle?,
         idToComplicationData: Map<Int, ComplicationData>?
-    ): Bitmap = TraceEvent("HeadlessWatchFaceClientImpl.takeWatchFaceScreenshot").use {
+    ): Bitmap = TraceEvent("HeadlessWatchFaceClientImpl.renderWatchFaceToBitmap").use {
         SharedMemoryImage.ashmemReadImageBundle(
-            iHeadlessWatchFace.takeWatchFaceScreenshot(
-                WatchfaceScreenshotParams(
+            iHeadlessWatchFace.renderWatchFaceToBitmap(
+                WatchFaceRenderParams(
                     renderParameters.toWireFormat(),
                     calendarTimeMillis,
                     userStyle?.toWireFormat(),
@@ -145,15 +196,15 @@
     }
 
     @RequiresApi(27)
-    override fun takeComplicationScreenshot(
+    override fun renderComplicationToBitmap(
         complicationId: Int,
         renderParameters: RenderParameters,
         calendarTimeMillis: Long,
         complicationData: ComplicationData,
         userStyle: UserStyle?,
-    ): Bitmap? = TraceEvent("HeadlessWatchFaceClientImpl.takeComplicationScreenshot").use {
-        iHeadlessWatchFace.takeComplicationScreenshot(
-            ComplicationScreenshotParams(
+    ): Bitmap? = TraceEvent("HeadlessWatchFaceClientImpl.renderComplicationToBitmap").use {
+        iHeadlessWatchFace.renderComplicationToBitmap(
+            ComplicationRenderParams(
                 complicationId,
                 renderParameters.toWireFormat(),
                 calendarTimeMillis,
@@ -165,9 +216,33 @@
         }
     }
 
+    override fun addClientDisconnectListener(
+        listener: HeadlessWatchFaceClient.ClientDisconnectListener,
+        executor: Executor
+    ) {
+        synchronized(lock) {
+            require(!listeners.contains(listener)) {
+                "Don't call addClientDisconnectListener multiple times for the same listener"
+            }
+            listeners.put(listener, executor)
+        }
+    }
+
+    override fun removeClientDisconnectListener(
+        listener: HeadlessWatchFaceClient.ClientDisconnectListener
+    ) {
+        synchronized(lock) {
+            listeners.remove(listener)
+        }
+    }
+
+    override fun isConnectionAlive() = iHeadlessWatchFace.asBinder().isBinderAlive
+
+    override fun toBundle() = Bundle().apply {
+        this.putBinder(HeadlessWatchFaceClient.BINDER_KEY, iHeadlessWatchFace.asBinder())
+    }
+
     override fun close() = TraceEvent("HeadlessWatchFaceClientImpl.close").use {
         iHeadlessWatchFace.release()
     }
-
-    override fun asBinder(): IBinder = iHeadlessWatchFace.asBinder()
 }
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
new file mode 100644
index 0000000..decd2e9
--- /dev/null
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.client
+
+import android.app.PendingIntent
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.support.wearable.watchface.SharedMemoryImage
+import androidx.annotation.AnyThread
+import androidx.annotation.Px
+import androidx.annotation.RequiresApi
+import androidx.wear.complications.data.ComplicationData
+import androidx.wear.complications.data.ComplicationText
+import androidx.wear.complications.data.toApiComplicationText
+import androidx.wear.utility.TraceEvent
+import androidx.wear.watchface.Complication
+import androidx.wear.watchface.ComplicationsManager
+import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.TapType
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
+import androidx.wear.watchface.data.ComplicationBoundsType
+import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
+import androidx.wear.watchface.data.WatchUiState
+import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleData
+import java.util.Objects
+import java.util.concurrent.Executor
+
+/**
+ * Controls a stateful remote interactive watch face. Typically this will be used for the current
+ * active watch face.
+ *
+ * Note clients should call [close] when finished.
+ */
+public interface InteractiveWatchFaceClient : AutoCloseable {
+    /**
+     * Sends new ComplicationData to the watch face. Note this doesn't have to be a full update,
+     * it's possible to update just one complication at a time, but doing so may result in a less
+     * visually clean transition.
+     */
+    public fun updateComplicationData(idToComplicationData: Map<Int, ComplicationData>)
+
+    /**
+     * Renders the watchface to a shared memory backed [Bitmap] with the given settings.
+     *
+     * @param renderParameters The [RenderParameters] to draw with.
+     * @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with.
+     * @param userStyle Optional [UserStyle] to render with, if null the current style is used.
+     * @param idAndComplicationData Map of complication ids to [ComplicationData] to render with, or
+     *     if null then the existing complication data if any is used.
+     * @return A shared memory backed [Bitmap] containing a screenshot of the watch  face with the
+     *     given settings.
+     */
+    @RequiresApi(27)
+    public fun renderWatchFaceToBitmap(
+        renderParameters: RenderParameters,
+        calendarTimeMillis: Long,
+        userStyle: UserStyle?,
+        idAndComplicationData: Map<Int, ComplicationData>?
+    ): Bitmap
+
+    /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
+    public val previewReferenceTimeMillis: Long
+
+    /**
+     * Renames this instance to [newInstanceId] (must be unique, usually this would be different
+     * from the old ID but that's not a requirement). Sets the current [UserStyle] and clears
+     * any complication data. Setting the new UserStyle may have a side effect of enabling or
+     * disabling complications, which will be visible via [ComplicationState.isEnabled].
+     */
+    public fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyle)
+
+    /**
+     * Renames this instance to [newInstanceId] (must be unique, usually this would be different
+     * from the old ID but that's not a requirement). Sets the current [UserStyle] represented as a
+     * [UserStyleData> and clears any complication data. Setting the new UserStyle may have a
+     * side effect of enabling or disabling complications, which will be visible via
+     * [ComplicationState.isEnabled].
+     */
+    public fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyleData)
+
+    /** Returns the ID of this watch face instance. */
+    public val instanceId: String
+
+    /** The watch face's [UserStyleSchema]. */
+    public val userStyleSchema: UserStyleSchema
+
+    /**
+     * Map of complication ids to [ComplicationState] for each [Complication] registered with the
+     * watch face's [ComplicationsManager]. The ComplicationState is based on the initial state of
+     * each Complication plus any overrides from a [ComplicationsUserStyleSetting]. As a
+     * consequence ComplicationState may update based on style changes.
+     */
+    public val complicationsState: Map<Int, ComplicationState>
+
+    /** Returns the ID of the complication at the given coordinates or `null` if there isn't one.*/
+    @SuppressWarnings("AutoBoxing")
+    public fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? =
+        complicationsState.asSequence().firstOrNull {
+            it.value.isEnabled && when (it.value.boundsType) {
+                ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
+                ComplicationBoundsType.BACKGROUND -> false
+                ComplicationBoundsType.EDGE -> false
+                else -> false
+            }
+        }?.key
+
+    /**
+     * Requests that [ComplicationsManager.displayPressedAnimation] is called for [complicationId].
+     */
+    public fun displayPressedAnimation(complicationId: Int)
+
+    public companion object {
+        /** Indicates a "down" touch event on the watch face. */
+        public const val TAP_TYPE_DOWN: Int = IInteractiveWatchFace.TAP_TYPE_DOWN
+
+        /**
+         * Indicates that a previous [TAP_TYPE_DOWN] event has been canceled. This generally happens
+         * when the watch face is touched but then a move or long press occurs.
+         */
+        public const val TAP_TYPE_CANCEL: Int = IInteractiveWatchFace.TAP_TYPE_CANCEL
+
+        /**
+         * Indicates that an "up" event on the watch face has occurred that has not been consumed by
+         * another activity. A [TAP_TYPE_DOWN] always occur first. This event will not occur if a
+         * [TAP_TYPE_CANCEL] is sent.
+         */
+        public const val TAP_TYPE_UP: Int = IInteractiveWatchFace.TAP_TYPE_UP
+    }
+
+    /**
+     * Sends a tap event to the watch face for processing.
+     */
+    public fun sendTouchEvent(@Px xPosition: Int, @Px yPosition: Int, @TapType tapType: Int)
+
+    /**
+     * Describes regions of the watch face for use by a screen reader.
+     *
+     * @param text [ComplicationText] associated with the region, to be read by the screen reader.
+     * @param bounds [Rect] describing the area of the feature on screen.
+     * @param tapAction [PendingIntent] to be used if the screen reader's user triggers a tap
+     *     action.
+     */
+    public class ContentDescriptionLabel(
+        private val text: ComplicationText,
+        public val bounds: Rect,
+        public val tapAction: PendingIntent?
+    ) {
+        /**
+         * Returns the text that should be displayed for the given timestamp.
+         *
+         * @param resources [Resources] from the current [android.content.Context]
+         * @param dateTimeMillis milliseconds since epoch, e.g. from [System.currentTimeMillis]
+         */
+        public fun getTextAt(resources: Resources, dateTimeMillis: Long): CharSequence =
+            text.getTextAt(resources, dateTimeMillis)
+
+        override fun equals(other: Any?): Boolean =
+            other is ContentDescriptionLabel &&
+                text == other.text &&
+                bounds == other.bounds &&
+                tapAction == other.tapAction
+
+        override fun hashCode(): Int {
+            return Objects.hash(
+                text,
+                bounds,
+                tapAction
+            )
+        }
+    }
+
+    /**
+     * Returns the [ContentDescriptionLabel]s describing the watch face, for the use by screen
+     * readers.
+     */
+    public val contentDescriptionLabels: List<ContentDescriptionLabel>
+
+    /** Updates the watch faces [WatchUiState]. */
+    public fun setWatchUiState(watchUiState: androidx.wear.watchface.client.WatchUiState)
+
+    /** Triggers watch face rendering into the surface when in ambient mode. */
+    public fun performAmbientTick()
+
+    /** Callback that observes when the client disconnects. */
+    public interface ClientDisconnectListener {
+        /**
+         * The client disconnected, typically due to the server side crashing. Note this is not
+         * called in response to [close] being called on [InteractiveWatchFaceClient].
+         */
+        public fun onClientDisconnected()
+    }
+
+    /** Registers a [ClientDisconnectListener]. */
+    @AnyThread
+    public fun addClientDisconnectListener(listener: ClientDisconnectListener, executor: Executor)
+
+    /**
+     * Removes a [ClientDisconnectListener] previously registered by [addClientDisconnectListener].
+     */
+    @AnyThread
+    public fun removeClientDisconnectListener(listener: ClientDisconnectListener)
+
+    /** Returns true if the connection to the server side is alive. */
+    @AnyThread
+    public fun isConnectionAlive(): Boolean
+}
+
+/** Controls a stateful remote interactive watch face. */
+internal class InteractiveWatchFaceClientImpl internal constructor(
+    private val iInteractiveWatchFace: IInteractiveWatchFace
+) : InteractiveWatchFaceClient {
+
+    private val lock = Any()
+    private val listeners = HashMap<InteractiveWatchFaceClient.ClientDisconnectListener, Executor>()
+
+    init {
+        iInteractiveWatchFace.asBinder().linkToDeath(
+            {
+                var listenerCopy:
+                    HashMap<InteractiveWatchFaceClient.ClientDisconnectListener, Executor>
+
+                synchronized(lock) {
+                    listenerCopy = HashMap(listeners)
+                }
+
+                for ((listener, executor) in listenerCopy) {
+                    executor.execute {
+                        listener.onClientDisconnected()
+                    }
+                }
+            },
+            0
+        )
+    }
+
+    override fun updateComplicationData(
+        idToComplicationData: Map<Int, ComplicationData>
+    ) = TraceEvent("InteractiveWatchFaceClientImpl.updateComplicationData").use {
+        iInteractiveWatchFace.updateComplicationData(
+            idToComplicationData.map {
+                IdAndComplicationDataWireFormat(it.key, it.value.asWireComplicationData())
+            }
+        )
+    }
+
+    @RequiresApi(27)
+    override fun renderWatchFaceToBitmap(
+        renderParameters: RenderParameters,
+        calendarTimeMillis: Long,
+        userStyle: UserStyle?,
+        idAndComplicationData: Map<Int, ComplicationData>?
+    ): Bitmap = TraceEvent("InteractiveWatchFaceClientImpl.renderWatchFaceToBitmap").use {
+        SharedMemoryImage.ashmemReadImageBundle(
+            iInteractiveWatchFace.renderWatchFaceToBitmap(
+                WatchFaceRenderParams(
+                    renderParameters.toWireFormat(),
+                    calendarTimeMillis,
+                    userStyle?.toWireFormat(),
+                    idAndComplicationData?.map {
+                        IdAndComplicationDataWireFormat(
+                            it.key,
+                            it.value.asWireComplicationData()
+                        )
+                    }
+                )
+            )
+        )
+    }
+
+    override val previewReferenceTimeMillis: Long
+        get() = iInteractiveWatchFace.previewReferenceTimeMillis
+
+    override fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyle) = TraceEvent(
+        "InteractiveWatchFaceClientImpl.updateInstance"
+    ).use {
+        iInteractiveWatchFace.updateWatchfaceInstance(newInstanceId, userStyle.toWireFormat())
+    }
+
+    override fun updateWatchFaceInstance(
+        newInstanceId: String,
+        userStyle: UserStyleData
+    ) = TraceEvent(
+        "InteractiveWatchFaceClientImpl.updateInstance"
+    ).use {
+        iInteractiveWatchFace.updateWatchfaceInstance(
+            newInstanceId,
+            userStyle.toWireFormat()
+        )
+    }
+
+    override val instanceId: String
+        get() = iInteractiveWatchFace.instanceId
+
+    override val userStyleSchema: UserStyleSchema
+        get() = UserStyleSchema(iInteractiveWatchFace.userStyleSchema)
+
+    override val complicationsState: Map<Int, ComplicationState>
+        get() = iInteractiveWatchFace.complicationDetails.associateBy(
+            { it.id },
+            { ComplicationState(it.complicationState) }
+        )
+
+    override fun close() = TraceEvent("InteractiveWatchFaceClientImpl.close").use {
+        iInteractiveWatchFace.release()
+    }
+
+    override fun displayPressedAnimation(complicationId: Int) = TraceEvent(
+        "InteractiveWatchFaceClientImpl.bringAttentionToComplication"
+    ).use {
+        iInteractiveWatchFace.bringAttentionToComplication(complicationId)
+    }
+
+    override fun sendTouchEvent(
+        xPosition: Int,
+        yPosition: Int,
+        @TapType tapType: Int
+    ) = TraceEvent("InteractiveWatchFaceClientImpl.sendTouchEvent").use {
+        iInteractiveWatchFace.sendTouchEvent(xPosition, yPosition, tapType)
+    }
+
+    override val contentDescriptionLabels: List<InteractiveWatchFaceClient.ContentDescriptionLabel>
+        get() = iInteractiveWatchFace.contentDescriptionLabels.map {
+            InteractiveWatchFaceClient.ContentDescriptionLabel(
+                it.text.toApiComplicationText(),
+                it.bounds,
+                it.tapAction
+            )
+        }
+
+    override fun setWatchUiState(
+        watchUiState: androidx.wear.watchface.client.WatchUiState
+    ) = TraceEvent(
+        "InteractiveWatchFaceClientImpl.setSystemState"
+    ).use {
+        iInteractiveWatchFace.setWatchUiState(
+            WatchUiState(
+                watchUiState.inAmbientMode,
+                watchUiState.interruptionFilter
+            )
+        )
+    }
+
+    override fun performAmbientTick() = TraceEvent(
+        "InteractiveWatchFaceClientImpl.performAmbientTick"
+    ).use {
+        iInteractiveWatchFace.ambientTickUpdate()
+    }
+
+    override fun addClientDisconnectListener(
+        listener: InteractiveWatchFaceClient.ClientDisconnectListener,
+        executor: Executor
+    ) {
+        synchronized(lock) {
+            require(!listeners.contains(listener)) {
+                "Don't call addClientDisconnectListener multiple times for the same listener"
+            }
+            listeners.put(listener, executor)
+        }
+    }
+
+    override fun removeClientDisconnectListener(
+        listener: InteractiveWatchFaceClient.ClientDisconnectListener
+    ) {
+        synchronized(lock) {
+            listeners.remove(listener)
+        }
+    }
+
+    override fun isConnectionAlive() = iInteractiveWatchFace.asBinder().isBinderAlive
+}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
deleted file mode 100644
index 334dcf4..0000000
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.client
-
-import android.app.PendingIntent
-import android.content.res.Resources
-import android.graphics.Bitmap
-import android.graphics.Rect
-import android.os.IBinder
-import android.support.wearable.complications.TimeDependentText
-import android.support.wearable.watchface.SharedMemoryImage
-import androidx.annotation.IntDef
-import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
-import androidx.wear.complications.data.ComplicationData
-import androidx.wear.utility.TraceEvent
-import androidx.wear.watchface.RenderParameters
-import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
-import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.style.UserStyle
-import java.util.Objects
-
-/**
- * The type of tap event passed to the watch face.
- * @hide
- */
-@IntDef(
-    InteractiveWatchFaceSysUiClient.TAP_TYPE_TOUCH,
-    InteractiveWatchFaceSysUiClient.TAP_TYPE_TOUCH_CANCEL,
-    InteractiveWatchFaceSysUiClient.TAP_TYPE_TAP
-)
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public annotation class TapType
-
-/**
- * Controls a stateful remote interactive watch face with an interface tailored for SysUI the
- * WearOS launcher app. Typically this will be used for the current active watch face.
- *
- * Note clients should call [close] when finished.
- */
-public interface InteractiveWatchFaceSysUiClient : AutoCloseable {
-
-    public companion object {
-        /** Indicates a "down" touch event on the watch face. */
-        public const val TAP_TYPE_TOUCH: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH
-
-        /**
-         * Indicates that a previous TAP_TYPE_TOUCH event has been canceled. This generally happens
-         * when the watch face is touched but then a move or long press occurs.
-         */
-        public const val TAP_TYPE_TOUCH_CANCEL: Int =
-            IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH_CANCEL
-
-        /**
-         * Indicates that an "up" event on the watch face has occurred that has not been consumed by
-         * another activity. A TAP_TYPE_TOUCH always occur first. This event will not occur if a
-         * TAP_TYPE_TOUCH_CANCEL is sent.
-         */
-        public const val TAP_TYPE_TAP: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TAP
-
-        /**
-         * Constructs an [InteractiveWatchFaceSysUiClient] from the [IBinder] returned by
-         * [asBinder].
-         */
-        @JvmStatic
-        public fun createFromBinder(binder: IBinder): InteractiveWatchFaceSysUiClient =
-            InteractiveWatchFaceSysUiClientImpl(binder)
-    }
-
-    /**
-     * Sends a tap event to the watch face for processing.
-     */
-    public fun sendTouchEvent(xPosition: Int, yPosition: Int, @TapType tapType: Int)
-
-    /**
-     * Describes regions of the watch face for use by a screen reader.
-     *
-     * @param text Text associated with the region, to be read by the screen reader.
-     * @param bounds [Rect] describing the area of the feature on screen.
-     * @param tapAction [PendingIntent] to be used if the screen reader's user triggers a tap
-     *     action.
-     */
-    public class ContentDescriptionLabel(
-        private val text: TimeDependentText,
-        public val bounds: Rect,
-        public val tapAction: PendingIntent?
-    ) {
-        /**
-         * Returns the text that should be displayed for the given timestamp.
-         *
-         * @param resources [Resources] from the current [android.content.Context]
-         * @param dateTimeMillis milliseconds since epoch, e.g. from [System.currentTimeMillis]
-         */
-        public fun getTextAt(resources: Resources, dateTimeMillis: Long): CharSequence =
-            text.getTextAt(resources, dateTimeMillis)
-
-        override fun equals(other: Any?): Boolean =
-            other is ContentDescriptionLabel &&
-                text == other.text &&
-                bounds == other.bounds &&
-                tapAction == other.tapAction
-
-        override fun hashCode(): Int {
-            return Objects.hash(
-                text,
-                bounds,
-                tapAction
-            )
-        }
-    }
-
-    /**
-     * Returns the [ContentDescriptionLabel]s describing the watch face, for the use by screen
-     * readers.
-     */
-    public val contentDescriptionLabels: List<ContentDescriptionLabel>
-
-    /**
-     * Requests a shared memory backed [Bitmap] containing a screenshot of the watch face with the
-     * given settings.
-     *
-     * @param renderParameters The [RenderParameters] to draw with.
-     * @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with.
-     * @param userStyle Optional [UserStyle] to render with, if null the current style is used.
-     * @param idAndComplicationData Map of complication ids to [ComplicationData] to render with, or
-     *     if null then the existing complication data if any is used.
-     * @return A shared memory backed [Bitmap] containing a screenshot of the watch face with the
-     *     given settings.
-     */
-    @RequiresApi(27)
-    public fun takeWatchFaceScreenshot(
-        renderParameters: RenderParameters,
-        calendarTimeMillis: Long,
-        userStyle: UserStyle?,
-        idAndComplicationData: Map<Int, ComplicationData>?
-    ): Bitmap
-
-    /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
-    public val previewReferenceTimeMillis: Long
-
-    /** Updates the watch faces [SystemState]. */
-    public fun setSystemState(systemState: SystemState)
-
-    /** Returns the ID of this watch face instance. */
-    public val instanceId: String
-
-    /** Triggers watch face rendering into the surface when in ambient mode. */
-    public fun performAmbientTick()
-
-    /** Returns the associated [IBinder]. Allows this interface to be passed over AIDL. */
-    public fun asBinder(): IBinder
-}
-
-internal class InteractiveWatchFaceSysUiClientImpl internal constructor(
-    private val iInteractiveWatchFaceSysUI: IInteractiveWatchFaceSysUI
-) : InteractiveWatchFaceSysUiClient {
-
-    constructor(binder: IBinder) : this(IInteractiveWatchFaceSysUI.Stub.asInterface(binder))
-
-    override fun sendTouchEvent(
-        xPosition: Int,
-        yPosition: Int,
-        @TapType tapType: Int
-    ) = TraceEvent("InteractiveWatchFaceSysUiClientImpl.sendTouchEvent").use {
-        iInteractiveWatchFaceSysUI.sendTouchEvent(xPosition, yPosition, tapType)
-    }
-
-    override val contentDescriptionLabels:
-        List<InteractiveWatchFaceSysUiClient.ContentDescriptionLabel>
-            get() = iInteractiveWatchFaceSysUI.contentDescriptionLabels.map {
-                InteractiveWatchFaceSysUiClient.ContentDescriptionLabel(
-                    it.text,
-                    it.bounds,
-                    it.tapAction
-                )
-            }
-
-    @RequiresApi(27)
-    override fun takeWatchFaceScreenshot(
-        renderParameters: RenderParameters,
-        calendarTimeMillis: Long,
-        userStyle: UserStyle?,
-        idAndComplicationData: Map<Int, ComplicationData>?
-    ): Bitmap = TraceEvent("InteractiveWatchFaceSysUiClientImpl.takeWatchFaceScreenshot").use {
-        SharedMemoryImage.ashmemReadImageBundle(
-            iInteractiveWatchFaceSysUI.takeWatchFaceScreenshot(
-                WatchfaceScreenshotParams(
-                    renderParameters.toWireFormat(),
-                    calendarTimeMillis,
-                    userStyle?.toWireFormat(),
-                    idAndComplicationData?.map {
-                        IdAndComplicationDataWireFormat(
-                            it.key,
-                            it.value.asWireComplicationData()
-                        )
-                    }
-                )
-            )
-        )
-    }
-
-    override val previewReferenceTimeMillis: Long
-        get() = iInteractiveWatchFaceSysUI.previewReferenceTimeMillis
-
-    override fun setSystemState(systemState: SystemState) = TraceEvent(
-        "InteractiveWatchFaceSysUiClientImpl.setSystemState"
-    ).use {
-        iInteractiveWatchFaceSysUI.setSystemState(
-            androidx.wear.watchface.data.SystemState(
-                systemState.inAmbientMode,
-                systemState.interruptionFilter
-            )
-        )
-    }
-
-    override val instanceId: String
-        get() = iInteractiveWatchFaceSysUI.instanceId
-
-    override fun performAmbientTick() = TraceEvent(
-        "InteractiveWatchFaceSysUiClientImpl.performAmbientTick"
-    ).use {
-        iInteractiveWatchFaceSysUI.ambientTickUpdate()
-    }
-
-    override fun close() = TraceEvent("InteractiveWatchFaceSysUiClientImpl.close").use {
-        iInteractiveWatchFaceSysUI.release()
-    }
-
-    override fun asBinder(): IBinder = iInteractiveWatchFaceSysUI.asBinder()
-}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
deleted file mode 100644
index e60a906..0000000
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.client
-
-import android.graphics.Bitmap
-import android.os.IBinder
-import android.support.wearable.watchface.SharedMemoryImage
-import androidx.annotation.Px
-import androidx.annotation.RequiresApi
-import androidx.wear.complications.data.ComplicationData
-import androidx.wear.utility.TraceEvent
-import androidx.wear.watchface.RenderParameters
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
-import androidx.wear.watchface.data.ComplicationBoundsType
-import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleSchema
-import androidx.wear.watchface.style.data.UserStyleWireFormat
-
-/**
- * Controls a stateful remote interactive watch face with an interface tailored for WCS the
- * WearOS system server responsible for watch face management. Typically this will be used for
- * the current active watch face.
- *
- * Note clients should call [close] when finished.
- */
-public interface InteractiveWatchFaceWcsClient : AutoCloseable {
-
-    public companion object {
-        /**
-         * Constructs an [InteractiveWatchFaceWcsClient] from the [IBinder] returned by [asBinder].
-         */
-        @JvmStatic
-        public fun createFromBinder(binder: IBinder): InteractiveWatchFaceWcsClient =
-            InteractiveWatchFaceWcsClientImpl(binder)
-    }
-
-    /**
-     * Sends new ComplicationData to the watch face. Note this doesn't have to be a full update,
-     * it's possible to update just one complication at a time, but doing so may result in a less
-     * visually clean transition.
-     */
-    public fun updateComplicationData(idToComplicationData: Map<Int, ComplicationData>)
-
-    /**
-     * Requests a shared memory backed [Bitmap] containing a screenshot of the watch face with the
-     * given settings.
-     *
-     * @param renderParameters The [RenderParameters] to draw with.
-     * @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with.
-     * @param userStyle Optional [UserStyle] to render with, if null the current style is used.
-     * @param idAndComplicationData Map of complication ids to [ComplicationData] to render with, or
-     *     if null then the existing complication data if any is used.
-     * @return A shared memory backed [Bitmap] containing a screenshot of the watch  face with the
-     *     given settings.
-     */
-    @RequiresApi(27)
-    public fun takeWatchFaceScreenshot(
-        renderParameters: RenderParameters,
-        calendarTimeMillis: Long,
-        userStyle: UserStyle?,
-        idAndComplicationData: Map<Int, ComplicationData>?
-    ): Bitmap
-
-    /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
-    public val previewReferenceTimeMillis: Long
-
-    /**
-     * Renames this instance to [newInstanceId] (must be unique, usually this would be different
-     * from the old ID but that's not a requirement). Sets the current [UserStyle] and clears
-     * any complication data. Setting the new UserStyle may have a side effect of enabling or
-     * disabling complications, which will be visible via [ComplicationState.isEnabled].
-     */
-    public fun updateInstance(newInstanceId: String, userStyle: UserStyle)
-
-    /**
-     * Renames this instance to [newInstanceId] (must be unique, usually this would be different
-     * from the old ID but that's not a requirement). Sets the current [UserStyle] represented as a
-     * Map<String, String> and clears any complication data. Setting the new UserStyle may have
-     * a side effect of enabling or disabling complications, which will be visible via
-     * [ComplicationState.isEnabled].
-     */
-    public fun updateInstance(newInstanceId: String, userStyle: Map<String, String>)
-
-    /** Returns the ID of this watch face instance. */
-    public val instanceId: String
-
-    /** The watch face's [UserStyleSchema]. */
-    public val userStyleSchema: UserStyleSchema
-
-    /**
-     * Map of complication ids to [ComplicationState] for each complication slot. Note
-     * this can change, typically in response to styling.
-     */
-    public val complicationState: Map<Int, ComplicationState>
-
-    /** Returns the associated [IBinder]. Allows this interface to be passed over AIDL. */
-    public fun asBinder(): IBinder
-
-    /** Returns the ID of the complication at the given coordinates or `null` if there isn't one.*/
-    @SuppressWarnings("AutoBoxing")
-    public fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? =
-        complicationState.asSequence().firstOrNull {
-            it.value.isEnabled && when (it.value.boundsType) {
-                ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
-                ComplicationBoundsType.BACKGROUND -> false
-                ComplicationBoundsType.EDGE -> false
-                else -> false
-            }
-        }?.key
-
-    /**
-     * Requests the specified complication is highlighted for a short period to bring attention to
-     * it.
-     */
-    public fun bringAttentionToComplication(complicationId: Int)
-}
-
-/** Controls a stateful remote interactive watch face with an interface tailored for WCS. */
-internal class InteractiveWatchFaceWcsClientImpl internal constructor(
-    private val iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
-) : InteractiveWatchFaceWcsClient {
-
-    constructor(binder: IBinder) : this(IInteractiveWatchFaceWCS.Stub.asInterface(binder))
-
-    override fun updateComplicationData(
-        idToComplicationData: Map<Int, ComplicationData>
-    ) = TraceEvent("InteractiveWatchFaceWcsClientImpl.updateComplicationData").use {
-        iInteractiveWatchFaceWcs.updateComplicationData(
-            idToComplicationData.map {
-                IdAndComplicationDataWireFormat(it.key, it.value.asWireComplicationData())
-            }
-        )
-    }
-
-    @RequiresApi(27)
-    override fun takeWatchFaceScreenshot(
-        renderParameters: RenderParameters,
-        calendarTimeMillis: Long,
-        userStyle: UserStyle?,
-        idAndComplicationData: Map<Int, ComplicationData>?
-    ): Bitmap = TraceEvent("InteractiveWatchFaceWcsClientImpl.takeWatchFaceScreenshot").use {
-        SharedMemoryImage.ashmemReadImageBundle(
-            iInteractiveWatchFaceWcs.takeWatchFaceScreenshot(
-                WatchfaceScreenshotParams(
-                    renderParameters.toWireFormat(),
-                    calendarTimeMillis,
-                    userStyle?.toWireFormat(),
-                    idAndComplicationData?.map {
-                        IdAndComplicationDataWireFormat(
-                            it.key,
-                            it.value.asWireComplicationData()
-                        )
-                    }
-                )
-            )
-        )
-    }
-
-    override val previewReferenceTimeMillis: Long
-        get() = iInteractiveWatchFaceWcs.previewReferenceTimeMillis
-
-    override fun updateInstance(newInstanceId: String, userStyle: UserStyle) = TraceEvent(
-        "InteractiveWatchFaceWcsClientImpl.updateInstance"
-    ).use {
-        iInteractiveWatchFaceWcs.updateInstance(newInstanceId, userStyle.toWireFormat())
-    }
-
-    override fun updateInstance(newInstanceId: String, userStyle: Map<String, String>) = TraceEvent(
-        "InteractiveWatchFaceWcsClientImpl.updateInstance"
-    ).use {
-        iInteractiveWatchFaceWcs.updateInstance(newInstanceId, UserStyleWireFormat(userStyle))
-    }
-
-    override val instanceId: String
-        get() = iInteractiveWatchFaceWcs.instanceId
-
-    override val userStyleSchema: UserStyleSchema
-        get() = UserStyleSchema(iInteractiveWatchFaceWcs.userStyleSchema)
-
-    override val complicationState: Map<Int, ComplicationState>
-        get() = iInteractiveWatchFaceWcs.complicationDetails.associateBy(
-            { it.id },
-            { ComplicationState(it.complicationState) }
-        )
-
-    override fun close() = TraceEvent("InteractiveWatchFaceWcsClientImpl.close").use {
-        iInteractiveWatchFaceWcs.release()
-    }
-
-    override fun asBinder(): IBinder = iInteractiveWatchFaceWcs.asBinder()
-
-    override fun bringAttentionToComplication(complicationId: Int) = TraceEvent(
-        "InteractiveWatchFaceWcsClientImpl.bringAttentionToComplication"
-    ).use {
-        iInteractiveWatchFaceWcs.bringAttentionToComplication(complicationId)
-    }
-}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
index c06d5ac..652bfb12 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
@@ -22,20 +22,25 @@
 import android.content.Intent
 import android.content.ServiceConnection
 import android.os.IBinder
+import androidx.annotation.Px
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.utility.AsyncTraceEvent
 import androidx.wear.utility.TraceEvent
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace
 import androidx.wear.watchface.control.IWatchFaceControlService
 import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
+import androidx.wear.watchface.data.WatchUiState
 import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleData
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
 
 /**
  * Connects to a watch face's WatchFaceControlService which allows the user to control the watch
@@ -103,22 +108,22 @@
     public class ServiceStartFailureException : Exception()
 
     /**
-     * Returns the [InteractiveWatchFaceSysUiClient] for the given instance id, or null if no such
+     * Returns the [InteractiveWatchFaceClient] for the given instance id, or null if no such
      * instance exists.
      *
-     * When finished call [InteractiveWatchFaceSysUiClient.close] to release resources.
+     * When finished call [InteractiveWatchFaceClient.close] to release resources.
      *
      * @param instanceId The name of the interactive watch face instance to retrieve
-     * @return The [InteractiveWatchFaceSysUiClient] or `null` if [instanceId] is unrecognized,
+     * @return The [InteractiveWatchFaceClient] or `null` if [instanceId] is unrecognized,
      *    or [ServiceNotBoundException] if the WatchFaceControlService is not bound.
      */
-    public fun getInteractiveWatchFaceSysUiClientInstance(
+    public fun getInteractiveWatchFaceClientInstance(
         instanceId: String
-    ): InteractiveWatchFaceSysUiClient?
+    ): InteractiveWatchFaceClient?
 
     /**
      * Creates a [HeadlessWatchFaceClient] with the specified [DeviceConfig]. Screenshots made with
-     * [HeadlessWatchFaceClient.takeWatchFaceScreenshot] will be `surfaceWidth` x `surfaceHeight` in
+     * [HeadlessWatchFaceClient.renderWatchFaceToBitmap] will be `surfaceWidth` x `surfaceHeight` in
      * size.
      *
      * When finished call [HeadlessWatchFaceClient.close] to release resources.
@@ -134,33 +139,33 @@
     public fun createHeadlessWatchFaceClient(
         watchFaceName: ComponentName,
         deviceConfig: DeviceConfig,
-        surfaceWidth: Int,
-        surfaceHeight: Int
+        @Px surfaceWidth: Int,
+        @Px surfaceHeight: Int
     ): HeadlessWatchFaceClient?
 
     /**
-     * Requests either an existing [InteractiveWatchFaceWcsClient] with the specified [id] or
-     * schedules creation of an [InteractiveWatchFaceWcsClient] for the next time the
+     * Requests either an existing [InteractiveWatchFaceClient] with the specified [id] or
+     * schedules creation of an [InteractiveWatchFaceClient] for the next time the
      * WallpaperService creates an engine.
      *
-     * NOTE that currently only one [InteractiveWatchFaceWcsClient] per process can exist at a time.
+     * NOTE that currently only one [InteractiveWatchFaceClient] per process can exist at a time.
      *
-     * @param id The ID for the requested [InteractiveWatchFaceWcsClient].
+     * @param id The ID for the requested [InteractiveWatchFaceClient].
      * @param deviceConfig The [DeviceConfig] for the wearable.
-     * @param systemState The initial [SystemState] for the wearable.
-     * @param userStyle The initial style map (see [UserStyle]), or null if the default should be
-     *     used.
+     * @param watchUiState The initial [WatchUiState] for the wearable.
+     * @param userStyle The initial style map encoded as [UserStyleData] (see [UserStyle]),
+     *     or null if the default should be used.
      * @param idToComplicationData The initial complication data, or null if unavailable.
-     * @return A [Deferred] [InteractiveWatchFaceWcsClient], or a [ServiceStartFailureException] if
-     *    the watchface dies during startup.
+     * @return The [InteractiveWatchFaceClient], this should be closed when finished.
+     * @throws [ServiceStartFailureException] if the watchface dies during startup.
      */
-    public fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    public suspend fun getOrCreateInteractiveWatchFaceClient(
         id: String,
         deviceConfig: DeviceConfig,
-        systemState: SystemState,
-        userStyle: Map<String, String>?,
+        watchUiState: androidx.wear.watchface.client.WatchUiState,
+        userStyle: UserStyleData?,
         idToComplicationData: Map<Int, ComplicationData>?
-    ): Deferred<InteractiveWatchFaceWcsClient>
+    ): InteractiveWatchFaceClient
 
     public fun getEditorServiceClient(): EditorServiceClient
 }
@@ -172,10 +177,10 @@
 ) : WatchFaceControlClient {
     private var closed = false
 
-    override fun getInteractiveWatchFaceSysUiClientInstance(
+    override fun getInteractiveWatchFaceClientInstance(
         instanceId: String
-    ) = service.getInteractiveWatchFaceInstanceSysUI(instanceId)?.let {
-        InteractiveWatchFaceSysUiClientImpl(it)
+    ) = service.getInteractiveWatchFaceInstance(instanceId)?.let {
+        InteractiveWatchFaceClientImpl(it)
     }
 
     override fun createHeadlessWatchFaceClient(
@@ -204,74 +209,71 @@
         }
     }
 
-    override fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+    override suspend fun getOrCreateInteractiveWatchFaceClient(
         id: String,
         deviceConfig: DeviceConfig,
-        systemState: SystemState,
-        userStyle: Map<String, String>?,
+        watchUiState: androidx.wear.watchface.client.WatchUiState,
+        userStyle: UserStyleData?,
         idToComplicationData: Map<Int, ComplicationData>?
-    ): Deferred<InteractiveWatchFaceWcsClient> {
+    ): InteractiveWatchFaceClient {
         requireNotClosed()
         val traceEvent = AsyncTraceEvent(
             "WatchFaceControlClientImpl" +
-                ".getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync"
+                ".getOrCreateWallpaperServiceBackedInteractiveWatchFaceClientAsync"
         )
-        val deferredClient = CompletableDeferred<InteractiveWatchFaceWcsClient>()
-
-        // [IWatchFaceControlService.getOrCreateInteractiveWatchFaceWCS] has an asynchronous
-        // callback and it's possible the watch face might crash during start up so we register
-        // a death observer.
-        val deathObserver = IBinder.DeathRecipient {
-            deferredClient.completeExceptionally(
-                WatchFaceControlClient.ServiceStartFailureException()
-            )
-        }
-        val serviceBinder = service.asBinder()
-        serviceBinder.linkToDeath(deathObserver, 0)
-
-        service.getOrCreateInteractiveWatchFaceWCS(
-            WallpaperInteractiveWatchFaceInstanceParams(
-                id,
-                androidx.wear.watchface.data.DeviceConfig(
-                    deviceConfig.hasLowBitAmbient,
-                    deviceConfig.hasBurnInProtection,
-                    deviceConfig.analogPreviewReferenceTimeMillis,
-                    deviceConfig.digitalPreviewReferenceTimeMillis
-                ),
-                androidx.wear.watchface.data.SystemState(
-                    systemState.inAmbientMode,
-                    systemState.interruptionFilter
-                ),
-                UserStyleWireFormat(userStyle ?: emptyMap()),
-                idToComplicationData?.map {
-                    IdAndComplicationDataWireFormat(
-                        it.key,
-                        it.value.asWireComplicationData()
-                    )
-                }
-            ),
-            object : IPendingInteractiveWatchFaceWCS.Stub() {
-                override fun getApiVersion() = IPendingInteractiveWatchFaceWCS.API_VERSION
-
-                override fun onInteractiveWatchFaceWcsCreated(
-                    iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
-                ) {
-                    serviceBinder.unlinkToDeath(deathObserver, 0)
-                    traceEvent.close()
-                    deferredClient.complete(
-                        InteractiveWatchFaceWcsClientImpl(iInteractiveWatchFaceWcs)
-                    )
-                }
+        return suspendCoroutine { continuation ->
+            // [IWatchFaceControlService.getOrCreateInteractiveWatchFaceWCS] has an asynchronous
+            // callback and it's possible the watch face might crash during start up so we register
+            // a death observer.
+            val deathObserver = IBinder.DeathRecipient {
+                continuation.resumeWithException(
+                    WatchFaceControlClient.ServiceStartFailureException()
+                )
             }
-        )?.let {
-            // There was an existing watchface.onInteractiveWatchFaceWcsCreated
-            serviceBinder.unlinkToDeath(deathObserver, 0)
-            traceEvent.close()
-            deferredClient.complete(InteractiveWatchFaceWcsClientImpl(it))
-        }
+            val serviceBinder = service.asBinder()
+            serviceBinder.linkToDeath(deathObserver, 0)
 
-        // Wait for [watchFaceCreatedCallback] or [deathObserver] to fire.
-        return deferredClient
+            service.getOrCreateInteractiveWatchFace(
+                WallpaperInteractiveWatchFaceInstanceParams(
+                    id,
+                    androidx.wear.watchface.data.DeviceConfig(
+                        deviceConfig.hasLowBitAmbient,
+                        deviceConfig.hasBurnInProtection,
+                        deviceConfig.analogPreviewReferenceTimeMillis,
+                        deviceConfig.digitalPreviewReferenceTimeMillis
+                    ),
+                    WatchUiState(
+                        watchUiState.inAmbientMode,
+                        watchUiState.interruptionFilter
+                    ),
+                    userStyle?.toWireFormat() ?: UserStyleWireFormat(emptyMap()),
+                    idToComplicationData?.map {
+                        IdAndComplicationDataWireFormat(
+                            it.key,
+                            it.value.asWireComplicationData()
+                        )
+                    }
+                ),
+                object : IPendingInteractiveWatchFace.Stub() {
+                    override fun getApiVersion() = IPendingInteractiveWatchFace.API_VERSION
+
+                    override fun onInteractiveWatchFaceCreated(
+                        iInteractiveWatchFace: IInteractiveWatchFace
+                    ) {
+                        serviceBinder.unlinkToDeath(deathObserver, 0)
+                        traceEvent.close()
+                        continuation.resume(
+                            InteractiveWatchFaceClientImpl(iInteractiveWatchFace)
+                        )
+                    }
+                }
+            )?.let {
+                // There was an existing watchface.onInteractiveWatchFaceCreated
+                serviceBinder.unlinkToDeath(deathObserver, 0)
+                traceEvent.close()
+                continuation.resume(InteractiveWatchFaceClientImpl(it))
+            }
+        }
     }
 
     override fun getEditorServiceClient(): EditorServiceClient = TraceEvent(
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/SystemState.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchUiState.kt
similarity index 70%
rename from wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/SystemState.kt
rename to wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchUiState.kt
index 748c9a6..e9da6de 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/SystemState.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchUiState.kt
@@ -35,13 +35,19 @@
 public annotation class InterruptionFilter
 
 /**
- * Describes the system state of the watch face.
+ * Describes the system state of the watch face ui.
  *
  * @param inAmbientMode Whether the device is is ambient mode or not.
- * @param interruptionFilter The current user interruption settings. @see [NotificationManager]
- *     for details.
+ * @param interruptionFilter The interruption filter defines which notifications are allowed to
+ *     interrupt the user. For watch faces this value is one of:
+ *     [NotificationManager.INTERRUPTION_FILTER_ALARMS],
+ *     [NotificationManager.INTERRUPTION_FILTER_ALL],
+ *     [NotificationManager.INTERRUPTION_FILTER_NONE],
+ *     [NotificationManager.INTERRUPTION_FILTER_PRIORITY],
+ *     [NotificationManager.INTERRUPTION_FILTER_UNKNOWN]. @see [NotificationManager] for more
+ *     details.
  */
-public class SystemState(
+public class WatchUiState(
     @get:JvmName("inAmbientMode")
     public val inAmbientMode: Boolean,
 
diff --git a/wear/wear-watchface-complications-rendering/build.gradle b/wear/wear-watchface-complications-rendering/build.gradle
index 3cbc76b..7307faf 100644
--- a/wear/wear-watchface-complications-rendering/build.gradle
+++ b/wear/wear-watchface-complications-rendering/build.gradle
@@ -64,7 +64,7 @@
     name = "Android Wear Watchface Complication Rendering"
     publish = Publish.SNAPSHOT_AND_RELEASE
     mavenGroup = LibraryGroups.WEAR
-    mavenVersion = LibraryVersions.WEAR_WATCHFACE
+    mavenVersion = LibraryVersions.WEAR_WATCHFACE_COMPLICATIONS_RENDERING
     inceptionYear = "2020"
     description = "Support for rendering complications on the watch face"
 }
diff --git a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
index 3519721..1492085 100644
--- a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
+++ b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
@@ -56,7 +56,7 @@
 import androidx.wear.watchface.WatchFaceService;
 import androidx.wear.watchface.WatchFaceType;
 import androidx.wear.watchface.WatchState;
-import androidx.wear.watchface.style.UserStyleRepository;
+import androidx.wear.watchface.style.CurrentUserStyleRepository;
 import androidx.wear.watchface.style.UserStyleSchema;
 
 import org.jetbrains.annotations.Nullable;
@@ -720,14 +720,14 @@
         protected Object createWatchFace(@NonNull SurfaceHolder surfaceHolder,
                 @NonNull WatchState watchState,
                 @NonNull Continuation<? super WatchFace> completion) {
-            UserStyleRepository userStyleRepository =
-                    new UserStyleRepository(new UserStyleSchema(new ArrayList<>()));
+            CurrentUserStyleRepository currentUserStyleRepository =
+                    new CurrentUserStyleRepository(new UserStyleSchema(new ArrayList<>()));
             return new WatchFace(
                     WatchFaceType.ANALOG,
-                    userStyleRepository,
+                    currentUserStyleRepository,
                     new Renderer.CanvasRenderer(
-                            surfaceHolder, userStyleRepository, watchState, CanvasType.SOFTWARE,
-                            16L) {
+                            surfaceHolder, currentUserStyleRepository, watchState,
+                            CanvasType.SOFTWARE, 16L) {
                         @Override
                         public void render(@NonNull Canvas canvas, @NonNull Rect bounds,
                                 @NonNull Calendar calendar) {
diff --git a/wear/wear-watchface-data/api/restricted_current.txt b/wear/wear-watchface-data/api/restricted_current.txt
index 3415e93..cba8655 100644
--- a/wear/wear-watchface-data/api/restricted_current.txt
+++ b/wear/wear-watchface-data/api/restricted_current.txt
@@ -123,8 +123,8 @@
 
 package androidx.wear.watchface.control.data {
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class ComplicationScreenshotParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public ComplicationScreenshotParams(int, androidx.wear.watchface.data.RenderParametersWireFormat, long, android.support.wearable.complications.ComplicationData?, androidx.wear.watchface.style.data.UserStyleWireFormat?);
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class ComplicationRenderParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
+    ctor public ComplicationRenderParams(int, androidx.wear.watchface.data.RenderParametersWireFormat, long, android.support.wearable.complications.ComplicationData?, androidx.wear.watchface.style.data.UserStyleWireFormat?);
     method public int describeContents();
     method public long getCalendarTimeMillis();
     method public android.support.wearable.complications.ComplicationData? getComplicationData();
@@ -132,7 +132,7 @@
     method public androidx.wear.watchface.data.RenderParametersWireFormat getRenderParametersWireFormat();
     method public androidx.wear.watchface.style.data.UserStyleWireFormat? getUserStyle();
     method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.ComplicationScreenshotParams!>! CREATOR;
+    field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.ComplicationRenderParams!>! CREATOR;
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class HeadlessWatchFaceInstanceParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
@@ -147,28 +147,28 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public class WallpaperInteractiveWatchFaceInstanceParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public WallpaperInteractiveWatchFaceInstanceParams(String, androidx.wear.watchface.data.DeviceConfig, androidx.wear.watchface.data.SystemState, androidx.wear.watchface.style.data.UserStyleWireFormat, java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
+    ctor public WallpaperInteractiveWatchFaceInstanceParams(String, androidx.wear.watchface.data.DeviceConfig, androidx.wear.watchface.data.WatchUiState, androidx.wear.watchface.style.data.UserStyleWireFormat, java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
     method public int describeContents();
     method public androidx.wear.watchface.data.DeviceConfig getDeviceConfig();
     method public java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>? getIdAndComplicationDataWireFormats();
     method public String getInstanceId();
-    method public androidx.wear.watchface.data.SystemState getSystemState();
     method public androidx.wear.watchface.style.data.UserStyleWireFormat getUserStyle();
+    method public androidx.wear.watchface.data.WatchUiState getWatchUiState();
     method public void setIdAndComplicationDataWireFormats(java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
     method public void setUserStyle(androidx.wear.watchface.style.data.UserStyleWireFormat);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams!>! CREATOR;
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class WatchfaceScreenshotParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public WatchfaceScreenshotParams(androidx.wear.watchface.data.RenderParametersWireFormat, long, androidx.wear.watchface.style.data.UserStyleWireFormat?, java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class WatchFaceRenderParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
+    ctor public WatchFaceRenderParams(androidx.wear.watchface.data.RenderParametersWireFormat, long, androidx.wear.watchface.style.data.UserStyleWireFormat?, java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
     method public int describeContents();
     method public long getCalendarTimeMillis();
     method public java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>? getIdAndComplicationDatumWireFormats();
     method public androidx.wear.watchface.data.RenderParametersWireFormat getRenderParametersWireFormat();
     method public androidx.wear.watchface.style.data.UserStyleWireFormat? getUserStyle();
     method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.WatchfaceScreenshotParams!>! CREATOR;
+    field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.WatchFaceRenderParams!>! CREATOR;
   }
 
 }
@@ -255,13 +255,13 @@
     field public static final android.os.Parcelable.Creator<androidx.wear.watchface.data.RenderParametersWireFormat!>! CREATOR;
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public final class SystemState implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public SystemState(boolean, int);
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public final class WatchUiState implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
+    ctor public WatchUiState(boolean, int);
     method public int describeContents();
     method public boolean getInAmbientMode();
     method public int getInterruptionFilter();
     method public void writeToParcel(android.os.Parcel, int);
-    field public static final android.os.Parcelable.Creator<androidx.wear.watchface.data.SystemState!>! CREATOR;
+    field public static final android.os.Parcelable.Creator<androidx.wear.watchface.data.WatchUiState!>! CREATOR;
   }
 
 }
@@ -284,8 +284,7 @@
 package androidx.wear.watchface.style.data {
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class BooleanOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
-    ctor public BooleanOptionWireFormat(String, boolean);
-    field @androidx.versionedparcelable.ParcelField(2) public boolean mValue;
+    ctor public BooleanOptionWireFormat(byte[]);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class BooleanUserStyleSettingWireFormat extends androidx.wear.watchface.style.data.UserStyleSettingWireFormat {
@@ -306,7 +305,7 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class ComplicationsOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
-    ctor public ComplicationsOptionWireFormat(String, CharSequence, android.graphics.drawable.Icon?, androidx.wear.watchface.style.data.ComplicationOverlayWireFormat![]);
+    ctor public ComplicationsOptionWireFormat(byte[], CharSequence, android.graphics.drawable.Icon?, androidx.wear.watchface.style.data.ComplicationOverlayWireFormat![]);
     field @androidx.versionedparcelable.ParcelField(100) public androidx.wear.watchface.style.data.ComplicationOverlayWireFormat![] mComplicationOverlays;
     field @androidx.versionedparcelable.ParcelField(2) public CharSequence mDisplayName;
     field @androidx.versionedparcelable.ParcelField(3) public android.graphics.drawable.Icon? mIcon;
@@ -317,7 +316,7 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class CustomValueOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
-    ctor public CustomValueOptionWireFormat(String);
+    ctor public CustomValueOptionWireFormat(byte[]);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class CustomValueUserStyleSettingWireFormat extends androidx.wear.watchface.style.data.UserStyleSettingWireFormat {
@@ -325,8 +324,7 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class DoubleRangeOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
-    ctor public DoubleRangeOptionWireFormat(String, double);
-    field @androidx.versionedparcelable.ParcelField(2) public double mValue;
+    ctor public DoubleRangeOptionWireFormat(byte[]);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class DoubleRangeUserStyleSettingWireFormat extends androidx.wear.watchface.style.data.UserStyleSettingWireFormat {
@@ -334,7 +332,7 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class ListOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
-    ctor public ListOptionWireFormat(String, CharSequence, android.graphics.drawable.Icon?);
+    ctor public ListOptionWireFormat(byte[], CharSequence, android.graphics.drawable.Icon?);
     field @androidx.versionedparcelable.ParcelField(2) public CharSequence mDisplayName;
     field @androidx.versionedparcelable.ParcelField(3) public android.graphics.drawable.Icon? mIcon;
   }
@@ -344,8 +342,7 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class LongRangeOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
-    ctor public LongRangeOptionWireFormat(String, long);
-    field @androidx.versionedparcelable.ParcelField(2) public long mValue;
+    ctor public LongRangeOptionWireFormat(byte[]);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class LongRangeUserStyleSettingWireFormat extends androidx.wear.watchface.style.data.UserStyleSettingWireFormat {
@@ -353,11 +350,11 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class OptionWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public OptionWireFormat(String);
+    ctor public OptionWireFormat(byte[]);
     method public int describeContents();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<androidx.wear.watchface.style.data.OptionWireFormat!>! CREATOR;
-    field @androidx.versionedparcelable.ParcelField(1) public String mId;
+    field @androidx.versionedparcelable.ParcelField(1) public byte[] mId;
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class UserStyleSchemaWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
@@ -383,11 +380,11 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public class UserStyleWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
-    ctor public UserStyleWireFormat(java.util.Map<java.lang.String!,java.lang.String!>);
+    ctor public UserStyleWireFormat(java.util.Map<java.lang.String!,byte[]!>);
     method public int describeContents();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<androidx.wear.watchface.style.data.UserStyleWireFormat!>! CREATOR;
-    field @androidx.versionedparcelable.ParcelField(1) public java.util.Map<java.lang.String!,java.lang.String!> mUserStyle;
+    field @androidx.versionedparcelable.ParcelField(1) public java.util.Map<java.lang.String!,byte[]!> mUserStyle;
   }
 
 }
diff --git a/wear/wear-watchface-data/src/main/aidl/android/support/wearable/watchface/IWatchFaceService.aidl b/wear/wear-watchface-data/src/main/aidl/android/support/wearable/watchface/IWatchFaceService.aidl
index d0ca805..eacc2ff 100644
--- a/wear/wear-watchface-data/src/main/aidl/android/support/wearable/watchface/IWatchFaceService.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/android/support/wearable/watchface/IWatchFaceService.aidl
@@ -31,12 +31,12 @@
 interface IWatchFaceService {
     // IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
     // in the future to remain binary backwards compatible.
-    // Next Id: 8
+    // Next Id: 9
 
     /**
      * API version number. This should be incremented every time a new method is added.
      */
-    const int WATCHFACE_SERVICE_API_VERSION = 3;
+    const int WATCHFACE_SERVICE_API_VERSION = 4;
 
     /**
      * Requests that the style for the provided watch face be set to the given style.
@@ -97,4 +97,7 @@
      * @since API version 0.
      */
     int getApiVersion() = 7;
+
+    /** Reserved. Do not use. */
+    void reserved8() = 8;
 }
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IHeadlessWatchFace.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IHeadlessWatchFace.aidl
index 2fd8652..af5245c 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IHeadlessWatchFace.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IHeadlessWatchFace.aidl
@@ -16,8 +16,8 @@
 
 package androidx.wear.watchface.control;
 
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams;
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams;
+import androidx.wear.watchface.control.data.ComplicationRenderParams;
+import androidx.wear.watchface.control.data.WatchFaceRenderParams;
 import androidx.wear.watchface.data.IdAndComplicationStateWireFormat;
 import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat;
 
@@ -73,24 +73,24 @@
      * Watch Face.
      *
      * @since API version 1.
-     * @param params The {@link WatchfaceScreenshotParams} for this screenshot.
+     * @param params The {@link WatchFaceRenderParams} for this screenshot.
      * @return A bundle containing a compressed shared memory backed {@link Bitmap} of the watch
      *     face with the requested settings
      * TODO(alexclarke): Refactor to return a parcelable rather than a bundle.
      */
-    Bundle takeWatchFaceScreenshot(in WatchfaceScreenshotParams params) = 5;
+    Bundle renderWatchFaceToBitmap(in WatchFaceRenderParams params) = 5;
 
     /**
      * Request for a {@link Bundle} containing a WebP compressed shared memory backed {@link Bitmap}
      * (see {@link SharedMemoryImage#ashmemCompressedImageBundleToBitmap}).
      *
      * @since API version 1.
-     * @param params The {@link ComplicationScreenshotParams} for this screenshot.
+     * @param params The {@link ComplicationRenderParams} for this screenshot.
      * @return A bundle containing a compressed shared memory backed {@link Bitmap} of the
      *     complication with the requested settings
      * TODO(alexclarke): Refactor to return a parcelable rather than a bundle.
      */
-    Bundle takeComplicationScreenshot(in ComplicationScreenshotParams params) = 6;
+    Bundle renderComplicationToBitmap(in ComplicationRenderParams params) = 6;
 
     /**
      * Releases the watch face instance.  It is an error to issue any further commands on any AIDLs
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
similarity index 62%
rename from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
rename to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
index 001c086..ed3d3fb 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
@@ -16,29 +16,46 @@
 
 package androidx.wear.watchface.control;
 
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams;
-import androidx.wear.watchface.data.SystemState;
+import android.support.wearable.watchface.accessibility.ContentDescriptionLabel;
+import androidx.wear.watchface.control.data.WatchFaceRenderParams;
+import androidx.wear.watchface.data.WatchUiState;
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat;
 import androidx.wear.watchface.data.IdAndComplicationStateWireFormat;
 import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat;
 import androidx.wear.watchface.style.data.UserStyleWireFormat;
 
 /**
- * Interface for interacting with an interactive instance of a watch face from WCS. SysUI can also
- * control the same instance via {@link IInteractiveWatchFaceSysUI}.
+ * Interface for interacting with an interactive instance of a watch face.
  *
  * @hide
  */
-interface IInteractiveWatchFaceWCS {
+interface IInteractiveWatchFace {
     // IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
     // in the future to remain binary backwards compatible.
-    // Next Id: 13
+    // Next Id: 17
 
     /**
      * API version number. This should be incremented every time a new method is added.
      */
     const int API_VERSION = 1;
 
+    /** Indicates a "down" touch event on the watch face. */
+    const int TAP_TYPE_DOWN = 0;
+
+    /**
+     * Indicates that a previous {@link #TAP_TYPE_DOWN} event has been canceled. This generally
+     * happens when the watch face is touched but then a move or long press occurs.
+     */
+    const int TAP_TYPE_CANCEL = 1;
+
+    /**
+     * Indicates that an "up" event on the watch face has occurred that has not been consumed by
+     * another activity. A {@link #TAP_TYPE_DOWN} always occur first. This event will not occur if a
+     * {@link #TAP_TYPE_CANCEL} is sent.
+     *
+     */
+    const int TAP_TYPE_UP = 2;
+
     /**
      * Returns the version number for this API which the client can use to determine which methods
      * are available.
@@ -59,7 +76,8 @@
      *
      * @since API version 1.
      */
-    oneway void updateComplicationData(in List<IdAndComplicationDataWireFormat> complicationData) = 4;
+    oneway void updateComplicationData(
+            in List<IdAndComplicationDataWireFormat> complicationData) = 4;
 
     /**
      * Renames this instance to newInstanceId, sets the current user style
@@ -68,7 +86,8 @@
      *
      * @since API version 1.
      */
-    oneway void updateInstance(in String newInstanceId, in UserStyleWireFormat userStyle) = 5;
+    oneway void updateWatchfaceInstance(
+            in String newInstanceId, in UserStyleWireFormat userStyle) = 5;
 
     /**
      * Returns the reference preview time for this watch face in milliseconds since the epoch.
@@ -101,12 +120,12 @@
      * calendarTimeMillis.
      *
      * @since API version 1.
-     * @param params The {@link WatchfaceScreenshotParams} for this screenshot.
+     * @param params The {@link WatchFaceRenderParams} for this screenshot.
      * @return A bundle containing a compressed shared memory backed {@link Bitmap} of the watch
      *     face with the requested settings
      * TODO(alexclarke): Refactor to return a parcelable rather than a bundle.
      */
-    Bundle takeWatchFaceScreenshot(in WatchfaceScreenshotParams params) = 10;
+    Bundle renderWatchFaceToBitmap(in WatchFaceRenderParams params) = 10;
 
     /**
      * If there's no {@link IInteractiveWatchFaceSysUI} holding a reference then the
@@ -123,4 +142,37 @@
      * @since API version 1.
      */
     oneway void bringAttentionToComplication(in int complicationId) = 12;
+
+    /**
+     * Forwards a touch event for the WatchFace to process.
+     *
+     * @param xPos X Coordinate of the touch event
+     * @param yPos Y Coordinate of the touch event
+     * @param tapType One of {@link #TAP_TYPE_DOWN}, {@link #TAP_TYPE_CANCEL}, {@link #TAP_TYPE_UP}
+     * @since API version 1.
+     */
+    oneway void sendTouchEvent(in int xPos, in int yPos, in int tapType) = 13;
+
+    /**
+     * Called periodically when the watch is in ambient mode to update the watchface.
+     *
+     * @since API version 1.
+     */
+    oneway void ambientTickUpdate() = 14;
+
+    /**
+     * Sends the current {@link WatchUiState} to the Watch Face.
+     *
+     * @since API version 1.
+     */
+    oneway void setWatchUiState(in WatchUiState watchUiState) = 15;
+
+    /**
+     * Gets the labels to be read aloud by screen readers. The results will change depending on the
+     * current style and complications.  Note the labes include the central time piece in addition
+     * to any complications.
+     *
+     * @since API version 1.
+     */
+    ContentDescriptionLabel[] getContentDescriptionLabels() = 16;
 }
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceSysUI.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceSysUI.aidl
deleted file mode 100644
index dd86be4..0000000
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceSysUI.aidl
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.control;
-
-import android.support.wearable.watchface.accessibility.ContentDescriptionLabel;
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams;
-import androidx.wear.watchface.data.SystemState;
-import androidx.wear.watchface.style.data.UserStyleWireFormat;
-
-/**
- * Interface for interacting with an interactive instance of a watch face from SysUI. WCS can also
- * control the same instance via {@link IInteractiveWatchFaceWCS}.
- *
- * @hide
- */
-interface IInteractiveWatchFaceSysUI {
-    // IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
-    // in the future to remain binary backwards compatible.
-    // Next Id: 10
-
-    /**
-     * API version number. This should be incremented every time a new method is added.
-     */
-    const int API_VERSION = 1;
-
-    /** Indicates a "down" touch event on the watch face. */
-    const int TAP_TYPE_TOUCH = 0;
-
-    /**
-     * Indicates that a previous TAP_TYPE_TOUCH event has been canceled. This generally happens when
-     * the watch face is touched but then a move or long press occurs.
-     */
-    const int TAP_TYPE_TOUCH_CANCEL = 1;
-
-    /**
-     * Indicates that an "up" event on the watch face has occurred that has not been consumed by
-     * another activity. A TAP_TYPE_TOUCH always occur first. This event will not occur if a
-     * TAP_TYPE_TOUCH_CANCEL is sent.
-     *
-     */
-    const int TAP_TYPE_TAP = 2;
-
-    /**
-     * Returns the version number for this API which the client can use to determine which methods
-     * are available.
-     *
-     * @since API version 1.
-     */
-    int getApiVersion() = 1;
-
-    /**
-     * Returns the ID parameter set at creation.
-     *
-     * @since API version 1.
-     */
-    String getInstanceId() = 2;
-
-    /**
-     * Returns the reference preview time for this watch face in milliseconds since the epoch.
-     *
-     * @since API version 1.
-     */
-    long getPreviewReferenceTimeMillis() = 3;
-
-    /**
-     * Forwards a touch event for the WatchFace to process.
-     *
-     * @param xPos X Coordinate of the touch event
-     * @param yPos Y Coordinate of the touch event
-     * @param tapType One of {@link #TAP_TYPE_TOUCH}, {@link #TAP_TYPE_TOUCH_CANCEL},
-     *    {@link #TAP_TYPE_TAP}
-     * @since API version 1.
-     */
-    oneway void sendTouchEvent(in int xPos, in int yPos, in int tapType) = 4;
-
-    /**
-     * Called periodically when the watch is in ambient mode to update the watchface.
-     *
-     * @since API version 1.
-     */
-    oneway void ambientTickUpdate() = 5;
-
-    /**
-     * Sends the current system state to the Watch Face.
-     *
-     * @since API version 1.
-     */
-    oneway void setSystemState(in SystemState systemState) = 6;
-
-    /**
-     * Request for a {@link Bundle} containing a WebP compressed shared memory backed {@link Bitmap}
-     * (see {@link SharedMemoryImage#ashmemCompressedImageBundleToBitmap}) with a screenshot of the
-     * Watch Face with the specified DrawMode (see {@link androidx.wear.watchface.DrawMode}) and
-     * calendarTimeMillis.
-     *
-     * @since API version 1.
-     * @param params The {@link WatchfaceScreenshotParams} for this screenshot.
-     * @return A bundle containing a compressed shared memory backed {@link Bitmap} of the watch
-     *     face with the requested settings
-     * TODO(alexclarke): Refactor to return a parcelable rather than a bundle.
-     */
-    Bundle takeWatchFaceScreenshot(in WatchfaceScreenshotParams params) = 7;
-
-    /**
-     * Gets the labels to be read aloud by screen readers. The results will change depending on the
-     * current style and complications.  Note the labes include the central time piece in addition
-     * to any complications.
-     *
-     * @since API version 1.
-     */
-    ContentDescriptionLabel[] getContentDescriptionLabels() = 8;
-
-    /**
-     * If there's no {@link IInteractiveWatchFaceWCS} holding a reference then the instance is
-     * diposed of. It is an error to issue any further AIDL commands via this interface.
-     *
-     * @since API version 1.
-     */
-    oneway void release() = 9;
-}
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFaceWCS.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFace.aidl
similarity index 85%
rename from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFaceWCS.aidl
rename to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFace.aidl
index 42fbc22..005eac3 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFaceWCS.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFace.aidl
@@ -16,14 +16,14 @@
 
 package androidx.wear.watchface.control;
 
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS;
+import androidx.wear.watchface.control.IInteractiveWatchFace;
 
 /**
  * Callback issued when {@link IInteractiveWatchFaceWcs} has been created.
  *
  * @hide
  */
-interface IPendingInteractiveWatchFaceWCS {
+interface IPendingInteractiveWatchFace {
    // IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
    // in the future to remain binary backwards compatible.
    // Next Id: 8
@@ -42,6 +42,5 @@
    int getApiVersion() = 1;
 
    /** Called by the watchface when {@link IInteractiveWatchFaceWcs} has been created. */
-   oneway void onInteractiveWatchFaceWcsCreated(
-         in IInteractiveWatchFaceWCS iInteractiveWatchFaceWcs) = 2;
+   oneway void onInteractiveWatchFaceCreated(in IInteractiveWatchFace iInteractiveWatchFace) = 2;
 }
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IWatchFaceControlService.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IWatchFaceControlService.aidl
index 49d95f3..103b961 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IWatchFaceControlService.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IWatchFaceControlService.aidl
@@ -16,10 +16,9 @@
 
 package androidx.wear.watchface.control;
 
-import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI;
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS;
+import androidx.wear.watchface.control.IInteractiveWatchFace;
 import androidx.wear.watchface.control.IHeadlessWatchFace;
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS;
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace;
 import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams;
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams;
 import androidx.wear.watchface.editor.IEditorService;
@@ -48,11 +47,11 @@
     int getApiVersion() = 1;
 
     /**
-     * Gets the {@link IInteractiveWatchFaceSysUI} corresponding to the id of an existing watch
+     * Gets the {@link IInteractiveWatchFace} corresponding to the id of an existing watch
      * face instance, or null if there is no such instance. The id is set when the instance is
      * created, see {@link WallpaperInteractiveWatchFaceInstanceParams}.
      */
-    IInteractiveWatchFaceSysUI getInteractiveWatchFaceInstanceSysUI(in String id) = 2;
+    IInteractiveWatchFace getInteractiveWatchFaceInstance(in String id) = 2;
 
     /**
      * Creates a headless WatchFace instance for the specified watchFaceName and returns an {@link
@@ -70,7 +69,7 @@
             in HeadlessWatchFaceInstanceParams params) = 3;
 
     /**
-     * Either returns an existing IInteractiveWatchFaceWCS instance or othrwise schedules
+     * Either returns an existing IInteractiveWatchFace instance or othrwise schedules
      * creation of an IInteractiveWatchFace for the next time the wallpaper service connects and
      * calls WatchFaceService.onCreateEngine.
      *
@@ -78,13 +77,13 @@
      *      instance to be made when WatchFaceService.onCreateEngine is called. If an existing
      *      instance is returned this callback won't fire.
      * @param callback Callback fired when the wathface is created.
-     * @return The existing {@link IInteractiveWatchFaceWCS} or null in which the callback will fire
+     * @return The existing {@link IInteractiveWatchFace} or null in which the callback will fire
      *      the next time the wallpaper service connects and calls WatchFaceService.onCreateEngine.
      * @since API version 1.
      */
-    IInteractiveWatchFaceWCS getOrCreateInteractiveWatchFaceWCS(
+    IInteractiveWatchFace getOrCreateInteractiveWatchFace(
             in WallpaperInteractiveWatchFaceInstanceParams params,
-            in IPendingInteractiveWatchFaceWCS callback) = 4;
+            in IPendingInteractiveWatchFace callback) = 4;
 
     /**
      * Returns the {@link IEditorService}
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationRenderParams.aidl
similarity index 94%
rename from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl
rename to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationRenderParams.aidl
index decdabc..299880b 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationRenderParams.aidl
@@ -17,4 +17,4 @@
 package androidx.wear.watchface.control.data;
 
 /** @hide */
-parcelable WatchfaceScreenshotParams;
+parcelable ComplicationRenderParams;
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationScreenshotParams.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationScreenshotParams.aidl
deleted file mode 100644
index 3f8b02c..0000000
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationScreenshotParams.aidl
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.control.data;
-
-/** @hide */
-parcelable ComplicationScreenshotParams;
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchFaceRenderParams.aidl
similarity index 94%
copy from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl
copy to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchFaceRenderParams.aidl
index decdabc..0577ccf 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchFaceRenderParams.aidl
@@ -17,4 +17,4 @@
 package androidx.wear.watchface.control.data;
 
 /** @hide */
-parcelable WatchfaceScreenshotParams;
+parcelable WatchFaceRenderParams;
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/SystemState.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/WatchUiState.aidl
similarity index 96%
rename from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/SystemState.aidl
rename to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/WatchUiState.aidl
index e648e52..d7a8888 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/SystemState.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/WatchUiState.aidl
@@ -17,4 +17,4 @@
 package androidx.wear.watchface.data;
 
 /** @hide */
-parcelable SystemState;
+parcelable WatchUiState;
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationScreenshotParams.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationRenderParams.java
similarity index 86%
rename from wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationScreenshotParams.java
rename to wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationRenderParams.java
index a57438c..f3a9b0d 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationScreenshotParams.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationRenderParams.java
@@ -32,14 +32,14 @@
 import androidx.wear.watchface.style.data.UserStyleWireFormat;
 
 /**
- * Parameters for the various AIDL takeComplicationScreenshot commands.
+ * Parameters for the various AIDL renderComplicationToBitmap commands.
  *
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @VersionedParcelize
 @SuppressLint("BanParcelableUsage") // TODO(b/169214666): Remove Parcelable
-public class ComplicationScreenshotParams implements VersionedParcelable, Parcelable {
+public class ComplicationRenderParams implements VersionedParcelable, Parcelable {
 
     /** ID of the complication we want to take a screen short of. */
     @ParcelField(1)
@@ -72,9 +72,9 @@
     UserStyleWireFormat mUserStyle;
 
     /** Used by VersionedParcelable. */
-    ComplicationScreenshotParams() {}
+    ComplicationRenderParams() {}
 
-    public ComplicationScreenshotParams(
+    public ComplicationRenderParams(
             int complicationId,
             @NonNull RenderParametersWireFormat renderParametersWireFormats,
             long calendarTimeMillis,
@@ -122,17 +122,17 @@
         return 0;
     }
 
-    public static final Parcelable.Creator<ComplicationScreenshotParams> CREATOR =
-            new Parcelable.Creator<ComplicationScreenshotParams>() {
+    public static final Parcelable.Creator<ComplicationRenderParams> CREATOR =
+            new Parcelable.Creator<ComplicationRenderParams>() {
                 @Override
-                public ComplicationScreenshotParams createFromParcel(Parcel source) {
+                public ComplicationRenderParams createFromParcel(Parcel source) {
                     return ParcelUtils.fromParcelable(
                             source.readParcelable(getClass().getClassLoader()));
                 }
 
                 @Override
-                public ComplicationScreenshotParams[] newArray(int size) {
-                    return new ComplicationScreenshotParams[size];
+                public ComplicationRenderParams[] newArray(int size) {
+                    return new ComplicationRenderParams[size];
                 }
             };
 }
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
index 0f81640..a9a3729 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
@@ -30,7 +30,7 @@
 import androidx.wear.watchface.control.IWatchFaceControlService;
 import androidx.wear.watchface.data.DeviceConfig;
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat;
-import androidx.wear.watchface.data.SystemState;
+import androidx.wear.watchface.data.WatchUiState;
 import androidx.wear.watchface.style.data.UserStyleWireFormat;
 
 import java.util.List;
@@ -56,10 +56,10 @@
     @NonNull
     DeviceConfig mDeviceConfig;
 
-    /** The {@link SystemState} for the host wearable. */
+    /** The {@link WatchUiState} for the host wearable. */
     @ParcelField(3)
     @NonNull
-    SystemState mSystemState;
+    WatchUiState mWatchUiState;
 
     /** The initial {@link UserStyleWireFormat}. */
     @ParcelField(4)
@@ -78,12 +78,12 @@
     public WallpaperInteractiveWatchFaceInstanceParams(
             @NonNull String instanceId,
             @NonNull DeviceConfig deviceConfig,
-            @NonNull SystemState systemState,
+            @NonNull WatchUiState watchUiState,
             @NonNull UserStyleWireFormat userStyle,
             @Nullable List<IdAndComplicationDataWireFormat> idAndComplicationDataWireFormats) {
         mInstanceId = instanceId;
         mDeviceConfig = deviceConfig;
-        mSystemState = systemState;
+        mWatchUiState = watchUiState;
         mUserStyle = userStyle;
         mIdAndComplicationDataWireFormats = idAndComplicationDataWireFormats;
     }
@@ -99,8 +99,8 @@
     }
 
     @NonNull
-    public SystemState getSystemState() {
-        return mSystemState;
+    public WatchUiState getWatchUiState() {
+        return mWatchUiState;
     }
 
     @NonNull
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchFaceRenderParams.java
similarity index 87%
rename from wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.java
rename to wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchFaceRenderParams.java
index dc5da8f..291fd02 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchFaceRenderParams.java
@@ -41,7 +41,7 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @VersionedParcelize
 @SuppressLint("BanParcelableUsage") // TODO(b/169214666): Remove Parcelable
-public class WatchfaceScreenshotParams implements VersionedParcelable, Parcelable {
+public class WatchFaceRenderParams implements VersionedParcelable, Parcelable {
     /** The {@link RenderParametersWireFormat} to render with. */
     @ParcelField(1)
     @NonNull
@@ -68,9 +68,9 @@
     List<IdAndComplicationDataWireFormat> mIdAndComplicationDatumWireFormats;
 
     /** Used by VersionedParcelable. */
-    WatchfaceScreenshotParams() {}
+    WatchFaceRenderParams() {}
 
-    public WatchfaceScreenshotParams(
+    public WatchFaceRenderParams(
             @NonNull RenderParametersWireFormat renderParametersWireFormats,
             long calendarTimeMillis,
             @Nullable UserStyleWireFormat userStyle,
@@ -112,17 +112,17 @@
         return 0;
     }
 
-    public static final Parcelable.Creator<WatchfaceScreenshotParams> CREATOR =
-            new Parcelable.Creator<WatchfaceScreenshotParams>() {
+    public static final Parcelable.Creator<WatchFaceRenderParams> CREATOR =
+            new Parcelable.Creator<WatchFaceRenderParams>() {
                 @Override
-                public WatchfaceScreenshotParams createFromParcel(Parcel source) {
+                public WatchFaceRenderParams createFromParcel(Parcel source) {
                     return ParcelUtils.fromParcelable(
                             source.readParcelable(getClass().getClassLoader()));
                 }
 
                 @Override
-                public WatchfaceScreenshotParams[] newArray(int size) {
-                    return new WatchfaceScreenshotParams[size];
+                public WatchFaceRenderParams[] newArray(int size) {
+                    return new WatchFaceRenderParams[size];
                 }
             };
 }
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/SystemState.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/WatchUiState.java
similarity index 79%
rename from wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/SystemState.java
rename to wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/WatchUiState.java
index 996d953..78920fe 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/SystemState.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/WatchUiState.java
@@ -28,14 +28,14 @@
 import androidx.versionedparcelable.VersionedParcelize;
 
 /**
- * Data sent over AIDL for {@link IWatchFaceCommand#setSystemState}.
+ * Data sent over AIDL for {@link IWatchFaceCommand#setWatchUiState}.
  *
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @VersionedParcelize(allowSerialization = true)
 @SuppressLint("BanParcelableUsage") // TODO(b/169214666): Remove Parcelable
-public final class SystemState implements VersionedParcelable, Parcelable {
+public final class WatchUiState implements VersionedParcelable, Parcelable {
     @ParcelField(1)
     boolean mInAmbientMode;
 
@@ -43,9 +43,9 @@
     int mInterruptionFilter;
 
     /** Used by VersionedParcelable. */
-    SystemState() {}
+    WatchUiState() {}
 
-    public SystemState(
+    public WatchUiState(
             boolean inAmbientMode,
             int interruptionFilter) {
         mInAmbientMode = inAmbientMode;
@@ -60,7 +60,7 @@
         return mInterruptionFilter;
     }
 
-    /** Serializes this SystemState to the specified {@link Parcel}. */
+    /** Serializes this WatchUiState to the specified {@link Parcel}. */
     @Override
     public void writeToParcel(@NonNull Parcel parcel, int flags) {
         parcel.writeParcelable(ParcelUtils.toParcelable(this), flags);
@@ -71,17 +71,17 @@
         return 0;
     }
 
-    public static final Parcelable.Creator<SystemState> CREATOR =
-            new Parcelable.Creator<SystemState>() {
+    public static final Parcelable.Creator<WatchUiState> CREATOR =
+            new Parcelable.Creator<WatchUiState>() {
                 @Override
-                public SystemState createFromParcel(Parcel source) {
+                public WatchUiState createFromParcel(Parcel source) {
                     return ParcelUtils.fromParcelable(
                             source.readParcelable(getClass().getClassLoader()));
                 }
 
                 @Override
-                public SystemState[] newArray(int size) {
-                    return new SystemState[size];
+                public WatchUiState[] newArray(int size) {
+                    return new WatchUiState[size];
                 }
             };
 }
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/BooleanOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/BooleanOptionWireFormat.java
index 5ffdb73..3bcb13a 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/BooleanOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/BooleanOptionWireFormat.java
@@ -18,7 +18,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.versionedparcelable.ParcelField;
 import androidx.versionedparcelable.VersionedParcelize;
 
 /**
@@ -29,14 +28,11 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @VersionedParcelize
 public class BooleanOptionWireFormat extends OptionWireFormat {
-    @ParcelField(2)
-    public boolean mValue;
 
     BooleanOptionWireFormat() {
     }
 
-    public BooleanOptionWireFormat(@NonNull String id, boolean value) {
+    public BooleanOptionWireFormat(@NonNull byte[] id) {
         super(id);
-        this.mValue = value;
     }
 }
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsOptionWireFormat.java
index b73af58..a220a66 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsOptionWireFormat.java
@@ -56,7 +56,7 @@
     }
 
     public ComplicationsOptionWireFormat(
-            @NonNull String id,
+            @NonNull byte[] id,
             @NonNull CharSequence displayName,
             @Nullable Icon icon,
             @NonNull ComplicationOverlayWireFormat[]
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/CustomValueOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/CustomValueOptionWireFormat.java
index df52b64b..63ce7ad 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/CustomValueOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/CustomValueOptionWireFormat.java
@@ -32,7 +32,7 @@
     CustomValueOptionWireFormat() {
     }
 
-    public CustomValueOptionWireFormat(@NonNull String id) {
+    public CustomValueOptionWireFormat(@NonNull byte[] id) {
         super(id);
     }
 }
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/DoubleRangeOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/DoubleRangeOptionWireFormat.java
index efff12b..a840917 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/DoubleRangeOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/DoubleRangeOptionWireFormat.java
@@ -18,7 +18,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.versionedparcelable.ParcelField;
 import androidx.versionedparcelable.VersionedParcelize;
 
 /**
@@ -30,15 +29,10 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @VersionedParcelize
 public class DoubleRangeOptionWireFormat extends OptionWireFormat {
-    /* The value for this option. Must be within the range [minimumValue .. maximumValue]. */
-    @ParcelField(2)
-    public double mValue;
-
     DoubleRangeOptionWireFormat() {
     }
 
-    public DoubleRangeOptionWireFormat(@NonNull String id, double value) {
+    public DoubleRangeOptionWireFormat(@NonNull byte[] id) {
         super(id);
-        this.mValue = value;
     }
 }
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ListOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ListOptionWireFormat.java
index 1468064..b81eb21 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ListOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ListOptionWireFormat.java
@@ -47,7 +47,7 @@
     }
 
     public ListOptionWireFormat(
-            @NonNull String id,
+            @NonNull byte[] id,
             @NonNull CharSequence displayName,
             @Nullable Icon icon
     ) {
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/LongRangeOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/LongRangeOptionWireFormat.java
index 78bc6be..27ffb72 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/LongRangeOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/LongRangeOptionWireFormat.java
@@ -18,7 +18,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.versionedparcelable.ParcelField;
 import androidx.versionedparcelable.VersionedParcelize;
 
 /**
@@ -30,15 +29,10 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 @VersionedParcelize
 public class LongRangeOptionWireFormat extends OptionWireFormat {
-    /* The value for this option. Must be within the range [minimumValue .. maximumValue]. */
-    @ParcelField(2)
-    public long mValue;
-
     LongRangeOptionWireFormat() {
     }
 
-    public LongRangeOptionWireFormat(@NonNull String id, long value) {
+    public LongRangeOptionWireFormat(@NonNull byte[] id) {
         super(id);
-        this.mValue = value;
     }
 }
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/OptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/OptionWireFormat.java
index af2ff2b..2d2190e 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/OptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/OptionWireFormat.java
@@ -39,12 +39,12 @@
     /** Identifier for the option, must be unique within the UserStyleCategory. */
     @ParcelField(1)
     @NonNull
-    public String mId = "";
+    public byte[] mId = new byte[0];
 
     OptionWireFormat() {
     }
 
-    public OptionWireFormat(@NonNull String id) {
+    public OptionWireFormat(@NonNull byte[] id) {
         mId = id;
     }
 
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleWireFormat.java
index 4d69501..0e5f3b8 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleWireFormat.java
@@ -40,11 +40,11 @@
     @ParcelField(1)
     @NonNull
     /** Map from user style setting id to user style option id. */
-    public Map<String, String> mUserStyle = new HashMap<>();
+    public Map<String, byte[]> mUserStyle = new HashMap<>();
 
     UserStyleWireFormat() {}
 
-    public UserStyleWireFormat(@NonNull Map<String, String> userStyle) {
+    public UserStyleWireFormat(@NonNull Map<String, byte[]> userStyle) {
         mUserStyle = userStyle;
     }
 
diff --git a/wear/wear-watchface-editor/api/current.txt b/wear/wear-watchface-editor/api/current.txt
index 8dd9c55..5e23ca2 100644
--- a/wear/wear-watchface-editor/api/current.txt
+++ b/wear/wear-watchface-editor/api/current.txt
@@ -2,16 +2,17 @@
 package androidx.wear.watchface.editor {
 
   public final class EditorRequest {
-    ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, String? watchFaceInstanceId, java.util.Map<java.lang.String,java.lang.String>? initialUserStyle);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle, @RequiresApi(android.os.Build.VERSION_CODES.R) androidx.wear.watchface.client.WatchFaceId watchFaceId);
+    ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle);
     method public static androidx.wear.watchface.editor.EditorRequest? createFromIntent(android.content.Intent intent);
     method public String getEditorPackageName();
-    method public java.util.Map<java.lang.String,java.lang.String>? getInitialUserStyle();
+    method public androidx.wear.watchface.style.UserStyleData? getInitialUserStyle();
     method public android.content.ComponentName getWatchFaceComponentName();
-    method public String? getWatchFaceInstanceId();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     property public final String editorPackageName;
-    property public final java.util.Map<java.lang.String,java.lang.String>? initialUserStyle;
+    property public final androidx.wear.watchface.style.UserStyleData? initialUserStyle;
     property public final android.content.ComponentName watchFaceComponentName;
-    property public final String? watchFaceInstanceId;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.EditorRequest.Companion Companion;
   }
 
@@ -22,35 +23,35 @@
   public abstract class EditorSession implements java.lang.AutoCloseable {
     ctor public EditorSession();
     method @RequiresApi(27) @UiThread public static final androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
-    method @UiThread public static final kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+    method @UiThread public static final suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity p, android.content.Intent activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> editIntent);
     method public abstract Integer? getBackgroundComplicationId();
     method @UiThread public abstract Integer? getComplicationIdAt(@Px int x, @Px int y);
-    method @UiThread public abstract suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
-    method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public abstract String? getInstanceId();
+    method @UiThread public abstract suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+    method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public abstract long getPreviewReferenceTimeMillis();
     method public abstract androidx.wear.watchface.style.UserStyle getUserStyle();
     method public abstract androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public abstract android.content.ComponentName getWatchFaceComponentName();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     method @UiThread public final boolean isCommitChangesOnClose();
-    method @UiThread public abstract suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @UiThread public abstract suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @UiThread public abstract android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     method @UiThread public final void setCommitChangesOnClose(boolean p);
     method public abstract void setUserStyle(androidx.wear.watchface.style.UserStyle p);
-    method @UiThread public abstract android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract Integer? backgroundComplicationId;
     property @UiThread public final boolean commitChangesOnClose;
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public abstract String? instanceId;
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyle userStyle;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
     property public abstract android.content.ComponentName watchFaceComponentName;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.EditorSession.Companion Companion;
   }
 
   public static final class EditorSession.Companion {
     method @RequiresApi(27) @UiThread public androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
-    method @UiThread public kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+    method @UiThread public suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> p);
   }
 
   public class WatchFaceEditorContract extends androidx.activity.result.contract.ActivityResultContract<androidx.wear.watchface.editor.EditorRequest,kotlin.Unit> {
diff --git a/wear/wear-watchface-editor/api/public_plus_experimental_current.txt b/wear/wear-watchface-editor/api/public_plus_experimental_current.txt
index 8dd9c55..5e23ca2 100644
--- a/wear/wear-watchface-editor/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-editor/api/public_plus_experimental_current.txt
@@ -2,16 +2,17 @@
 package androidx.wear.watchface.editor {
 
   public final class EditorRequest {
-    ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, String? watchFaceInstanceId, java.util.Map<java.lang.String,java.lang.String>? initialUserStyle);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle, @RequiresApi(android.os.Build.VERSION_CODES.R) androidx.wear.watchface.client.WatchFaceId watchFaceId);
+    ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle);
     method public static androidx.wear.watchface.editor.EditorRequest? createFromIntent(android.content.Intent intent);
     method public String getEditorPackageName();
-    method public java.util.Map<java.lang.String,java.lang.String>? getInitialUserStyle();
+    method public androidx.wear.watchface.style.UserStyleData? getInitialUserStyle();
     method public android.content.ComponentName getWatchFaceComponentName();
-    method public String? getWatchFaceInstanceId();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     property public final String editorPackageName;
-    property public final java.util.Map<java.lang.String,java.lang.String>? initialUserStyle;
+    property public final androidx.wear.watchface.style.UserStyleData? initialUserStyle;
     property public final android.content.ComponentName watchFaceComponentName;
-    property public final String? watchFaceInstanceId;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.EditorRequest.Companion Companion;
   }
 
@@ -22,35 +23,35 @@
   public abstract class EditorSession implements java.lang.AutoCloseable {
     ctor public EditorSession();
     method @RequiresApi(27) @UiThread public static final androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
-    method @UiThread public static final kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+    method @UiThread public static final suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity p, android.content.Intent activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> editIntent);
     method public abstract Integer? getBackgroundComplicationId();
     method @UiThread public abstract Integer? getComplicationIdAt(@Px int x, @Px int y);
-    method @UiThread public abstract suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
-    method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public abstract String? getInstanceId();
+    method @UiThread public abstract suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+    method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public abstract long getPreviewReferenceTimeMillis();
     method public abstract androidx.wear.watchface.style.UserStyle getUserStyle();
     method public abstract androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public abstract android.content.ComponentName getWatchFaceComponentName();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     method @UiThread public final boolean isCommitChangesOnClose();
-    method @UiThread public abstract suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @UiThread public abstract suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @UiThread public abstract android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     method @UiThread public final void setCommitChangesOnClose(boolean p);
     method public abstract void setUserStyle(androidx.wear.watchface.style.UserStyle p);
-    method @UiThread public abstract android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract Integer? backgroundComplicationId;
     property @UiThread public final boolean commitChangesOnClose;
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public abstract String? instanceId;
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyle userStyle;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
     property public abstract android.content.ComponentName watchFaceComponentName;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.EditorSession.Companion Companion;
   }
 
   public static final class EditorSession.Companion {
     method @RequiresApi(27) @UiThread public androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
-    method @UiThread public kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+    method @UiThread public suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> p);
   }
 
   public class WatchFaceEditorContract extends androidx.activity.result.contract.ActivityResultContract<androidx.wear.watchface.editor.EditorRequest,kotlin.Unit> {
diff --git a/wear/wear-watchface-editor/api/restricted_current.txt b/wear/wear-watchface-editor/api/restricted_current.txt
index c3fa11c..a42c76e 100644
--- a/wear/wear-watchface-editor/api/restricted_current.txt
+++ b/wear/wear-watchface-editor/api/restricted_current.txt
@@ -7,10 +7,10 @@
     method public Integer? getBackgroundComplicationId();
     method protected final boolean getClosed();
     method public Integer? getComplicationIdAt(@Px int x, @Px int y);
-    method public suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> $completion);
+    method public suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> $completion);
     method public final kotlinx.coroutines.CoroutineScope getCoroutineScope();
     method protected final boolean getForceClosed();
-    method public suspend Object? launchComplicationProviderChooser(int p, kotlin.coroutines.Continuation<? super java.lang.Boolean> $completion);
+    method public suspend Object? openComplicationProviderChooser(int p, kotlin.coroutines.Continuation<? super java.lang.Boolean> $completion);
     method @UiThread protected abstract void releaseResources();
     method protected final void requireNotClosed();
     method protected final void setClosed(boolean p);
@@ -22,16 +22,17 @@
   }
 
   public final class EditorRequest {
-    ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, String? watchFaceInstanceId, java.util.Map<java.lang.String,java.lang.String>? initialUserStyle);
+    ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle, @RequiresApi(android.os.Build.VERSION_CODES.R) androidx.wear.watchface.client.WatchFaceId watchFaceId);
+    ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle);
     method public static androidx.wear.watchface.editor.EditorRequest? createFromIntent(android.content.Intent intent);
     method public String getEditorPackageName();
-    method public java.util.Map<java.lang.String,java.lang.String>? getInitialUserStyle();
+    method public androidx.wear.watchface.style.UserStyleData? getInitialUserStyle();
     method public android.content.ComponentName getWatchFaceComponentName();
-    method public String? getWatchFaceInstanceId();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     property public final String editorPackageName;
-    property public final java.util.Map<java.lang.String,java.lang.String>? initialUserStyle;
+    property public final androidx.wear.watchface.style.UserStyleData? initialUserStyle;
     property public final android.content.ComponentName watchFaceComponentName;
-    property public final String? watchFaceInstanceId;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.EditorRequest.Companion Companion;
   }
 
@@ -42,35 +43,35 @@
   public abstract class EditorSession implements java.lang.AutoCloseable {
     ctor public EditorSession();
     method @RequiresApi(27) @UiThread public static final androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
-    method @UiThread public static final kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+    method @UiThread public static final suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity p, android.content.Intent activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> editIntent);
     method public abstract Integer? getBackgroundComplicationId();
     method @UiThread public abstract Integer? getComplicationIdAt(@Px int x, @Px int y);
-    method @UiThread public abstract suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
-    method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public abstract String? getInstanceId();
+    method @UiThread public abstract suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+    method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public abstract long getPreviewReferenceTimeMillis();
     method public abstract androidx.wear.watchface.style.UserStyle getUserStyle();
     method public abstract androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public abstract android.content.ComponentName getWatchFaceComponentName();
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     method @UiThread public final boolean isCommitChangesOnClose();
-    method @UiThread public abstract suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @UiThread public abstract suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @UiThread public abstract android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     method @UiThread public final void setCommitChangesOnClose(boolean p);
     method public abstract void setUserStyle(androidx.wear.watchface.style.UserStyle p);
-    method @UiThread public abstract android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract Integer? backgroundComplicationId;
     property @UiThread public final boolean commitChangesOnClose;
-    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public abstract String? instanceId;
+    property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyle userStyle;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
     property public abstract android.content.ComponentName watchFaceComponentName;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.EditorSession.Companion Companion;
   }
 
   public static final class EditorSession.Companion {
     method @RequiresApi(27) @UiThread public androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
-    method @UiThread public kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+    method @UiThread public suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> p);
   }
 
   public class WatchFaceEditorContract extends androidx.activity.result.contract.ActivityResultContract<androidx.wear.watchface.editor.EditorRequest,kotlin.Unit> {
diff --git a/wear/wear-watchface-editor/guava/api/current.txt b/wear/wear-watchface-editor/guava/api/current.txt
index 097e699..8a92560 100644
--- a/wear/wear-watchface-editor/guava/api/current.txt
+++ b/wear/wear-watchface-editor/guava/api/current.txt
@@ -7,26 +7,26 @@
     method @RequiresApi(27) @UiThread public static androidx.wear.watchface.editor.ListenableEditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
     method public Integer? getBackgroundComplicationId();
     method public Integer? getComplicationIdAt(int x, int y);
-    method public suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public String? getInstanceId();
+    method public suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public com.google.common.util.concurrent.ListenableFuture<java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData>> getListenableComplicationPreviewData();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyle getUserStyle();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public android.content.ComponentName getWatchFaceComponentName();
-    method public suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     method @UiThread public static com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ListenableEditorSession> listenableCreateOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableLaunchComplicationProviderChooser(int complicationId);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableOpenComplicationProviderChooser(int complicationId);
+    method public suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle value);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public Integer? backgroundComplicationId;
-    property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public String? instanceId;
+    property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public long previewReferenceTimeMillis;
     property public androidx.wear.watchface.style.UserStyle userStyle;
     property public androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
     property public android.content.ComponentName watchFaceComponentName;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.ListenableEditorSession.Companion Companion;
   }
 
diff --git a/wear/wear-watchface-editor/guava/api/public_plus_experimental_current.txt b/wear/wear-watchface-editor/guava/api/public_plus_experimental_current.txt
index 097e699..8a92560 100644
--- a/wear/wear-watchface-editor/guava/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-editor/guava/api/public_plus_experimental_current.txt
@@ -7,26 +7,26 @@
     method @RequiresApi(27) @UiThread public static androidx.wear.watchface.editor.ListenableEditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
     method public Integer? getBackgroundComplicationId();
     method public Integer? getComplicationIdAt(int x, int y);
-    method public suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public String? getInstanceId();
+    method public suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public com.google.common.util.concurrent.ListenableFuture<java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData>> getListenableComplicationPreviewData();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyle getUserStyle();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public android.content.ComponentName getWatchFaceComponentName();
-    method public suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     method @UiThread public static com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ListenableEditorSession> listenableCreateOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableLaunchComplicationProviderChooser(int complicationId);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableOpenComplicationProviderChooser(int complicationId);
+    method public suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle value);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public Integer? backgroundComplicationId;
-    property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public String? instanceId;
+    property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public long previewReferenceTimeMillis;
     property public androidx.wear.watchface.style.UserStyle userStyle;
     property public androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
     property public android.content.ComponentName watchFaceComponentName;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.ListenableEditorSession.Companion Companion;
   }
 
diff --git a/wear/wear-watchface-editor/guava/api/restricted_current.txt b/wear/wear-watchface-editor/guava/api/restricted_current.txt
index 097e699..8a92560 100644
--- a/wear/wear-watchface-editor/guava/api/restricted_current.txt
+++ b/wear/wear-watchface-editor/guava/api/restricted_current.txt
@@ -7,26 +7,26 @@
     method @RequiresApi(27) @UiThread public static androidx.wear.watchface.editor.ListenableEditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
     method public Integer? getBackgroundComplicationId();
     method public Integer? getComplicationIdAt(int x, int y);
-    method public suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
-    method public String? getInstanceId();
+    method public suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
     method public com.google.common.util.concurrent.ListenableFuture<java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData>> getListenableComplicationPreviewData();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyle getUserStyle();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public android.content.ComponentName getWatchFaceComponentName();
-    method public suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
     method @UiThread public static com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ListenableEditorSession> listenableCreateOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
-    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableLaunchComplicationProviderChooser(int complicationId);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableOpenComplicationProviderChooser(int complicationId);
+    method public suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+    method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle value);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public Integer? backgroundComplicationId;
-    property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
-    property public String? instanceId;
+    property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
     property public long previewReferenceTimeMillis;
     property public androidx.wear.watchface.style.UserStyle userStyle;
     property public androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
     property public android.content.ComponentName watchFaceComponentName;
+    property @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId watchFaceId;
     field public static final androidx.wear.watchface.editor.ListenableEditorSession.Companion Companion;
   }
 
diff --git a/wear/wear-watchface-editor/guava/build.gradle b/wear/wear-watchface-editor/guava/build.gradle
index 86e9298..5d7afd9 100644
--- a/wear/wear-watchface-editor/guava/build.gradle
+++ b/wear/wear-watchface-editor/guava/build.gradle
@@ -45,7 +45,7 @@
     name = "Android Wear Watchface Client Editor"
     type = LibraryType.PUBLISHED_LIBRARY
     mavenGroup = LibraryGroups.WEAR
-    mavenVersion = LibraryVersions.WEAR_WATCHFACE_EDITOR
+    mavenVersion = LibraryVersions.WEAR_WATCHFACE_EDITOR_GUAVA
     inceptionYear = "2021"
     description = "Guava wrappers for the Androidx Wear Watchface Editor library"
 }
diff --git a/wear/wear-watchface-editor/guava/src/main/java/androidx/wear/watchface/editor/ListenableEditorSession.kt b/wear/wear-watchface-editor/guava/src/main/java/androidx/wear/watchface/editor/ListenableEditorSession.kt
index b59c76e..7827a94 100644
--- a/wear/wear-watchface-editor/guava/src/main/java/androidx/wear/watchface/editor/ListenableEditorSession.kt
+++ b/wear/wear-watchface-editor/guava/src/main/java/androidx/wear/watchface/editor/ListenableEditorSession.kt
@@ -19,6 +19,7 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.graphics.Bitmap
+import android.os.Build
 import androidx.activity.ComponentActivity
 import androidx.annotation.RequiresApi
 import androidx.annotation.UiThread
@@ -27,6 +28,7 @@
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.client.ComplicationState
 import androidx.wear.watchface.client.HeadlessWatchFaceClient
+import androidx.wear.watchface.client.WatchFaceId
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleSchema
 import com.google.common.util.concurrent.ListenableFuture
@@ -67,10 +69,9 @@
             coroutineScope.launch {
                 try {
                     result.set(
-                        EditorSession.createOnWatchEditingSessionAsync(
-                            activity,
-                            editIntent
-                        ).await()?.let { ListenableEditorSession(it) }
+                        createOnWatchEditingSession(activity, editIntent)?.let {
+                            ListenableEditorSession(it)
+                        }
                     )
                 } catch (e: Exception) {
                     result.setException(e)
@@ -105,7 +106,9 @@
 
     override val watchFaceComponentName: ComponentName = wrappedEditorSession.watchFaceComponentName
 
-    override val instanceId: String? = wrappedEditorSession.instanceId
+    @get:RequiresApi(Build.VERSION_CODES.R)
+    @RequiresApi(Build.VERSION_CODES.R)
+    override val watchFaceId: WatchFaceId = wrappedEditorSession.watchFaceId
 
     override var userStyle: UserStyle
         get() = wrappedEditorSession.userStyle
@@ -118,16 +121,16 @@
     override val userStyleSchema: UserStyleSchema
         get() = wrappedEditorSession.userStyleSchema
 
-    override val complicationState: Map<Int, ComplicationState>
-        get() = wrappedEditorSession.complicationState
+    override val complicationsState: Map<Int, ComplicationState>
+        get() = wrappedEditorSession.complicationsState
 
-    /** [ListenableFuture] wrapper around [EditorSession.getComplicationPreviewData]. */
+    /** [ListenableFuture] wrapper around [EditorSession.getComplicationsPreviewData]. */
     public fun getListenableComplicationPreviewData():
         ListenableFuture<Map<Int, ComplicationData>> {
             val future = ResolvableFuture.create<Map<Int, ComplicationData>>()
             getCoroutineScope().launch {
                 try {
-                    future.set(wrappedEditorSession.getComplicationPreviewData())
+                    future.set(wrappedEditorSession.getComplicationsPreviewData())
                 } catch (e: Exception) {
                     future.setException(e)
                 }
@@ -135,8 +138,8 @@
             return future
         }
 
-    override suspend fun getComplicationPreviewData(): Map<Int, ComplicationData> =
-        wrappedEditorSession.getComplicationPreviewData()
+    override suspend fun getComplicationsPreviewData(): Map<Int, ComplicationData> =
+        wrappedEditorSession.getComplicationsPreviewData()
 
     @get:SuppressWarnings("AutoBoxing")
     override val backgroundComplicationId: Int?
@@ -146,25 +149,25 @@
     override fun getComplicationIdAt(x: Int, y: Int): Int? =
         wrappedEditorSession.getComplicationIdAt(x, y)
 
-    override fun takeWatchFaceScreenshot(
+    override fun renderWatchFaceToBitmap(
         renderParameters: RenderParameters,
         calendarTimeMillis: Long,
         idToComplicationData: Map<Int, ComplicationData>?
-    ): Bitmap = wrappedEditorSession.takeWatchFaceScreenshot(
+    ): Bitmap = wrappedEditorSession.renderWatchFaceToBitmap(
         renderParameters,
         calendarTimeMillis,
         idToComplicationData
     )
 
-    /** [ListenableFuture] wrapper around [EditorSession.launchComplicationProviderChooser]. */
-    public fun listenableLaunchComplicationProviderChooser(
+    /** [ListenableFuture] wrapper around [EditorSession.openComplicationProviderChooser]. */
+    public fun listenableOpenComplicationProviderChooser(
         complicationId: Int
     ): ListenableFuture<Boolean> {
         val future = ResolvableFuture.create<Boolean>()
         getCoroutineScope().launch {
             try {
                 future.set(
-                    wrappedEditorSession.launchComplicationProviderChooser(complicationId)
+                    wrappedEditorSession.openComplicationProviderChooser(complicationId)
                 )
             } catch (e: Exception) {
                 future.setException(e)
@@ -173,8 +176,8 @@
         return future
     }
 
-    override suspend fun launchComplicationProviderChooser(complicationId: Int): Boolean =
-        wrappedEditorSession.launchComplicationProviderChooser(complicationId)
+    override suspend fun openComplicationProviderChooser(complicationId: Int): Boolean =
+        wrappedEditorSession.openComplicationProviderChooser(complicationId)
 
     override fun close() {
         wrappedEditorSession.close()
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ComplicationConfigFragment.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ComplicationConfigFragment.kt
index c3b0876..ebd6b45 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ComplicationConfigFragment.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ComplicationConfigFragment.kt
@@ -97,7 +97,7 @@
     init {
         watchFaceConfigActivity.coroutineScope.launch {
             previewComplicationData =
-                watchFaceConfigActivity.editorSession.getComplicationPreviewData()
+                watchFaceConfigActivity.editorSession.getComplicationsPreviewData()
             setWillNotDraw(false)
         }
     }
@@ -159,7 +159,7 @@
         // Silently do nothing if the complication is fixed. Note the user is given a visual clue
         // that the complication is not editable in [Complication.drawOutline] so this is OK.
         val complicationState =
-            watchFaceConfigActivity.editorSession.complicationState[complicationId]!!
+            watchFaceConfigActivity.editorSession.complicationsState[complicationId]!!
         if (complicationState.fixedComplicationProvider) {
             return true
         }
@@ -209,13 +209,13 @@
 
     override fun onDraw(canvas: Canvas) {
         val editingSession = watchFaceConfigActivity.editorSession
-        val bitmap = editingSession.takeWatchFaceScreenshot(
+        val bitmap = editingSession.renderWatchFaceToBitmap(
             RenderParameters(
                 DrawMode.INTERACTIVE,
                 mapOf(
-                    Layer.BASE_LAYER to LayerMode.DRAW,
+                    Layer.BASE to LayerMode.DRAW,
                     Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
-                    Layer.TOP_LAYER to LayerMode.DRAW
+                    Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
                 ),
                 selectedComplicationId,
                 Color.RED
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ConfigFragment.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ConfigFragment.kt
index e88ea1c..7f06cf8 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ConfigFragment.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ConfigFragment.kt
@@ -87,7 +87,7 @@
     private fun initConfigOptions() {
         val editingSession = watchFaceConfigActivity.editorSession
         val hasBackgroundComplication = editingSession.backgroundComplicationId != null
-        val numComplications = editingSession.complicationState.size
+        val numComplications = editingSession.complicationsState.size
         val hasNonBackgroundComplication =
             numComplications > if (hasBackgroundComplication) 1 else 0
         val configOptions = ArrayList<ConfigOption>()
@@ -113,7 +113,7 @@
         for (styleCategory in editingSession.userStyleSchema.userStyleSettings) {
             configOptions.add(
                 ConfigOption(
-                    id = styleCategory.id,
+                    id = styleCategory.id.value,
                     icon = styleCategory.icon,
                     title = styleCategory.displayName.toString(),
                     summary = styleCategory.description.toString()
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
index 2f63855..e3b783f 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
@@ -37,6 +37,7 @@
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleData
 import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import androidx.wear.widget.SwipeDismissFrameLayout
@@ -101,9 +102,13 @@
 
         when {
             booleanUserStyleSetting.isNotEmpty() -> {
-                booleanStyle.isChecked = userStyle[styleSetting]!!.id.toBoolean()
+                booleanStyle.isChecked = userStyle[styleSetting]?.toBooleanOption()!!.value
                 booleanStyle.setOnCheckedChangeListener { _, isChecked ->
-                    setUserStyleOption(styleSetting.getOptionForId(isChecked.toString()))
+                    setUserStyleOption(
+                        styleSetting.getOptionForId(
+                            BooleanUserStyleSetting.BooleanOption(isChecked).id.value
+                        )
+                    )
                 }
                 styleOptionsList.visibility = View.GONE
                 styleOptionsList.isEnabled = false
@@ -163,7 +168,9 @@
                         ) {
                             setUserStyleOption(
                                 rangedStyleSetting.getOptionForId(
-                                    (minValue + delta * progress.toFloat()).toString()
+                                    DoubleRangeUserStyleSetting.DoubleRangeOption(
+                                        minValue + delta * progress.toFloat()
+                                    ).id.value
                                 )
                             )
                         }
@@ -199,13 +206,15 @@
         )
 
         userStyle = UserStyle(
-            ParcelUtils.fromParcelable<UserStyleWireFormat>(
-                requireArguments().getParcelable(USER_STYLE)!!
-            )!!,
+            UserStyleData(
+                ParcelUtils.fromParcelable<UserStyleWireFormat>(
+                    requireArguments().getParcelable(USER_STYLE)!!
+                )!!
+            ),
             styleSchema
         )
 
-        styleSetting = styleSchema.userStyleSettings.first { it.id == settingId }
+        styleSetting = styleSchema.userStyleSettings.first { it.id.value == settingId }
     }
 
     internal fun setUserStyleOption(userStyleOption: UserStyleSetting.Option) {
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
index b9db60c..6955326 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
@@ -82,13 +82,12 @@
         super.onCreate(savedInstanceState)
         handler = Handler(Looper.getMainLooper())
         coroutineScope = CoroutineScope(handler.asCoroutineDispatcher().immediate)
-        val deferredEditorSession = EditorSession.createOnWatchEditingSessionAsync(
-            this@WatchFaceConfigActivity,
-            intent!!
-        )
         coroutineScope.launch {
             init(
-                deferredEditorSession.await()!!,
+                EditorSession.createOnWatchEditingSession(
+                    this@WatchFaceConfigActivity,
+                    intent!!
+                )!!,
                 object : FragmentController {
                     @SuppressLint("SyntheticAccessor")
                     override fun showConfigFragment() {
@@ -118,7 +117,7 @@
                     @SuppressWarnings("deprecation")
                     override suspend fun showComplicationConfig(
                         complicationId: Int
-                    ) = editorSession.launchComplicationProviderChooser(complicationId)
+                    ) = editorSession.openComplicationProviderChooser(complicationId)
                 }
             )
         }
@@ -175,7 +174,7 @@
         if (hasBackgroundComplication) {
             topLevelOptionCount++
         }
-        val numComplications = editorSession.complicationState.size
+        val numComplications = editorSession.complicationsState.size
         val hasNonBackgroundComplication =
             numComplications > (if (hasBackgroundComplication) 1 else 0)
         if (hasNonBackgroundComplication) {
@@ -189,7 +188,7 @@
 
             // For a single complication go directly to the provider selector.
             numComplications == 1 -> {
-                val onlyComplication = editorSession.complicationState.entries.first()
+                val onlyComplication = editorSession.complicationsState.entries.first()
                 coroutineScope.launch {
                     fragmentController.showComplicationConfig(onlyComplication.key)
                 }
@@ -203,7 +202,7 @@
                 // There should only be a single userStyle setting if we get here.
                 val onlyStyleSetting = editorSession.userStyleSchema.userStyleSettings.first()
                 fragmentController.showStyleConfigFragment(
-                    onlyStyleSetting.id,
+                    onlyStyleSetting.id.value,
                     editorSession.userStyleSchema,
                     editorSession.userStyle
                 )
diff --git a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
index 22cc43d..d2c8270 100644
--- a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
+++ b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
@@ -51,15 +51,18 @@
 import androidx.wear.watchface.MutableWatchState
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.WatchFace
+import androidx.wear.watchface.client.WatchFaceId
 import androidx.wear.watchface.client.asApiEditorState
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
 import androidx.wear.watchface.data.ComplicationBoundsType
 import androidx.wear.watchface.editor.data.EditorStateWireFormat
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
+import androidx.wear.watchface.style.UserStyleSetting.Option
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -116,8 +119,8 @@
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        val deferredEditorSession =
-            EditorSession.createOnWatchEditingSessionAsyncImpl(
+        immediateCoroutineScope.launch {
+            editorSession = EditorSession.createOnWatchEditingSessionImpl(
                 this@OnWatchFaceEditingTestActivity,
                 intent!!,
                 object : ProviderInfoRetrieverProvider {
@@ -129,14 +132,14 @@
                                     "ProviderApp1",
                                     "Provider1",
                                     providerIcon1,
-                                    ComplicationType.SHORT_TEXT.asWireComplicationType(),
+                                    ComplicationType.SHORT_TEXT.toWireComplicationType(),
                                     provider1
                                 ),
                                 RIGHT_COMPLICATION_ID to ComplicationProviderInfo(
                                     "ProviderApp2",
                                     "Provider2",
                                     providerIcon2,
-                                    ComplicationType.LONG_TEXT.asWireComplicationType(),
+                                    ComplicationType.LONG_TEXT.toWireComplicationType(),
                                     provider2
                                 )
                             ),
@@ -157,10 +160,7 @@
                         )
                     )
                 }
-            )
-
-        immediateCoroutineScope.launch {
-            editorSession = deferredEditorSession.await()!!
+            )!!
         }
     }
 }
@@ -219,7 +219,7 @@
                         Icon.createWithBitmap(
                             Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
                         ),
-                        ComplicationType.LONG_TEXT.asWireComplicationType(),
+                        ComplicationType.LONG_TEXT.toWireComplicationType(),
                         provider3
                     )
                 )
@@ -229,59 +229,48 @@
     }
 }
 
-// Disables the requirement that watchFaceInstanceId has to be non-null on R and above.
-public class WatchFaceEditorContractForTest : WatchFaceEditorContract() {
-    override fun nullWatchFaceInstanceIdOK() = true
-}
-
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 public class EditorSessionTest {
     private val testComponentName = ComponentName("test.package", "test.class")
     private val testEditorPackageName = "test.package"
-    private val testInstanceId = "TEST_INSTANCE_ID"
+    private val testInstanceId = WatchFaceId("TEST_INSTANCE_ID")
     private lateinit var editorDelegate: WatchFace.EditorDelegate
     private val screenBounds = Rect(0, 0, 400, 400)
 
-    private val redStyleOption =
-        UserStyleSetting.ListUserStyleSetting.ListOption("red_style", "Red", icon = null)
+    private val redStyleOption = ListOption(Option.Id("red_style"), "Red", icon = null)
 
-    private val greenStyleOption =
-        UserStyleSetting.ListUserStyleSetting.ListOption("green_style", "Green", icon = null)
+    private val greenStyleOption = ListOption(Option.Id("green_style"), "Green", icon = null)
 
-    private val blueStyleOption =
-        UserStyleSetting.ListUserStyleSetting.ListOption("bluestyle", "Blue", icon = null)
+    private val blueStyleOption = ListOption(Option.Id("bluestyle"), "Blue", icon = null)
 
     private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
 
     private val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
-        "color_style_setting",
+        UserStyleSetting.Id("color_style_setting"),
         "Colors",
         "Watchface colorization", /* icon = */
         null,
         colorStyleList,
-        listOf(Layer.BASE_LAYER)
+        listOf(Layer.BASE)
     )
 
-    private val classicStyleOption =
-        UserStyleSetting.ListUserStyleSetting.ListOption("classic_style", "Classic", icon = null)
+    private val classicStyleOption = ListOption(Option.Id("classic_style"), "Classic", icon = null)
 
-    private val modernStyleOption =
-        UserStyleSetting.ListUserStyleSetting.ListOption("modern_style", "Modern", icon = null)
+    private val modernStyleOption = ListOption(Option.Id("modern_style"), "Modern", icon = null)
 
-    private val gothicStyleOption =
-        UserStyleSetting.ListUserStyleSetting.ListOption("gothic_style", "Gothic", icon = null)
+    private val gothicStyleOption = ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
 
     private val watchHandStyleList =
         listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
 
     private val watchHandStyleSetting = UserStyleSetting.ListUserStyleSetting(
-        "hand_style_setting",
+        UserStyleSetting.Id("hand_style_setting"),
         "Hand Style",
         "Hand visual look", /* icon = */
         null,
         watchHandStyleList,
-        listOf(Layer.TOP_LAYER)
+        listOf(Layer.COMPLICATIONS_OVERLAY)
     )
 
     private val placeholderWatchState = MutableWatchState().asWatchState()
@@ -361,10 +350,10 @@
     private fun createOnWatchFaceEditingTestActivity(
         userStyleSettings: List<UserStyleSetting>,
         complications: List<Complication>,
-        instanceId: String? = testInstanceId,
+        watchFaceId: WatchFaceId = testInstanceId,
         previewReferenceTimeMillis: Long = 12345
     ): ActivityScenario<OnWatchFaceEditingTestActivity> {
-        val userStyleRepository = UserStyleRepository(UserStyleSchema(userStyleSettings))
+        val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(userStyleSettings))
         val complicationsManager = ComplicationsManager(complications, userStyleRepository)
 
         // Mocking getters and setters with mockito at the same time is hard so we do this instead.
@@ -372,13 +361,15 @@
             override val userStyleSchema = userStyleRepository.schema
             override var userStyle: UserStyle
                 get() = userStyleRepository.userStyle
-                set(value) { userStyleRepository.userStyle = value }
+                set(value) {
+                    userStyleRepository.userStyle = value
+                }
 
             override val complicationsManager = complicationsManager
             override val screenBounds = [email protected]
             override val previewReferenceTimeMillis = previewReferenceTimeMillis
 
-            override fun takeScreenshot(
+            override fun renderWatchFaceToBitmap(
                 renderParameters: RenderParameters,
                 calendarTimeMillis: Long,
                 idToComplicationData: Map<Int, androidx.wear.complications.data.ComplicationData>?
@@ -391,9 +382,9 @@
         WatchFace.registerEditorDelegate(testComponentName, editorDelegate)
 
         return ActivityScenario.launch(
-            WatchFaceEditorContractForTest().createIntent(
+            WatchFaceEditorContract().createIntent(
                 ApplicationProvider.getApplicationContext<Context>(),
-                EditorRequest(testComponentName, testEditorPackageName, instanceId, null)
+                EditorRequest(testComponentName, testEditorPackageName, null, watchFaceId)
             ).apply {
                 component = ComponentName(
                     ApplicationProvider.getApplicationContext<Context>(),
@@ -415,7 +406,7 @@
     public fun instanceId() {
         val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
         scenario.onActivity {
-            assertThat(it.editorSession.instanceId).isEqualTo(testInstanceId)
+            assertThat(it.editorSession.watchFaceId.id).isEqualTo(testInstanceId.id)
         }
     }
 
@@ -448,10 +439,12 @@
         scenario.onActivity {
             val userStyleSchema = it.editorSession.userStyleSchema
             assertThat(userStyleSchema.userStyleSettings.size).isEqualTo(2)
-            assertThat(userStyleSchema.userStyleSettings[0].id).isEqualTo(colorStyleSetting.id)
+            assertThat(userStyleSchema.userStyleSettings[0].id.value)
+                .isEqualTo(colorStyleSetting.id.value)
             assertThat(userStyleSchema.userStyleSettings[0].options.size)
                 .isEqualTo(colorStyleSetting.options.size)
-            assertThat(userStyleSchema.userStyleSettings[1].id).isEqualTo(watchHandStyleSetting.id)
+            assertThat(userStyleSchema.userStyleSettings[1].id.value)
+                .isEqualTo(watchHandStyleSetting.id.value)
             assertThat(userStyleSchema.userStyleSettings[1].options.size)
                 .isEqualTo(watchHandStyleSetting.options.size)
             // We could test more state but this should be enough.
@@ -478,40 +471,42 @@
             listOf(leftComplication, rightComplication, backgroundComplication)
         )
         scenario.onActivity {
-            assertThat(it.editorSession.complicationState.size).isEqualTo(3)
-            assertThat(it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.bounds)
+            assertThat(it.editorSession.complicationsState.size).isEqualTo(3)
+            assertThat(it.editorSession.complicationsState[LEFT_COMPLICATION_ID]!!.bounds)
                 .isEqualTo(Rect(80, 160, 160, 240))
-            assertThat(it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.boundsType)
+            assertThat(it.editorSession.complicationsState[LEFT_COMPLICATION_ID]!!.boundsType)
                 .isEqualTo(ComplicationBoundsType.ROUND_RECT)
             assertFalse(
-                it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.fixedComplicationProvider
+                it.editorSession.complicationsState[
+                    LEFT_COMPLICATION_ID
+                ]!!.fixedComplicationProvider
             )
             assertTrue(
-                it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.isInitiallyEnabled
+                it.editorSession.complicationsState[LEFT_COMPLICATION_ID]!!.isInitiallyEnabled
             )
 
-            assertThat(it.editorSession.complicationState[RIGHT_COMPLICATION_ID]!!.bounds)
+            assertThat(it.editorSession.complicationsState[RIGHT_COMPLICATION_ID]!!.bounds)
                 .isEqualTo(Rect(240, 160, 320, 240))
-            assertThat(it.editorSession.complicationState[RIGHT_COMPLICATION_ID]!!.boundsType)
+            assertThat(it.editorSession.complicationsState[RIGHT_COMPLICATION_ID]!!.boundsType)
                 .isEqualTo(ComplicationBoundsType.ROUND_RECT)
             assertFalse(
-                it.editorSession.complicationState[RIGHT_COMPLICATION_ID]!!
+                it.editorSession.complicationsState[RIGHT_COMPLICATION_ID]!!
                     .fixedComplicationProvider
             )
             assertTrue(
-                it.editorSession.complicationState[RIGHT_COMPLICATION_ID]!!.isInitiallyEnabled
+                it.editorSession.complicationsState[RIGHT_COMPLICATION_ID]!!.isInitiallyEnabled
             )
 
-            assertThat(it.editorSession.complicationState[BACKGROUND_COMPLICATION_ID]!!.bounds)
+            assertThat(it.editorSession.complicationsState[BACKGROUND_COMPLICATION_ID]!!.bounds)
                 .isEqualTo(screenBounds)
-            assertThat(it.editorSession.complicationState[BACKGROUND_COMPLICATION_ID]!!.boundsType)
+            assertThat(it.editorSession.complicationsState[BACKGROUND_COMPLICATION_ID]!!.boundsType)
                 .isEqualTo(ComplicationBoundsType.BACKGROUND)
             assertFalse(
-                it.editorSession.complicationState[BACKGROUND_COMPLICATION_ID]!!
+                it.editorSession.complicationsState[BACKGROUND_COMPLICATION_ID]!!
                     .fixedComplicationProvider
             )
             assertFalse(
-                it.editorSession.complicationState[BACKGROUND_COMPLICATION_ID]!!.isInitiallyEnabled
+                it.editorSession.complicationsState[BACKGROUND_COMPLICATION_ID]!!.isInitiallyEnabled
             )
             // We could test more state but this should be enough.
         }
@@ -544,15 +539,17 @@
         )
         scenario.onActivity {
             assertTrue(
-                it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.fixedComplicationProvider
+                it.editorSession.complicationsState[
+                    LEFT_COMPLICATION_ID
+                ]!!.fixedComplicationProvider
             )
 
             try {
                 runBlocking {
-                    it.editorSession.launchComplicationProviderChooser(LEFT_COMPLICATION_ID)
+                    it.editorSession.openComplicationProviderChooser(LEFT_COMPLICATION_ID)
 
                     fail(
-                        "launchComplicationProviderChooser should fail for a fixed complication " +
+                        "openComplicationProviderChooser should fail for a fixed complication " +
                             "provider"
                     )
                 }
@@ -753,11 +750,11 @@
              * Invoke [TestComplicationHelperActivity] which will change the provider (and hence
              * the preview data) for [LEFT_COMPLICATION_ID].
              */
-            assertTrue(editorSession.launchComplicationProviderChooser(LEFT_COMPLICATION_ID))
+            assertTrue(editorSession.openComplicationProviderChooser(LEFT_COMPLICATION_ID))
 
             // This should update the preview data to point to the updated provider3 data.
             val previewComplication =
-                editorSession.getComplicationPreviewData()[LEFT_COMPLICATION_ID]
+                editorSession.getComplicationsPreviewData()[LEFT_COMPLICATION_ID]
                     as LongTextComplicationData
 
             assertThat(
@@ -771,7 +768,7 @@
                 TestComplicationHelperActivity.lastIntent?.extras?.getString(
                     ProviderChooserIntent.EXTRA_WATCHFACE_INSTANCE_ID
                 )
-            ).isEqualTo(testInstanceId)
+            ).isEqualTo(testInstanceId.id)
         }
     }
 
@@ -790,7 +787,7 @@
         }
 
         runBlocking {
-            assertTrue(editorSession.launchComplicationProviderChooser(RIGHT_COMPLICATION_ID))
+            assertTrue(editorSession.openComplicationProviderChooser(RIGHT_COMPLICATION_ID))
 
             assertThat(
                 TestComplicationHelperActivity.lastIntent?.extras?.getString(
@@ -816,12 +813,12 @@
     }
 
     @Test
-    public fun takeWatchFaceScreenshot() {
+    public fun renderWatchFaceToBitmap() {
         val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
 
         scenario.onActivity {
             assertThat(
-                it.editorSession.takeWatchFaceScreenshot(
+                it.editorSession.renderWatchFaceToBitmap(
                     RenderParameters.DEFAULT_INTERACTIVE,
                     1234L,
                     null
@@ -858,18 +855,21 @@
             TimeUnit.MILLISECONDS
         ).asApiEditorState()
 
-        assertThat(result.userStyle[colorStyleSetting.id]).isEqualTo(blueStyleOption.id)
-        assertThat(result.userStyle[watchHandStyleSetting.id]).isEqualTo(gothicStyleOption.id)
-        assertThat(result.watchFaceInstanceId).isEqualTo(testInstanceId)
-        assertTrue(result.commitChanges)
+        assertThat(result.userStyle.userStyleMap[colorStyleSetting.id.value])
+            .isEqualTo(blueStyleOption.id.value)
+        assertThat(result.userStyle.userStyleMap[watchHandStyleSetting.id.value])
+            .isEqualTo(gothicStyleOption.id.value)
+        assertThat(result.watchFaceId.id).isEqualTo(testInstanceId.id)
+        assertTrue(result.shouldCommitChanges)
 
         // The style change should also have been applied to the watchface
-        assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id).isEqualTo(blueStyleOption.id)
-        assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id)
-            .isEqualTo(gothicStyleOption.id)
+        assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+            .isEqualTo(blueStyleOption.id.value)
+        assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+            .isEqualTo(gothicStyleOption.id.value)
 
-        assertThat(result.previewComplicationData.size).isEqualTo(2)
-        val leftComplicationData = result.previewComplicationData[LEFT_COMPLICATION_ID] as
+        assertThat(result.previewComplicationsData.size).isEqualTo(2)
+        val leftComplicationData = result.previewComplicationsData[LEFT_COMPLICATION_ID] as
             ShortTextComplicationData
         assertThat(
             leftComplicationData.text.getTextAt(
@@ -878,7 +878,7 @@
             )
         ).isEqualTo("Left")
 
-        val rightComplicationData = result.previewComplicationData[RIGHT_COMPLICATION_ID] as
+        val rightComplicationData = result.previewComplicationsData[RIGHT_COMPLICATION_ID] as
             LongTextComplicationData
         assertThat(
             rightComplicationData.text.getTextAt(
@@ -891,11 +891,11 @@
     }
 
     @Test
-    public fun nullInstanceId() {
+    public fun emptyInstanceId() {
         val scenario = createOnWatchFaceEditingTestActivity(
             listOf(colorStyleSetting, watchHandStyleSetting),
             emptyList(),
-            instanceId = null
+            watchFaceId = WatchFaceId("")
         )
 
         val editorObserver = TestEditorObserver()
@@ -903,7 +903,7 @@
 
         scenario.onActivity { activity ->
             runBlocking {
-                assertThat(activity.editorSession.instanceId).isNull()
+                assertThat(activity.editorSession.watchFaceId.id).isEmpty()
                 activity.editorSession.close()
                 activity.finish()
             }
@@ -913,7 +913,7 @@
             TIMEOUT_MILLIS,
             TimeUnit.MILLISECONDS
         ).asApiEditorState()
-        assertThat(result.watchFaceInstanceId).isEmpty()
+        assertThat(result.watchFaceId.id).isEmpty()
 
         EditorService.globalEditorService.unregisterObserver(observerId)
     }
@@ -934,7 +934,7 @@
             TIMEOUT_MILLIS,
             TimeUnit.MILLISECONDS
         ).asApiEditorState()
-        assertThat(result.previewComplicationData).isEmpty()
+        assertThat(result.previewComplicationsData).isEmpty()
 
         EditorService.globalEditorService.unregisterObserver(observerId)
     }
@@ -949,10 +949,10 @@
         val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
         scenario.onActivity { activity ->
             runBlocking {
-                assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id)
-                    .isEqualTo(redStyleOption.id)
-                assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id)
-                    .isEqualTo(classicStyleOption.id)
+                assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+                    .isEqualTo(redStyleOption.id.value)
+                assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+                    .isEqualTo(classicStyleOption.id.value)
 
                 // Select [blueStyleOption] and [gothicStyleOption].
                 val styleMap = activity.editorSession.userStyle.selectedOptions.toMutableMap()
@@ -972,15 +972,18 @@
             TIMEOUT_MILLIS,
             TimeUnit.MILLISECONDS
         ).asApiEditorState()
-        assertThat(result.userStyle[colorStyleSetting.id]).isEqualTo(blueStyleOption.id)
-        assertThat(result.userStyle[watchHandStyleSetting.id]).isEqualTo(gothicStyleOption.id)
-        assertFalse(result.commitChanges)
+        assertThat(result.userStyle.userStyleMap[colorStyleSetting.id.value])
+            .isEqualTo(blueStyleOption.id.value)
+        assertThat(result.userStyle.userStyleMap[watchHandStyleSetting.id.value])
+            .isEqualTo(gothicStyleOption.id.value)
+        assertFalse(result.shouldCommitChanges)
 
         // The original style should be applied to the watch face however because
         // commitChangesOnClose is false.
-        assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id).isEqualTo(redStyleOption.id)
-        assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id)
-            .isEqualTo(classicStyleOption.id)
+        assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+            .isEqualTo(redStyleOption.id.value)
+        assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+            .isEqualTo(classicStyleOption.id.value)
 
         EditorService.globalEditorService.unregisterObserver(observerId)
     }
@@ -990,7 +993,7 @@
         runBlocking {
             val intent = WatchFaceEditorContract().createIntent(
                 ApplicationProvider.getApplicationContext<Context>(),
-                EditorRequest(testComponentName, testEditorPackageName, testInstanceId, null)
+                EditorRequest(testComponentName, testEditorPackageName, null, testInstanceId)
             )
             assertThat(intent.getPackage()).isEqualTo(testEditorPackageName)
 
@@ -998,7 +1001,7 @@
             assertThat(editorRequest.editorPackageName).isEqualTo(testEditorPackageName)
             assertThat(editorRequest.initialUserStyle).isNull()
             assertThat(editorRequest.watchFaceComponentName).isEqualTo(testComponentName)
-            assertThat(editorRequest.watchFaceInstanceId).isEqualTo(testInstanceId)
+            assertThat(editorRequest.watchFaceId.id).isEqualTo(testInstanceId.id)
         }
     }
 
@@ -1030,9 +1033,10 @@
         assertFalse(editorObserver.stateChangeObserved())
 
         // The style change should not have been applied to the watchface.
-        assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id).isEqualTo(redStyleOption.id)
-        assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id)
-            .isEqualTo(classicStyleOption.id)
+        assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+            .isEqualTo(redStyleOption.id.value)
+        assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+            .isEqualTo(classicStyleOption.id.value)
 
         EditorService.globalEditorService.unregisterObserver(observerId)
     }
@@ -1040,9 +1044,14 @@
     @Test
     public fun closeEditorSessionBeforeWatchFaceDelegateCreated() {
         val session: ActivityScenario<OnWatchFaceEditingTestActivity> = ActivityScenario.launch(
-            WatchFaceEditorContractForTest().createIntent(
+            WatchFaceEditorContract().createIntent(
                 ApplicationProvider.getApplicationContext<Context>(),
-                EditorRequest(testComponentName, testEditorPackageName, "instanceId", null)
+                EditorRequest(
+                    testComponentName,
+                    testEditorPackageName,
+                    null,
+                    WatchFaceId("instanceId")
+                )
             ).apply {
                 component = ComponentName(
                     ApplicationProvider.getApplicationContext<Context>(),
diff --git a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionGuavaTest.kt b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionGuavaTest.kt
index 4855834..d867db8 100644
--- a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionGuavaTest.kt
+++ b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionGuavaTest.kt
@@ -35,8 +35,9 @@
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.MutableWatchState
 import androidx.wear.watchface.WatchFace
+import androidx.wear.watchface.client.WatchFaceId
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
 import com.google.common.truth.Truth
@@ -54,7 +55,7 @@
 public class EditorSessionGuavaTest {
     private val testComponentName = ComponentName("test.package", "test.class")
     private val testEditorPackageName = "test.package"
-    private val testInstanceId = "TEST_INSTANCE_ID"
+    private val testInstanceId = WatchFaceId("TEST_INSTANCE_ID")
     private var editorDelegate = Mockito.mock(WatchFace.EditorDelegate::class.java)
     private val screenBounds = Rect(0, 0, 400, 400)
 
@@ -98,10 +99,10 @@
     private fun createOnWatchFaceEditingTestActivity(
         userStyleSettings: List<UserStyleSetting>,
         complications: List<Complication>,
-        instanceId: String? = testInstanceId,
+        watchFaceId: WatchFaceId = testInstanceId,
         previewReferenceTimeMillis: Long = 12345
     ): ActivityScenario<OnWatchFaceEditingTestActivity> {
-        val userStyleRepository = UserStyleRepository(UserStyleSchema(userStyleSettings))
+        val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(userStyleSettings))
         val complicationsManager = ComplicationsManager(complications, userStyleRepository)
 
         WatchFace.registerEditorDelegate(testComponentName, editorDelegate)
@@ -113,9 +114,9 @@
             .thenReturn(previewReferenceTimeMillis)
 
         return ActivityScenario.launch(
-            WatchFaceEditorContractForTest().createIntent(
+            WatchFaceEditorContract().createIntent(
                 ApplicationProvider.getApplicationContext<Context>(),
-                EditorRequest(testComponentName, testEditorPackageName, instanceId, null)
+                EditorRequest(testComponentName, testEditorPackageName, null, watchFaceId)
             ).apply {
                 component = ComponentName(
                     ApplicationProvider.getApplicationContext<Context>(),
@@ -155,7 +156,7 @@
     }
 
     @Test
-    public fun listenableLaunchComplicationProviderChooser() {
+    public fun listenableOpenComplicationProviderChooser() {
         ComplicationProviderChooserContract.useTestComplicationHelperActivity = true
         val scenario = createOnWatchFaceEditingTestActivity(
             emptyList(),
@@ -172,7 +173,7 @@
          * the preview data) for [LEFT_COMPLICATION_ID].
          */
         assertTrue(
-            listenableEditorSession.listenableLaunchComplicationProviderChooser(
+            listenableEditorSession.listenableOpenComplicationProviderChooser(
                 LEFT_COMPLICATION_ID
             ).get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
         )
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 3f7b0d6..65863bd 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -21,6 +21,7 @@
 import android.content.Context
 import android.content.Intent
 import android.graphics.Bitmap
+import android.os.Build
 import android.os.Bundle
 import android.os.Handler
 import android.os.Looper
@@ -45,27 +46,29 @@
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.client.ComplicationState
-import androidx.wear.watchface.client.EditorObserverListener
+import androidx.wear.watchface.client.EditorListener
 import androidx.wear.watchface.client.EditorServiceClient
 import androidx.wear.watchface.client.EditorState
 import androidx.wear.watchface.client.HeadlessWatchFaceClient
+import androidx.wear.watchface.client.WatchFaceId
 import androidx.wear.watchface.data.ComplicationBoundsType
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.editor.data.EditorStateWireFormat
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleData
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.async
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 /**
  * Interface for manipulating watch face state during an editing session for a watch face editing
- * session. The editor should adjust [userStyle] and call [launchComplicationProviderChooser] to
+ * session. The editor should adjust [userStyle] and call [openComplicationProviderChooser] to
  * configure the watch face and call [close] when done. This reports the updated [EditorState] to
- * the [EditorObserverListener]s registered via [EditorServiceClient.registerObserver].
+ * the [EditorListener]s registered via [EditorServiceClient.addListener].
  */
 public abstract class EditorSession : AutoCloseable {
     /** The [ComponentName] of the watch face being edited. */
@@ -76,7 +79,8 @@
      * beyond, it's `null` on Android P and earlier. Note each distinct [ComponentName] can have
      * multiple instances.
      */
-    public abstract val instanceId: String?
+    @get:RequiresApi(Build.VERSION_CODES.R)
+    public abstract val watchFaceId: WatchFaceId
 
     /** The current [UserStyle]. Assigning to this will cause the style to update. */
     public abstract var userStyle: UserStyle
@@ -91,7 +95,7 @@
      * Map of complication ids to [ComplicationState] for each complication slot. Note
      * [ComplicationState] can change, typically in response to styling.
      */
-    public abstract val complicationState: Map<Int, ComplicationState>
+    public abstract val complicationsState: Map<Int, ComplicationState>
 
     /**
      * Whether any changes should be committed when the session is closed (defaults to `true`).
@@ -114,11 +118,11 @@
      * Returns a map of complication ids to preview [ComplicationData] suitable for use in rendering
      * the watch face. Note if a slot is configured to be empty then it will not appear in the map,
      * however disabled complications are included. Note also unlike live data this is static per
-     * provider, but it may change (on the UIThread) as a result of
-     * [launchComplicationProviderChooser].
+     * provider, but it may update (on the UiThread) as a result of
+     * [openComplicationProviderChooser].
      */
     @UiThread
-    public abstract suspend fun getComplicationPreviewData(): Map<Int, ComplicationData>
+    public abstract suspend fun getComplicationsPreviewData(): Map<Int, ComplicationData>
 
     /** The ID of the background complication or `null` if there isn't one. */
     @get:SuppressWarnings("AutoBoxing")
@@ -130,25 +134,26 @@
     public abstract fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int?
 
     /**
-     * Takes a screen shot of the watch face using the current [userStyle].
+     * Renders the watch face to a [Bitmap] using the current [userStyle].
      *
      * @param renderParameters The [RenderParameters] to render with
      * @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with
      * @param idToComplicationData The [ComplicationData] for each complication to render with
      */
     @UiThread
-    public abstract fun takeWatchFaceScreenshot(
+    public abstract fun renderWatchFaceToBitmap(
         renderParameters: RenderParameters,
         calendarTimeMillis: Long,
         idToComplicationData: Map<Int, ComplicationData>?
     ): Bitmap
 
     /**
-     * Launches the complication provider chooser and returns `true` if the user made a selection or
-     * `false` if the activity was canceled.
+     * Opens the complication provider chooser and returns `true` if the user made a selection or
+     * `false` if the activity was canceled. If the complication provider was changed then the map
+     * returned by [getComplicationsPreviewData] is updated (on the UiThread).
      */
     @UiThread
-    public abstract suspend fun launchComplicationProviderChooser(complicationId: Int): Boolean
+    public abstract suspend fun openComplicationProviderChooser(complicationId: Int): Boolean
 
     public companion object {
         /**
@@ -164,10 +169,10 @@
         @SuppressWarnings("ExecutorRegistration")
         @JvmStatic
         @UiThread
-        public fun createOnWatchEditingSessionAsync(
+        public suspend fun createOnWatchEditingSession(
             activity: ComponentActivity,
             editIntent: Intent
-        ): Deferred<EditorSession?> = createOnWatchEditingSessionAsyncImpl(
+        ): EditorSession? = createOnWatchEditingSessionImpl(
             activity,
             editIntent,
             object : ProviderInfoRetrieverProvider {
@@ -176,11 +181,11 @@
         )
 
         // Used by tests.
-        internal fun createOnWatchEditingSessionAsyncImpl(
+        internal suspend fun createOnWatchEditingSessionImpl(
             activity: ComponentActivity,
             editIntent: Intent,
             providerInfoRetrieverProvider: ProviderInfoRetrieverProvider
-        ): Deferred<EditorSession?> = TraceEvent(
+        ): EditorSession? = TraceEvent(
             "EditorSession.createOnWatchEditingSessionAsyncImpl"
         ).use {
             val coroutineScope =
@@ -190,7 +195,7 @@
                 val session = OnWatchFaceEditorSessionImpl(
                     activity,
                     editorRequest.watchFaceComponentName,
-                    editorRequest.watchFaceInstanceId,
+                    editorRequest.watchFaceId,
                     editorRequest.initialUserStyle,
                     providerInfoRetrieverProvider,
                     coroutineScope
@@ -198,17 +203,18 @@
 
                 // But full initialization has to be deferred because
                 // [WatchFace.getOrCreateEditorDelegate] is async.
-                coroutineScope.async {
+                // Resolve only after init has been completed.
+                withContext(coroutineScope.coroutineContext) {
                     session.setEditorDelegate(
                         WatchFace.getOrCreateEditorDelegate(
                             editorRequest.watchFaceComponentName
                         ).await()!!
                     )
 
-                    // Resolve the Deferred<EditorSession?> only after init has been completed.
+                    // Resolve only after init has been completed.
                     session
                 }
-            } ?: CompletableDeferred(null)
+            }
         }
 
         /**
@@ -232,7 +238,7 @@
                     activity,
                     headlessWatchFaceClient,
                     it.watchFaceComponentName,
-                    it.watchFaceInstanceId,
+                    it.watchFaceId,
                     it.initialUserStyle!!,
                     object : ProviderInfoRetrieverProvider {
                         override fun getProviderInfoRetriever() = ProviderInfoRetriever(activity)
@@ -279,7 +285,7 @@
     private val deferredComplicationPreviewDataMap =
         CompletableDeferred<MutableMap<Int, ComplicationData>>()
 
-    override suspend fun getComplicationPreviewData(): Map<Int, ComplicationData> {
+    override suspend fun getComplicationsPreviewData(): Map<Int, ComplicationData> {
         return deferredComplicationPreviewDataMap.await()
     }
 
@@ -313,33 +319,33 @@
             }
         }
 
-    override suspend fun launchComplicationProviderChooser(
+    override suspend fun openComplicationProviderChooser(
         complicationId: Int
     ): Boolean = TraceEvent(
         "BaseEditorSession.launchComplicationProviderChooser $complicationId"
     ).use {
         requireNotClosed()
-        require(!complicationState[complicationId]!!.fixedComplicationProvider) {
+        require(!complicationsState[complicationId]!!.fixedComplicationProvider) {
             "Can't configure fixed complication ID $complicationId"
         }
         pendingComplicationProviderChooserResult = CompletableDeferred<Boolean>()
         pendingComplicationProviderId = complicationId
         chooseComplicationProvider.launch(
-            ComplicationProviderChooserRequest(this, complicationId, instanceId)
+            ComplicationProviderChooserRequest(this, complicationId, watchFaceId.id)
         )
         return pendingComplicationProviderChooserResult!!.await()
     }
 
     override val backgroundComplicationId: Int? by lazy {
         requireNotClosed()
-        complicationState.entries.firstOrNull {
+        complicationsState.entries.firstOrNull {
             it.value.boundsType == ComplicationBoundsType.BACKGROUND
         }?.key
     }
 
     override fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? {
         requireNotClosed()
-        return complicationState.entries.firstOrNull {
+        return complicationsState.entries.firstOrNull {
             it.value.isEnabled && when (it.value.boundsType) {
                 ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
                 ComplicationBoundsType.BACKGROUND -> false
@@ -405,7 +411,7 @@
             // better to crash and start over.
             val providerInfoArray = providerInfoRetriever.retrieveProviderInfo(
                 watchFaceComponentName,
-                complicationState.keys.toIntArray()
+                complicationsState.keys.toIntArray()
             )
             deferredComplicationPreviewDataMap.complete(
                 // Parallel fetch preview ComplicationData.
@@ -433,9 +439,9 @@
         EditorService.globalEditorService.removeCloseCallback(closeCallback)
         coroutineScope.launchWithTracing("BaseEditorSession.close") {
             val editorState = EditorStateWireFormat(
-                instanceId,
+                watchFaceId.id,
                 userStyle.toWireFormat(),
-                getComplicationPreviewData().map {
+                getComplicationsPreviewData().map {
                     IdAndComplicationDataWireFormat(
                         it.key,
                         it.value.asWireComplicationData()
@@ -474,8 +480,8 @@
 internal class OnWatchFaceEditorSessionImpl(
     activity: ComponentActivity,
     override val watchFaceComponentName: ComponentName,
-    override val instanceId: String?,
-    private val initialEditorUserStyle: Map<String, String>?,
+    override val watchFaceId: WatchFaceId,
+    private val initialEditorUserStyle: UserStyleData?,
     providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
     coroutineScope: CoroutineScope
 ) : BaseEditorSession(activity, providerInfoRetrieverProvider, coroutineScope) {
@@ -488,7 +494,7 @@
 
     override val previewReferenceTimeMillis by lazy { editorDelegate.previewReferenceTimeMillis }
 
-    override val complicationState
+    override val complicationsState
         get() = editorDelegate.complicationsManager.complications.mapValues {
             requireNotClosed()
             ComplicationState(
@@ -525,13 +531,13 @@
 
     private lateinit var previousWatchFaceUserStyle: UserStyle
 
-    override fun takeWatchFaceScreenshot(
+    override fun renderWatchFaceToBitmap(
         renderParameters: RenderParameters,
         calendarTimeMillis: Long,
         idToComplicationData: Map<Int, ComplicationData>?
     ): Bitmap {
         requireNotClosed()
-        return editorDelegate.takeScreenshot(
+        return editorDelegate.renderWatchFaceToBitmap(
             renderParameters,
             calendarTimeMillis,
             idToComplicationData
@@ -569,8 +575,8 @@
     activity: ComponentActivity,
     private val headlessWatchFaceClient: HeadlessWatchFaceClient,
     override val watchFaceComponentName: ComponentName,
-    override val instanceId: String?,
-    initialUserStyle: Map<String, String>,
+    override val watchFaceId: WatchFaceId,
+    initialUserStyle: UserStyleData,
     providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
     coroutineScope: CoroutineScope,
 ) : BaseEditorSession(activity, providerInfoRetrieverProvider, coroutineScope) {
@@ -580,15 +586,15 @@
 
     override val previewReferenceTimeMillis = headlessWatchFaceClient.previewReferenceTimeMillis
 
-    override val complicationState = headlessWatchFaceClient.complicationState
+    override val complicationsState = headlessWatchFaceClient.complicationsState
 
-    override fun takeWatchFaceScreenshot(
+    override fun renderWatchFaceToBitmap(
         renderParameters: RenderParameters,
         calendarTimeMillis: Long,
         idToComplicationData: Map<Int, ComplicationData>?
     ): Bitmap {
         requireNotClosed()
-        return headlessWatchFaceClient.takeWatchFaceScreenshot(
+        return headlessWatchFaceClient.renderWatchFaceToBitmap(
             renderParameters,
             calendarTimeMillis,
             userStyle,
@@ -630,10 +636,10 @@
             context,
             input.editorSession.watchFaceComponentName,
             input.complicationId,
-            input.editorSession.complicationState[input.complicationId]!!.supportedTypes,
+            input.editorSession.complicationsState[input.complicationId]!!.supportedTypes,
             input.instanceId
         )
-        val complicationState = input.editorSession.complicationState[input.complicationId]!!
+        val complicationState = input.editorSession.complicationsState[input.complicationId]!!
         intent.replaceExtras(
             Bundle(complicationState.complicationConfigExtras).apply { putAll(intent.extras!!) }
         )
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
index 66d94c9..f09e23c 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.editor
 
+import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.ComponentName
 import android.content.Context
@@ -23,10 +24,13 @@
 import android.os.Build
 import android.support.wearable.watchface.Constants
 import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.RequiresApi
 import androidx.wear.watchface.client.EditorServiceClient
 import androidx.wear.watchface.client.EditorState
 import androidx.wear.watchface.client.WatchFaceControlClient
+import androidx.wear.watchface.client.WatchFaceId
 import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleData
 
 internal const val INSTANCE_ID_KEY: String = "INSTANCE_ID_KEY"
 internal const val COMPONENT_NAME_KEY: String = "COMPONENT_NAME_KEY"
@@ -38,48 +42,75 @@
  *
  * @param watchFaceComponentName The [ComponentName] of the watch face being edited.
  * @param editorPackageName The package name of the watch face editor APK.
- * @param watchFaceInstanceId Unique ID for the instance of the watch face being edited, only
- *     defined for Android R and  beyond, it's `null` on Android P and earlier. Note each distinct
+ * @param initialUserStyle The initial [UserStyle] stored as a [UserStyleData] or `null`. Only
+ *     required for a headless [EditorSession].
+ * @param watchFaceId Unique ID for the instance of the watch face being edited, only
+ *     defined for Android R and beyond, it's `null` on Android P and earlier. Note each distinct
  *     [ComponentName] can have multiple instances.
- * @param initialUserStyle The initial [UserStyle], only required for a headless [EditorSession].
  */
-public class EditorRequest(
+public class EditorRequest @RequiresApi(Build.VERSION_CODES.R) constructor(
     public val watchFaceComponentName: ComponentName,
     public val editorPackageName: String,
-    public val watchFaceInstanceId: String?,
-    public val initialUserStyle: Map<String, String>?
+    public val initialUserStyle: UserStyleData?,
+
+    @get:RequiresApi(Build.VERSION_CODES.R)
+    @RequiresApi(Build.VERSION_CODES.R)
+    public val watchFaceId: WatchFaceId
 ) {
+    /**
+     * Constructs an [EditorRequest] without a [WatchFaceId]. This is for use pre-android R.
+     *
+     * @param watchFaceComponentName The [ComponentName] of the watch face being edited.
+     * @param editorPackageName The package name of the watch face editor APK.
+     * @param initialUserStyle The initial [UserStyle] stored as a [UserStyleData] or `null`. Only
+     *     required for a headless [EditorSession].
+     * [EditorSession].
+     */
+    @SuppressLint("NewApi")
+    public constructor(
+        watchFaceComponentName: ComponentName,
+        editorPackageName: String,
+        initialUserStyle: UserStyleData?
+    ) : this(
+        watchFaceComponentName,
+        editorPackageName,
+        initialUserStyle,
+        WatchFaceId("")
+    )
+
     public companion object {
         /**
          * Returns an [EditorRequest] saved to a [Intent] by [WatchFaceEditorContract.createIntent]
          * if there is one or `null` otherwise. Intended for use by the watch face editor activity.
          */
+        @SuppressLint("NewApi")
         @JvmStatic
         public fun createFromIntent(intent: Intent): EditorRequest? {
             val componentName =
                 intent.getParcelableExtra<ComponentName>(COMPONENT_NAME_KEY)
                     ?: intent.getParcelableExtra(Constants.EXTRA_WATCH_FACE_COMPONENT)
             val editorPackageName = intent.getPackage() ?: ""
-            val instanceId = intent.getStringExtra(INSTANCE_ID_KEY)
+            val instanceId = WatchFaceId(intent.getStringExtra(INSTANCE_ID_KEY) ?: "")
             val userStyleKey = intent.getStringArrayExtra(USER_STYLE_KEY)
-            val userStyleValue = intent.getStringArrayExtra(USER_STYLE_VALUES)
             return componentName?.let {
-                if (userStyleKey != null && userStyleValue != null &&
-                    userStyleKey.size == userStyleValue.size
-                ) {
+                if (userStyleKey != null) {
                     EditorRequest(
                         componentName,
                         editorPackageName,
-                        instanceId,
-                        HashMap<String, String>().apply {
-                            for (i in userStyleKey.indices) {
-                                put(userStyleKey[i], userStyleValue[i])
+                        UserStyleData(
+                            HashMap<String, ByteArray>().apply {
+                                for (i in userStyleKey.indices) {
+                                    val userStyleValue =
+                                        intent.getByteArrayExtra(USER_STYLE_VALUES + i)
+                                            ?: return null
+                                    put(userStyleKey[i], userStyleValue)
+                                }
                             }
-                        }
+                        ),
+                        instanceId
                     )
-                } else {
-                    EditorRequest(componentName, editorPackageName, instanceId, null)
                 }
+                EditorRequest(componentName, editorPackageName, null, instanceId)
             }
         }
     }
@@ -90,7 +121,7 @@
  * by SysUI and the normal activity result isn't used for returning [EditorState] because
  * [Activity.onStop] isn't guaranteed to be called when SysUI UX needs it to. Instead [EditorState]
  * is broadcast by the editor using[EditorSession.close], to observe these broadcasts use
- * [WatchFaceControlClient.getEditorServiceClient] and [EditorServiceClient.registerObserver].
+ * [WatchFaceControlClient.getEditorServiceClient] and [EditorServiceClient.addListener].
  */
 public open class WatchFaceEditorContract : ActivityResultContract<EditorRequest, Unit>() {
 
@@ -99,26 +130,19 @@
             "androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"
     }
 
-    // Required for testing.
-    internal open fun nullWatchFaceInstanceIdOK() =
-        Build.VERSION.SDK_INT < Build.VERSION_CODES.R
-
     override fun createIntent(
         context: Context,
         input: EditorRequest
     ): Intent {
-        require(
-            input.watchFaceInstanceId != null || nullWatchFaceInstanceIdOK()
-        ) {
-            "watchFaceInstanceId must be set from Android R and above"
-        }
         return Intent(ACTION_WATCH_FACE_EDITOR).apply {
             setPackage(input.editorPackageName)
             putExtra(COMPONENT_NAME_KEY, input.watchFaceComponentName)
-            putExtra(INSTANCE_ID_KEY, input.watchFaceInstanceId)
+            putExtra(INSTANCE_ID_KEY, input.watchFaceId.id)
             input.initialUserStyle?.let {
-                putExtra(USER_STYLE_KEY, it.keys.toTypedArray())
-                putExtra(USER_STYLE_VALUES, it.values.toTypedArray())
+                putExtra(USER_STYLE_KEY, it.userStyleMap.keys.toTypedArray())
+                for ((index, value) in it.userStyleMap.values.withIndex()) {
+                    putExtra(USER_STYLE_VALUES + index, value)
+                }
             }
         }
     }
diff --git a/wear/wear-watchface-style/api/current.txt b/wear/wear-watchface-style/api/current.txt
index 93e065c..c63feab 100644
--- a/wear/wear-watchface-style/api/current.txt
+++ b/wear/wear-watchface-style/api/current.txt
@@ -1,37 +1,43 @@
 // Signature format: 4.0
 package androidx.wear.watchface.style {
 
-  public enum Layer {
-    enum_constant public static final androidx.wear.watchface.style.Layer BASE_LAYER;
-    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
-    enum_constant public static final androidx.wear.watchface.style.Layer TOP_LAYER;
-  }
-
-  public final class UserStyle {
-    ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
-    ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    ctor public UserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
-    method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
-    method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
-    method public java.util.Map<java.lang.String,java.lang.String> toMap();
-    property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
-  }
-
-  public final class UserStyleRepository {
-    ctor public UserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
-    method @UiThread public void addUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+  public final class CurrentUserStyleRepository {
+    ctor public CurrentUserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
+    method @UiThread public void addUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
     method public androidx.wear.watchface.style.UserStyleSchema getSchema();
     method @UiThread public androidx.wear.watchface.style.UserStyle getUserStyle();
-    method @UiThread public void removeUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+    method @UiThread public void removeUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
     method @UiThread public void setUserStyle(androidx.wear.watchface.style.UserStyle style);
     property public final androidx.wear.watchface.style.UserStyleSchema schema;
     property @UiThread public final androidx.wear.watchface.style.UserStyle userStyle;
   }
 
-  public static interface UserStyleRepository.UserStyleListener {
+  public static interface CurrentUserStyleRepository.UserStyleChangeListener {
     method @UiThread public void onUserStyleChanged(androidx.wear.watchface.style.UserStyle userStyle);
   }
 
+  public enum Layer {
+    enum_constant public static final androidx.wear.watchface.style.Layer BASE;
+    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
+    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS_OVERLAY;
+  }
+
+  public final class UserStyle {
+    ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
+    ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
+    ctor public UserStyle(androidx.wear.watchface.style.UserStyleData userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
+    method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
+    method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
+    method public androidx.wear.watchface.style.UserStyleData toUserStyleData();
+    property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
+  }
+
+  public final class UserStyleData {
+    ctor public UserStyleData(java.util.Map<java.lang.String,byte[]> userStyleMap);
+    method public java.util.Map<java.lang.String,byte[]> getUserStyleMap();
+    property public final java.util.Map<java.lang.String,byte[]> userStyleMap;
+  }
+
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
     method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getUserStyleSettings();
@@ -39,28 +45,27 @@
   }
 
   public abstract sealed class UserStyleSetting {
-    method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectsLayers();
+    method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectedLayers();
     method public final androidx.wear.watchface.style.UserStyleSetting.Option getDefaultOption();
     method public final int getDefaultOptionIndex();
     method public final CharSequence getDescription();
     method public final CharSequence getDisplayName();
     method public final android.graphics.drawable.Icon? getIcon();
-    method public final String getId();
-    method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(String optionId);
+    method public final androidx.wear.watchface.style.UserStyleSetting.Id getId();
+    method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(byte[] optionId);
     method public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> getOptions();
-    property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectsLayers;
+    property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
     property public final int defaultOptionIndex;
     property public final CharSequence description;
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
-    property public final String id;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Id id;
     property public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> options;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Companion Companion;
-    field public static final int maxIdLength;
   }
 
   public static final class UserStyleSetting.BooleanUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.BooleanUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
+    ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
     method public boolean getDefaultValue();
   }
 
@@ -74,8 +79,8 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay {
@@ -96,7 +101,7 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
     method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> getComplicationOverlays();
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
@@ -106,17 +111,17 @@
   }
 
   public static final class UserStyleSetting.CustomValueUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, String defaultValue);
+    ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, byte[] defaultValue);
   }
 
   public static final class UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(String customValue);
-    method public String getCustomValue();
-    property public final String customValue;
+    ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(byte[] customValue);
+    method public byte[] getCustomValue();
+    property public final byte[] customValue;
   }
 
   public static final class UserStyleSetting.DoubleRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.DoubleRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
+    ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
     method public double getDefaultValue();
     method public double getMaximumValue();
     method public double getMinimumValue();
@@ -128,13 +133,24 @@
     property public final double value;
   }
 
+  public static final class UserStyleSetting.Id {
+    ctor public UserStyleSetting.Id(String value);
+    method public String getValue();
+    property public final String value;
+    field public static final androidx.wear.watchface.style.UserStyleSetting.Id.Companion Companion;
+    field public static final int MAX_LENGTH = 40; // 0x28
+  }
+
+  public static final class UserStyleSetting.Id.Companion {
+  }
+
   public static class UserStyleSetting.ListUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
-    ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+    ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
+    ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
   }
 
   public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon);
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
     property public final CharSequence displayName;
@@ -142,7 +158,7 @@
   }
 
   public static final class UserStyleSetting.LongRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.LongRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
+    ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
     method public long getDefaultValue();
     method public long getMaximumValue();
     method public long getMinimumValue();
@@ -155,21 +171,32 @@
   }
 
   public abstract static class UserStyleSetting.Option {
-    ctor public UserStyleSetting.Option(String id);
-    method public final String getId();
+    ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
+    method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
     method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption? toDoubleRangeOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
-    property public final String id;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
-    field public static final int maxIdLength;
   }
 
   public static final class UserStyleSetting.Option.Companion {
   }
 
+  public static final class UserStyleSetting.Option.Id {
+    ctor public UserStyleSetting.Option.Id(byte[] value);
+    ctor public UserStyleSetting.Option.Id(String value);
+    method public byte[] getValue();
+    property public final byte[] value;
+    field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Id.Companion Companion;
+    field public static final int MAX_LENGTH = 1024; // 0x400
+  }
+
+  public static final class UserStyleSetting.Option.Id.Companion {
+  }
+
 }
 
diff --git a/wear/wear-watchface-style/api/public_plus_experimental_current.txt b/wear/wear-watchface-style/api/public_plus_experimental_current.txt
index f92f80a..bd690a3 100644
--- a/wear/wear-watchface-style/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-style/api/public_plus_experimental_current.txt
@@ -1,37 +1,43 @@
 // Signature format: 4.0
 package androidx.wear.watchface.style {
 
-  public enum Layer {
-    enum_constant public static final androidx.wear.watchface.style.Layer BASE_LAYER;
-    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
-    enum_constant public static final androidx.wear.watchface.style.Layer TOP_LAYER;
-  }
-
-  public final class UserStyle {
-    ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
-    ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    ctor public UserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
-    method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
-    method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
-    method public java.util.Map<java.lang.String,java.lang.String> toMap();
-    property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
-  }
-
-  public final class UserStyleRepository {
-    ctor public UserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
-    method @UiThread public void addUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+  public final class CurrentUserStyleRepository {
+    ctor public CurrentUserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
+    method @UiThread public void addUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
     method public androidx.wear.watchface.style.UserStyleSchema getSchema();
     method @UiThread public androidx.wear.watchface.style.UserStyle getUserStyle();
-    method @UiThread public void removeUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+    method @UiThread public void removeUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
     method @UiThread public void setUserStyle(androidx.wear.watchface.style.UserStyle style);
     property public final androidx.wear.watchface.style.UserStyleSchema schema;
     property @UiThread public final androidx.wear.watchface.style.UserStyle userStyle;
   }
 
-  public static interface UserStyleRepository.UserStyleListener {
+  public static interface CurrentUserStyleRepository.UserStyleChangeListener {
     method @UiThread public void onUserStyleChanged(androidx.wear.watchface.style.UserStyle userStyle);
   }
 
+  public enum Layer {
+    enum_constant public static final androidx.wear.watchface.style.Layer BASE;
+    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
+    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS_OVERLAY;
+  }
+
+  public final class UserStyle {
+    ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
+    ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
+    ctor public UserStyle(androidx.wear.watchface.style.UserStyleData userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
+    method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
+    method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
+    method public androidx.wear.watchface.style.UserStyleData toUserStyleData();
+    property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
+  }
+
+  public final class UserStyleData {
+    ctor public UserStyleData(java.util.Map<java.lang.String,byte[]> userStyleMap);
+    method public java.util.Map<java.lang.String,byte[]> getUserStyleMap();
+    property public final java.util.Map<java.lang.String,byte[]> userStyleMap;
+  }
+
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
     method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getUserStyleSettings();
@@ -39,28 +45,27 @@
   }
 
   public abstract sealed class UserStyleSetting {
-    method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectsLayers();
+    method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectedLayers();
     method public final androidx.wear.watchface.style.UserStyleSetting.Option getDefaultOption();
     method public final int getDefaultOptionIndex();
     method public final CharSequence getDescription();
     method public final CharSequence getDisplayName();
     method public final android.graphics.drawable.Icon? getIcon();
-    method public final String getId();
-    method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(String optionId);
+    method public final androidx.wear.watchface.style.UserStyleSetting.Id getId();
+    method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(byte[] optionId);
     method public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> getOptions();
-    property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectsLayers;
+    property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
     property public final int defaultOptionIndex;
     property public final CharSequence description;
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
-    property public final String id;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Id id;
     property public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> options;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Companion Companion;
-    field public static final int maxIdLength;
   }
 
   public static final class UserStyleSetting.BooleanUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.BooleanUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
+    ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
     method public boolean getDefaultValue();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.BooleanUserStyleSettingWireFormat toWireFormat();
   }
@@ -76,8 +81,8 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ComplicationsUserStyleSettingWireFormat toWireFormat();
   }
 
@@ -99,7 +104,7 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
     method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> getComplicationOverlays();
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
@@ -110,19 +115,19 @@
   }
 
   public static final class UserStyleSetting.CustomValueUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, String defaultValue);
+    ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, byte[] defaultValue);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.CustomValueUserStyleSettingWireFormat toWireFormat();
   }
 
   public static final class UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(String customValue);
-    method public String getCustomValue();
+    ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(byte[] customValue);
+    method public byte[] getCustomValue();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.CustomValueOptionWireFormat toWireFormat();
-    property public final String customValue;
+    property public final byte[] customValue;
   }
 
   public static final class UserStyleSetting.DoubleRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.DoubleRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
+    ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
     method public double getDefaultValue();
     method public double getMaximumValue();
     method public double getMinimumValue();
@@ -136,14 +141,25 @@
     property public final double value;
   }
 
+  public static final class UserStyleSetting.Id {
+    ctor public UserStyleSetting.Id(String value);
+    method public String getValue();
+    property public final String value;
+    field public static final androidx.wear.watchface.style.UserStyleSetting.Id.Companion Companion;
+    field public static final int MAX_LENGTH = 40; // 0x28
+  }
+
+  public static final class UserStyleSetting.Id.Companion {
+  }
+
   public static class UserStyleSetting.ListUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
-    ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+    ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
+    ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ListUserStyleSettingWireFormat toWireFormat();
   }
 
   public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon);
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ListOptionWireFormat toWireFormat();
@@ -152,7 +168,7 @@
   }
 
   public static final class UserStyleSetting.LongRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.LongRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
+    ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
     method public long getDefaultValue();
     method public long getMaximumValue();
     method public long getMinimumValue();
@@ -167,21 +183,32 @@
   }
 
   public abstract static class UserStyleSetting.Option {
-    ctor public UserStyleSetting.Option(String id);
-    method public final String getId();
+    ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
+    method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
     method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption? toDoubleRangeOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
-    property public final String id;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
-    field public static final int maxIdLength;
   }
 
   public static final class UserStyleSetting.Option.Companion {
   }
 
+  public static final class UserStyleSetting.Option.Id {
+    ctor public UserStyleSetting.Option.Id(byte[] value);
+    ctor public UserStyleSetting.Option.Id(String value);
+    method public byte[] getValue();
+    property public final byte[] value;
+    field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Id.Companion Companion;
+    field public static final int MAX_LENGTH = 1024; // 0x400
+  }
+
+  public static final class UserStyleSetting.Option.Id.Companion {
+  }
+
 }
 
diff --git a/wear/wear-watchface-style/api/restricted_current.txt b/wear/wear-watchface-style/api/restricted_current.txt
index a9db649..2b3bab3 100644
--- a/wear/wear-watchface-style/api/restricted_current.txt
+++ b/wear/wear-watchface-style/api/restricted_current.txt
@@ -1,39 +1,46 @@
 // Signature format: 4.0
 package androidx.wear.watchface.style {
 
-  public enum Layer {
-    enum_constant public static final androidx.wear.watchface.style.Layer BASE_LAYER;
-    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
-    enum_constant public static final androidx.wear.watchface.style.Layer TOP_LAYER;
-  }
-
-  public final class UserStyle {
-    ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
-    ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    ctor public UserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
-    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public UserStyle(androidx.wear.watchface.style.data.UserStyleWireFormat userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
-    method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
-    method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
-    method public java.util.Map<java.lang.String,java.lang.String> toMap();
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.UserStyleWireFormat toWireFormat();
-    property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
-  }
-
-  public final class UserStyleRepository {
-    ctor public UserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
-    method @UiThread public void addUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+  public final class CurrentUserStyleRepository {
+    ctor public CurrentUserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
+    method @UiThread public void addUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
     method public androidx.wear.watchface.style.UserStyleSchema getSchema();
     method @UiThread public androidx.wear.watchface.style.UserStyle getUserStyle();
-    method @UiThread public void removeUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+    method @UiThread public void removeUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
     method @UiThread public void setUserStyle(androidx.wear.watchface.style.UserStyle style);
     property public final androidx.wear.watchface.style.UserStyleSchema schema;
     property @UiThread public final androidx.wear.watchface.style.UserStyle userStyle;
   }
 
-  public static interface UserStyleRepository.UserStyleListener {
+  public static interface CurrentUserStyleRepository.UserStyleChangeListener {
     method @UiThread public void onUserStyleChanged(androidx.wear.watchface.style.UserStyle userStyle);
   }
 
+  public enum Layer {
+    enum_constant public static final androidx.wear.watchface.style.Layer BASE;
+    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
+    enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS_OVERLAY;
+  }
+
+  public final class UserStyle {
+    ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
+    ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
+    ctor public UserStyle(androidx.wear.watchface.style.UserStyleData userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
+    method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
+    method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
+    method public androidx.wear.watchface.style.UserStyleData toUserStyleData();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.UserStyleWireFormat toWireFormat();
+    property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
+  }
+
+  public final class UserStyleData {
+    ctor public UserStyleData(java.util.Map<java.lang.String,byte[]> userStyleMap);
+    ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public UserStyleData(androidx.wear.watchface.style.data.UserStyleWireFormat userStyle);
+    method public java.util.Map<java.lang.String,byte[]> getUserStyleMap();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.UserStyleWireFormat toWireFormat();
+    property public final java.util.Map<java.lang.String,byte[]> userStyleMap;
+  }
+
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
     ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public UserStyleSchema(androidx.wear.watchface.style.data.UserStyleSchemaWireFormat wireFormat);
@@ -43,30 +50,29 @@
   }
 
   public abstract sealed class UserStyleSetting {
-    method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectsLayers();
+    method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectedLayers();
     method public final androidx.wear.watchface.style.UserStyleSetting.Option getDefaultOption();
     method public final int getDefaultOptionIndex();
     method public final CharSequence getDescription();
     method public final CharSequence getDisplayName();
     method public final android.graphics.drawable.Icon? getIcon();
-    method public final String getId();
-    method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(String optionId);
+    method public final androidx.wear.watchface.style.UserStyleSetting.Id getId();
+    method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(byte[] optionId);
     method public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> getOptions();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final java.util.List<androidx.wear.watchface.style.data.OptionWireFormat> getWireFormatOptionsList();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract androidx.wear.watchface.style.data.UserStyleSettingWireFormat toWireFormat();
-    property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectsLayers;
+    property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
     property public final int defaultOptionIndex;
     property public final CharSequence description;
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
-    property public final String id;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Id id;
     property public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> options;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Companion Companion;
-    field public static final int maxIdLength;
   }
 
   public static final class UserStyleSetting.BooleanUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.BooleanUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
+    ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
     method public boolean getDefaultValue();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.BooleanUserStyleSettingWireFormat toWireFormat();
   }
@@ -82,8 +88,8 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ComplicationsUserStyleSettingWireFormat toWireFormat();
   }
 
@@ -105,7 +111,7 @@
   }
 
   public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
+    ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
     method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> getComplicationOverlays();
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
@@ -116,19 +122,19 @@
   }
 
   public static final class UserStyleSetting.CustomValueUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, String defaultValue);
+    ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, byte[] defaultValue);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.CustomValueUserStyleSettingWireFormat toWireFormat();
   }
 
   public static final class UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(String customValue);
-    method public String getCustomValue();
+    ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(byte[] customValue);
+    method public byte[] getCustomValue();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.CustomValueOptionWireFormat toWireFormat();
-    property public final String customValue;
+    property public final byte[] customValue;
   }
 
   public static final class UserStyleSetting.DoubleRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.DoubleRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
+    ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
     method public double getDefaultValue();
     method public double getMaximumValue();
     method public double getMinimumValue();
@@ -142,14 +148,25 @@
     property public final double value;
   }
 
+  public static final class UserStyleSetting.Id {
+    ctor public UserStyleSetting.Id(String value);
+    method public String getValue();
+    property public final String value;
+    field public static final androidx.wear.watchface.style.UserStyleSetting.Id.Companion Companion;
+    field public static final int MAX_LENGTH = 40; // 0x28
+  }
+
+  public static final class UserStyleSetting.Id.Companion {
+  }
+
   public static class UserStyleSetting.ListUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
-    ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+    ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
+    ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ListUserStyleSettingWireFormat toWireFormat();
   }
 
   public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon);
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ListOptionWireFormat toWireFormat();
@@ -158,7 +175,7 @@
   }
 
   public static final class UserStyleSetting.LongRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
-    ctor public UserStyleSetting.LongRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
+    ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
     method public long getDefaultValue();
     method public long getMaximumValue();
     method public long getMinimumValue();
@@ -173,8 +190,8 @@
   }
 
   public abstract static class UserStyleSetting.Option {
-    ctor public UserStyleSetting.Option(String id);
-    method public final String getId();
+    ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
+    method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
     method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
@@ -182,14 +199,25 @@
     method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
     method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract androidx.wear.watchface.style.data.OptionWireFormat toWireFormat();
-    property public final String id;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
-    field public static final int maxIdLength;
   }
 
   public static final class UserStyleSetting.Option.Companion {
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.UserStyleSetting.Option createFromWireFormat(androidx.wear.watchface.style.data.OptionWireFormat wireFormat);
   }
 
+  public static final class UserStyleSetting.Option.Id {
+    ctor public UserStyleSetting.Option.Id(byte[] value);
+    ctor public UserStyleSetting.Option.Id(String value);
+    method public byte[] getValue();
+    property public final byte[] value;
+    field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Id.Companion Companion;
+    field public static final int MAX_LENGTH = 1024; // 0x400
+  }
+
+  public static final class UserStyleSetting.Option.Id.Companion {
+  }
+
 }
 
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
similarity index 64%
rename from wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
rename to wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index ba933ce7..99f5ee2 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -24,7 +24,9 @@
 
 /**
  * The users style choices represented as a map of [UserStyleSetting] to
- * [UserStyleSetting.Option].
+ * [UserStyleSetting.Option]. This is intended for use by the WatchFace and the [selectedOptions]
+ * map keys are the same objects as in the [UserStyleSchema]. This means you can't serialize a
+ * UserStyle directly, instead you need to use a [UserStyleData] (see [toUserStyleData]).
  *
  * @param selectedOptions The [UserStyleSetting.Option] selected for each [UserStyleSetting]
  */
@@ -39,22 +41,21 @@
     public constructor(userStyle: UserStyle) : this(HashMap(userStyle.selectedOptions))
 
     /**
-     * Constructs a [UserStyle] from a Map<String, String> and the [UserStyleSchema]. Unrecognized
+     * Constructs a [UserStyle] from a [UserStyleData] and the [UserStyleSchema]. Unrecognized
      * style settings will be ignored. Unlisted style settings will be initialized with that
      * settings default option.
      *
-     * @param userStyle The [UserStyle] represented as a Map<String, String> of
-     *     [UserStyleSetting.id] to [UserStyleSetting.Option.id]
-     * @param styleSchema The [UserStyleSchema] for this UserStyle, describes how we interpret
+     * @param userStyle The [UserStyle] represented as a [UserStyleData].
+     * @param styleSchema The  for this UserStyle, describes how we interpret
      *     [userStyle].
      */
     public constructor(
-        userStyle: Map<String, String>,
+        userStyle: UserStyleData,
         styleSchema: UserStyleSchema
     ) : this(
         HashMap<UserStyleSetting, UserStyleSetting.Option>().apply {
             for (styleSetting in styleSchema.userStyleSettings) {
-                val option = userStyle[styleSetting.id]
+                val option = userStyle.userStyleMap[styleSetting.id.value]
                 if (option != null) {
                     this[styleSetting] = styleSetting.getSettingOptionForId(option)
                 } else {
@@ -66,19 +67,14 @@
 
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    public constructor(
-        userStyle: UserStyleWireFormat,
-        styleSchema: UserStyleSchema
-    ) : this(userStyle.mUserStyle, styleSchema)
+    public fun toWireFormat(): UserStyleWireFormat = UserStyleWireFormat(toMap())
 
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    public fun toWireFormat(): UserStyleWireFormat =
-        UserStyleWireFormat(toMap())
+    /** Returns the style as a [UserStyleData]. */
+    public fun toUserStyleData(): UserStyleData = UserStyleData(toMap())
 
-    /** Returns the style as a Map<String, String>. */
-    public fun toMap(): Map<String, String> =
-        selectedOptions.entries.associate { it.key.id to it.value.id }
+    /** Returns the style as a [Map]<[String], [ByteArray]>. */
+    private fun toMap(): Map<String, ByteArray> =
+        selectedOptions.entries.associate { it.key.id.value to it.value.id.value }
 
     /** Returns the [UserStyleSetting.Option] for [setting] if there is one or `null` otherwise. */
     public operator fun get(setting: UserStyleSetting): UserStyleSetting.Option? =
@@ -86,11 +82,37 @@
 
     override fun toString(): String =
         "[" + selectedOptions.entries.joinToString(
-            transform = { it.key.id + " -> " + it.value.id }
+            transform = { it.key.id.value + " -> " + it.value.id.value }
         ) + "]"
 }
 
 /**
+ * A form of [UserStyle] which is easy to serialize. This is intended for use by the watch face
+ * clients and the editor where we can't practically use [UserStyle] due to it's limitations.
+ */
+public class UserStyleData(
+    public val userStyleMap: Map<String, ByteArray>
+) {
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public constructor(
+        userStyle: UserStyleWireFormat
+    ) : this(userStyle.mUserStyle)
+
+    override fun toString(): String = "{" + userStyleMap.map {
+        try {
+            it.key + "=" + it.value.decodeToString()
+        } catch (e: Exception) {
+            it.key + "=" + it.value
+        }
+    }.joinToString() + "}"
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public fun toWireFormat(): UserStyleWireFormat = UserStyleWireFormat(userStyleMap)
+}
+
+/**
  * Describes the list of [UserStyleSetting]s the user can configure.
  *
  * @param userStyleSettings The user configurable style categories associated with this watch
@@ -131,30 +153,30 @@
 }
 
 /**
- * An in memory storage for user style choices represented as [UserStyle], listeners can be
- * registered to observe style changes. The UserStyleRepository is initialized with a
+ * In memory storage for the current user style choices represented as [UserStyle], listeners can be
+ * registered to observe style changes. The CurrentUserStyleRepository is initialized with a
  * [UserStyleSchema].
  *
- * @param schema The [UserStyleSchema] for this UserStyleRepository which describes the available
- *     style categories.
+ * @param schema The [UserStyleSchema] for this CurrentUserStyleRepository which describes the
+ *     available style categories.
  */
-public class UserStyleRepository(
+public class CurrentUserStyleRepository(
     public val schema: UserStyleSchema
 ) {
     /** A listener for observing [UserStyle] changes. */
-    public interface UserStyleListener {
+    public interface UserStyleChangeListener {
         /** Called whenever the [UserStyle] changes. */
         @UiThread
         public fun onUserStyleChanged(userStyle: UserStyle)
     }
 
-    private val styleListeners = HashSet<UserStyleListener>()
+    private val styleListeners = HashSet<UserStyleChangeListener>()
 
-    private val idToStyleSetting = schema.userStyleSettings.associateBy { it.id }
+    private val idToStyleSetting = schema.userStyleSettings.associateBy { it.id.value }
 
     /**
-     * The current [UserStyle]. Assigning to this property triggers immediate [UserStyleListener]
-     * callbacks if if any options have changed.
+     * The current [UserStyle]. Assigning to this property triggers immediate
+     * [UserStyleChangeListener] callbacks if if any options have changed.
      */
     public var userStyle: UserStyle = UserStyle(
         HashMap<UserStyleSetting, UserStyleSetting.Option>().apply {
@@ -172,9 +194,9 @@
                 field.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
             for ((setting, option) in style.selectedOptions) {
                 // Ignore an unrecognized setting.
-                val localSetting = idToStyleSetting[setting.id] ?: continue
+                val localSetting = idToStyleSetting[setting.id.value] ?: continue
                 val styleSetting = field.selectedOptions[localSetting] ?: continue
-                if (styleSetting.id != option.id) {
+                if (styleSetting.id.value != option.id.value) {
                     changed = true
                 }
                 hashmap[localSetting] = option
@@ -190,19 +212,19 @@
         }
 
     /**
-     * Adds a [UserStyleListener] which is called immediately and whenever the style changes.
+     * Adds a [UserStyleChangeListener] which is called immediately and whenever the style changes.
      */
     @UiThread
     @SuppressLint("ExecutorRegistration")
-    public fun addUserStyleListener(userStyleListener: UserStyleListener) {
-        styleListeners.add(userStyleListener)
-        userStyleListener.onUserStyleChanged(userStyle)
+    public fun addUserStyleChangeListener(userStyleChangeListener: UserStyleChangeListener) {
+        styleListeners.add(userStyleChangeListener)
+        userStyleChangeListener.onUserStyleChanged(userStyle)
     }
 
-    /** Removes a [UserStyleListener] previously added by [addUserStyleListener]. */
+    /** Removes a [UserStyleChangeListener] previously added by [addUserStyleChangeListener]. */
     @UiThread
     @SuppressLint("ExecutorRegistration")
-    public fun removeUserStyleListener(userStyleListener: UserStyleListener) {
-        styleListeners.remove(userStyleListener)
+    public fun removeUserStyleChangeListener(userStyleChangeListener: UserStyleChangeListener) {
+        styleListeners.remove(userStyleChangeListener)
     }
 }
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/Layer.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/Layer.kt
index a23719d..6e378ed 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/Layer.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/Layer.kt
@@ -18,7 +18,12 @@
 
 /** Describes part of watchface. Used as a parameter for rendering. */
 public enum class Layer {
-    BASE_LAYER,
+    /** The watch excluding complications and anything that may render on top of complications. */
+    BASE,
+
+    /** The watch face complications. */
     COMPLICATIONS,
-    TOP_LAYER
+
+    /** Anything that may render on top of complications, e.g. watch hands. */
+    COMPLICATIONS_OVERLAY
 }
\ No newline at end of file
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index f37b3f8..9fb5fac 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -19,11 +19,8 @@
 import android.graphics.drawable.Icon
 import androidx.annotation.RestrictTo
 import androidx.wear.complications.ComplicationBounds
-import androidx.wear.watchface.style.UserStyleSetting.Companion.maxIdLength
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption
-import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption
-import androidx.wear.watchface.style.UserStyleSetting.Option.Companion.maxIdLength
 import androidx.wear.watchface.style.data.BooleanOptionWireFormat
 import androidx.wear.watchface.style.data.BooleanUserStyleSettingWireFormat
 import androidx.wear.watchface.style.data.ComplicationOverlayWireFormat
@@ -39,6 +36,7 @@
 import androidx.wear.watchface.style.data.LongRangeUserStyleSettingWireFormat
 import androidx.wear.watchface.style.data.OptionWireFormat
 import androidx.wear.watchface.style.data.UserStyleSettingWireFormat
+import java.nio.ByteBuffer
 import java.security.InvalidParameterException
 
 /**
@@ -65,23 +63,39 @@
  *     Companion).
  * @param defaultOptionIndex The default option index, used if nothing has been selected within the
  *     [options] list.
- * @param affectsLayers Used by the style configuration UI. Describes which rendering layers this
+ * @param affectedLayers Used by the style configuration UI. Describes which rendering layers this
  *     style affects.
  */
 public sealed class UserStyleSetting(
-    public val id: String,
+    public val id: Id,
     public val displayName: CharSequence,
     public val description: CharSequence,
     public val icon: Icon?,
     public val options: List<Option>,
     public val defaultOptionIndex: Int,
-    public val affectsLayers: Collection<Layer>
+    public val affectedLayers: Collection<Layer>
 ) {
-    public companion object {
-        /** Maximum length of the [id] field. */
-        @JvmField
-        public val maxIdLength: Int = 40
+    /**
+     * Machine readable identifier for [UserStyleSetting]s. The length of this identifier may not
+     * exceed [MAX_LENGTH].
+     */
+    public class Id(public val value: String) {
+        public companion object {
+            /** Maximum length of the [value] field. */
+            public const val MAX_LENGTH: Int = 40
+        }
 
+        init {
+            require(value.length <= MAX_LENGTH) {
+                "UserStyleSetting.value.length (${value.length}) must be less than MAX_LENGTH " +
+                    "($MAX_LENGTH)"
+            }
+        }
+
+        override fun toString(): String = value
+    }
+
+    public companion object {
         internal fun createFromWireFormat(
             wireFormat: UserStyleSettingWireFormat
         ): UserStyleSetting = when (wireFormat) {
@@ -108,13 +122,9 @@
         require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
             "defaultOptionIndex must be in the range [0 .. options.size)"
         }
-
-        require(id.length <= maxIdLength) {
-            "UserStyleSetting id length must not exceed $maxIdLength"
-        }
     }
 
-    internal fun getSettingOptionForId(id: String?) =
+    internal fun getSettingOptionForId(id: ByteArray?) =
         if (id == null) {
             options[defaultOptionIndex]
         } else {
@@ -122,7 +132,7 @@
         }
 
     private constructor(wireFormat: UserStyleSettingWireFormat) : this(
-        wireFormat.mId,
+        Id(wireFormat.mId),
         wireFormat.mDisplayName,
         wireFormat.mDescription,
         wireFormat.mIcon,
@@ -143,29 +153,50 @@
     /** Returns the default for when the user hasn't selected an option. */
     public fun getDefaultOption(): Option = options[defaultOptionIndex]
 
-    override fun toString(): String = "{$id : " + options.joinToString(transform = { it.id }) + "}"
+    override fun toString(): String = "{${id.value} : " +
+        options.joinToString(transform = { it.toString() }) + "}"
 
     /**
      * Represents a choice within a style setting which can either be an option from the list or a
      * an arbitrary value depending on the nature of the style setting.
      *
-     * @property id Machine readable identifier for the style setting. Identifier for the option
-     *     (or the option itself for [CustomValueUserStyleSetting.CustomValueOption]), must be
-     *     unique within the UserStyleSetting. Short ids are encouraged. There is a maximum
-     *     length see [maxIdLength].
+     * @property id Machine readable [Id] for the style setting. Identifier for the option (or the
+     *     option itself for [CustomValueUserStyleSetting.CustomValueOption]), must be unique
+     *     within the UserStyleSetting. Short ids are encouraged.
      */
-    public abstract class Option(public val id: String) {
-        init {
-            require(id.length <= maxIdLength) {
-                "UserStyleSetting.Option id length must not exceed $maxIdLength"
+    public abstract class Option(public val id: Id) {
+        /**
+         * Machine readable identifier for [Option]s. The length of this identifier may not exceed
+         * [MAX_LENGTH].
+         */
+        public class Id(public val value: ByteArray) {
+            /**
+             * Constructs an [Id] with a [String] encoded to a [ByteArray] by
+             * [String.encodeToByteArray].
+             */
+            public constructor(value: String) : this(value.encodeToByteArray())
+
+            public companion object {
+                /** Maximum length of the [value] field. */
+                public const val MAX_LENGTH: Int = 1024
             }
+
+            init {
+                require(value.size <= MAX_LENGTH) {
+                    "Option.Id.value.size (${value.size}) must be less than MAX_LENGTH " +
+                        "($MAX_LENGTH)"
+                }
+            }
+
+            override fun toString(): String =
+                try {
+                    value.decodeToString()
+                } catch (e: Exception) {
+                    value.toString()
+                }
         }
 
         public companion object {
-            /** Maximum length of the [id] field. */
-            @JvmField
-            public val maxIdLength: Int = 1024
-
             /** @hide */
             @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
             public fun createFromWireFormat(
@@ -243,6 +274,13 @@
             } else {
                 null
             }
+
+        override fun toString(): String =
+            try {
+                id.value.decodeToString()
+            } catch (e: Exception) {
+                id.value.toString()
+            }
     }
 
     /**
@@ -255,8 +293,8 @@
      *     of the UserStyleSetting. If optionName is unrecognized then the default value for the
      *     setting should be returned.
      */
-    public open fun getOptionForId(optionId: String): Option =
-        options.find { it.id == optionId } ?: options[defaultOptionIndex]
+    public open fun getOptionForId(optionId: ByteArray): Option =
+        options.find { it.id.value.contentEquals(optionId) } ?: options[defaultOptionIndex]
 
     /** A BooleanUserStyleSetting represents a setting with a true and a false setting. */
     public class BooleanUserStyleSetting : UserStyleSetting {
@@ -264,7 +302,7 @@
         /**
          * Constructs a [BooleanUserStyleSetting].
          *
-         * @param id Identifier for the element, must be unique.
+         * @param id [Id] for the element, must be unique.
          * @param displayName Localized human readable name for the element, used in the userStyle
          *     selection UI.
          * @param description Localized description string displayed under the displayName.
@@ -274,7 +312,7 @@
          * @param defaultValue The default value for this BooleanUserStyleSetting.
          */
         public constructor (
-            id: String,
+            id: Id,
             displayName: CharSequence,
             description: CharSequence,
             icon: Icon?,
@@ -299,13 +337,13 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         override fun toWireFormat(): BooleanUserStyleSettingWireFormat =
             BooleanUserStyleSettingWireFormat(
-                id,
+                id.value,
                 displayName,
                 description,
                 icon,
                 getWireFormatOptionsList(),
                 defaultOptionIndex,
-                affectsLayers.map { it.ordinal }
+                affectedLayers.map { it.ordinal }
             )
 
         /** Returns the default value. */
@@ -315,20 +353,24 @@
         public class BooleanOption : Option {
             public val value: Boolean
 
-            public constructor(value: Boolean) : super(value.toString()) {
+            public constructor(value: Boolean) : super(
+                Id(ByteArray(1).apply { this[0] = if (value) 1 else 0 })
+            ) {
                 this.value = value
             }
 
             internal constructor(
                 wireFormat: BooleanOptionWireFormat
-            ) : super(wireFormat.mId) {
-                value = wireFormat.mValue
+            ) : super(Id(wireFormat.mId)) {
+                value = wireFormat.mId[0] == 1.toByte()
             }
 
             /** @hide */
             @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
             override fun toWireFormat(): BooleanOptionWireFormat =
-                BooleanOptionWireFormat(id, value)
+                BooleanOptionWireFormat(id.value)
+
+            override fun toString(): String = if (id.value[0] == 1.toByte()) "true" else "false"
         }
     }
 
@@ -352,7 +394,7 @@
          * Overrides to be applied to the corresponding complication's initial config (as specified
          * in [androidx.wear.watchface.Complication]) when the setting is selected.
          *
-         * @param complicationId The id of the complication to configure.
+         * @param complicationId The [Id] of the complication to configure.
          * @param enabled If non null, whether the complication should be enabled for this
          *     configuration. If null then no changes are made.
          * @param complicationBounds If non null, the new [ComplicationBounds] for this
@@ -395,12 +437,9 @@
             ) : this(
                 wireFormat.mComplicationId,
                 when (wireFormat.mEnabled) {
-                    ComplicationOverlayWireFormat
-                        .ENABLED_UNKNOWN -> null
-                    ComplicationOverlayWireFormat
-                        .ENABLED_YES -> true
-                    ComplicationOverlayWireFormat
-                        .ENABLED_NO -> false
+                    ComplicationOverlayWireFormat.ENABLED_UNKNOWN -> null
+                    ComplicationOverlayWireFormat.ENABLED_YES -> true
+                    ComplicationOverlayWireFormat.ENABLED_NO -> false
                     else -> throw InvalidParameterException(
                         "Unrecognised wireFormat.mEnabled " + wireFormat.mEnabled
                     )
@@ -419,7 +458,7 @@
         /**
          * Constructs a [ComplicationsUserStyleSetting].
          *
-         * @param id Identifier for the element, must be unique.
+         * @param id [Id] for the element, must be unique.
          * @param displayName Localized human readable name for the element, used in the userStyle
          *     selection UI.
          * @param description Localized description string displayed under the displayName.
@@ -432,7 +471,7 @@
          */
         @JvmOverloads
         public constructor (
-            id: String,
+            id: Id,
             displayName: CharSequence,
             description: CharSequence,
             icon: Icon?,
@@ -459,13 +498,13 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         override fun toWireFormat(): ComplicationsUserStyleSettingWireFormat =
             ComplicationsUserStyleSettingWireFormat(
-                id,
+                id.value,
                 displayName,
                 description,
                 icon,
                 getWireFormatOptionsList(),
                 defaultOptionIndex,
-                affectsLayers.map { it.ordinal }
+                affectedLayers.map { it.ordinal }
             )
 
         /** Represents an override to the initial complication configuration. */
@@ -485,7 +524,7 @@
             /**
              * Constructs a [ComplicationsUserStyleSetting].
              *
-             * @param id Identifier for the element, must be unique.
+             * @param id [Id] for the element, must be unique.
              * @param displayName Localized human readable name for the element, used in the
              *     userStyle selection UI.
              * @param icon [Icon] for use in the style selection UI.
@@ -494,7 +533,7 @@
              *     configuration.
              */
             public constructor(
-                id: String,
+                id: Id,
                 displayName: CharSequence,
                 icon: Icon?,
                 complicationOverlays: Collection<ComplicationOverlay>
@@ -506,7 +545,7 @@
 
             internal constructor(
                 wireFormat: ComplicationsOptionWireFormat
-            ) : super(wireFormat.mId) {
+            ) : super(Id(wireFormat.mId)) {
                 complicationOverlays =
                     wireFormat.mComplicationOverlays.map { ComplicationOverlay(it) }
                 displayName = wireFormat.mDisplayName
@@ -518,7 +557,7 @@
             override fun toWireFormat():
                 ComplicationsOptionWireFormat =
                     ComplicationsOptionWireFormat(
-                        id,
+                        id.value,
                         displayName,
                         icon,
                         complicationOverlays.map { it.toWireFormat() }.toTypedArray()
@@ -557,7 +596,7 @@
         /**
          * Constructs a [DoubleRangeUserStyleSetting].
          *
-         * @param id Identifier for the element, must be unique.
+         * @param id [Id] for the element, must be unique.
          * @param displayName Localized human readable name for the element, used in the
          *     userStyle selection UI.
          * @param description Localized description string displayed under the displayName.
@@ -569,7 +608,7 @@
          * @param defaultValue The default value for this DoubleRangeUserStyleSetting.
          */
         public constructor (
-            id: String,
+            id: Id,
             displayName: CharSequence,
             description: CharSequence,
             icon: Icon?,
@@ -597,13 +636,13 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         override fun toWireFormat(): DoubleRangeUserStyleSettingWireFormat =
             DoubleRangeUserStyleSettingWireFormat(
-                id,
+                id.value,
                 displayName,
                 description,
                 icon,
                 getWireFormatOptionsList(),
                 defaultOptionIndex,
-                affectsLayers.map { it.ordinal }
+                affectedLayers.map { it.ordinal }
             )
 
         /** Represents an option as a [Double] in the range [minimumValue .. maximumValue]. */
@@ -616,23 +655,24 @@
              *
              * @param value The value of this [DoubleRangeOption]
              */
-            public constructor(value: Double) : super(value.toString()) {
+            public constructor(value: Double) : super(
+                Id(ByteArray(8).apply { ByteBuffer.wrap(this).putDouble(value) })
+            ) {
                 this.value = value
             }
 
             internal constructor(
                 wireFormat: DoubleRangeOptionWireFormat
-            ) : super(wireFormat.mId) {
-                value = wireFormat.mValue
+            ) : super(Id(wireFormat.mId)) {
+                value = ByteBuffer.wrap(wireFormat.mId).double
             }
 
             /** @hide */
             @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
             override fun toWireFormat(): DoubleRangeOptionWireFormat =
-                DoubleRangeOptionWireFormat(
-                    id,
-                    value
-                )
+                DoubleRangeOptionWireFormat(id.value)
+
+            override fun toString(): String = value.toString()
         }
 
         /** Returns the minimum value. */
@@ -646,18 +686,18 @@
             (options[defaultOptionIndex] as DoubleRangeOption).value
 
         /** We support all values in the range [min ... max] not just min & max. */
-        override fun getOptionForId(optionId: String): Option =
-            options.find { it.id == optionId } ?: checkedOptionForId(optionId)
+        override fun getOptionForId(optionId: ByteArray): Option =
+            options.find { it.id.value.contentEquals(optionId) } ?: checkedOptionForId(optionId)
 
-        private fun checkedOptionForId(optionId: String): DoubleRangeOption {
+        private fun checkedOptionForId(optionId: ByteArray): DoubleRangeOption {
             return try {
-                val value = optionId.toDouble()
+                val value = ByteBuffer.wrap(optionId).double
                 if (value < getMinimumValue() || value > getMaximumValue()) {
                     options[defaultOptionIndex] as DoubleRangeOption
                 } else {
                     DoubleRangeOption(value)
                 }
-            } catch (e: NumberFormatException) {
+            } catch (e: Exception) {
                 options[defaultOptionIndex] as DoubleRangeOption
             }
         }
@@ -669,7 +709,7 @@
         /**
          * Constructs a [ListUserStyleSetting].
          *
-         * @param id Identifier for the element, must be unique.
+         * @param id [Id] for the element, must be unique.
          * @param displayName Localized human readable name for the element, used in the userStyle
          *     selection UI.
          * @param description Localized description string displayed under the displayName.
@@ -681,7 +721,7 @@
          */
         @JvmOverloads
         public constructor (
-            id: String,
+            id: Id,
             displayName: CharSequence,
             description: CharSequence,
             icon: Icon?,
@@ -704,13 +744,13 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         override fun toWireFormat(): ListUserStyleSettingWireFormat =
             ListUserStyleSettingWireFormat(
-                id,
+                id.value,
                 displayName,
                 description,
                 icon,
                 getWireFormatOptionsList(),
                 defaultOptionIndex,
-                affectsLayers.map { it.ordinal }
+                affectedLayers.map { it.ordinal }
             )
 
         /**
@@ -726,20 +766,20 @@
             /**
              * Constructs a [ListOption].
              *
-             * @param id The id of this [ListOption], must be unique within the
+             * @param id The [Id] of this [ListOption], must be unique within the
              *     [ListUserStyleSetting].
              * @param displayName Localized human readable name for the setting, used in the style
              *     selection UI.
              * @param icon [Icon] for use in the style selection UI.
              */
-            public constructor(id: String, displayName: CharSequence, icon: Icon?) : super(id) {
+            public constructor(id: Id, displayName: CharSequence, icon: Icon?) : super(id) {
                 this.displayName = displayName
                 this.icon = icon
             }
 
             internal constructor(
                 wireFormat: ListOptionWireFormat
-            ) : super(wireFormat.mId) {
+            ) : super(Id(wireFormat.mId)) {
                 displayName = wireFormat.mDisplayName
                 icon = wireFormat.mIcon
             }
@@ -748,7 +788,7 @@
             @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
             override fun toWireFormat(): ListOptionWireFormat =
                 ListOptionWireFormat(
-                    id,
+                    id.value,
                     displayName,
                     icon
                 )
@@ -789,7 +829,7 @@
         /**
          * Constructs a [LongRangeUserStyleSetting].
          *
-         * @param id Identifier for the element, must be unique.
+         * @param id [Id] for the element, must be unique.
          * @param displayName Localized human readable name for the element, used in the userStyle
          *     selection UI.
          * @param description Localized description string displayed under the displayName.
@@ -801,7 +841,7 @@
          * @param defaultValue The default value for this LongRangeUserStyleSetting.
          */
         public constructor (
-            id: String,
+            id: Id,
             displayName: CharSequence,
             description: CharSequence,
             icon: Icon?,
@@ -829,13 +869,13 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         override fun toWireFormat(): LongRangeUserStyleSettingWireFormat =
             LongRangeUserStyleSettingWireFormat(
-                id,
+                id.value,
                 displayName,
                 description,
                 icon,
                 getWireFormatOptionsList(),
                 defaultOptionIndex,
-                affectsLayers.map { it.ordinal }
+                affectedLayers.map { it.ordinal }
             )
 
         /**
@@ -850,23 +890,24 @@
              *
              * @param value The value of this [LongRangeOption]
              */
-            public constructor(value: Long) : super(value.toString()) {
+            public constructor(value: Long) : super(
+                Id(ByteArray(8).apply { ByteBuffer.wrap(this).putLong(value) })
+            ) {
                 this.value = value
             }
 
             internal constructor(
                 wireFormat: LongRangeOptionWireFormat
-            ) : super(wireFormat.mId) {
-                value = wireFormat.mValue
+            ) : super(Id(wireFormat.mId)) {
+                value = ByteBuffer.wrap(wireFormat.mId).long
             }
 
             /** @hide */
             @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
             override fun toWireFormat(): LongRangeOptionWireFormat =
-                LongRangeOptionWireFormat(
-                    id,
-                    value
-                )
+                LongRangeOptionWireFormat(id.value)
+
+            override fun toString(): String = value.toString()
         }
 
         /**
@@ -887,18 +928,18 @@
         /**
          * We support all values in the range [min ... max] not just min & max.
          */
-        override fun getOptionForId(optionId: String): Option =
-            options.find { it.id == optionId } ?: checkedOptionForId(optionId)
+        override fun getOptionForId(optionId: ByteArray): Option =
+            options.find { it.id.value.contentEquals(optionId) } ?: checkedOptionForId(optionId)
 
-        private fun checkedOptionForId(optionId: String): LongRangeOption {
+        private fun checkedOptionForId(optionId: ByteArray): LongRangeOption {
             return try {
-                val value = optionId.toLong()
+                val value = ByteBuffer.wrap(optionId).long
                 if (value < getMinimumValue() || value > getMaximumValue()) {
                     options[defaultOptionIndex] as LongRangeOption
                 } else {
                     LongRangeOption(value)
                 }
-            } catch (e: NumberFormatException) {
+            } catch (e: Exception) {
                 options[defaultOptionIndex] as LongRangeOption
             }
         }
@@ -918,13 +959,13 @@
          *
          * @param affectsLayers Used by the style configuration UI. Describes which rendering layers
          *     this style affects.
-         * @param defaultValue The default value.
+         * @param defaultValue The default value [ByteArray].
          */
         public constructor (
             affectsLayers: Collection<Layer>,
-            defaultValue: String
+            defaultValue: ByteArray
         ) : super(
-            CUSTOM_VALUE_USER_STYLE_SETTING_ID,
+            Id(CUSTOM_VALUE_USER_STYLE_SETTING_ID),
             "",
             "",
             null,
@@ -939,12 +980,12 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         override fun toWireFormat(): CustomValueUserStyleSettingWireFormat =
             CustomValueUserStyleSettingWireFormat(
-                id,
+                id.value,
                 displayName,
                 description,
                 icon,
                 getWireFormatOptionsList(),
-                affectsLayers.map { it.ordinal }
+                affectedLayers.map { it.ordinal }
             )
 
         /**
@@ -952,28 +993,29 @@
          * same as the [CustomValueOption.id].
          */
         public class CustomValueOption : Option {
-            /* The value for this option which is the same as the [id]. */
-            public val customValue: String
-                get() = id
+            /* The [ByteArray] value for this option which is the same as the [id]. */
+            public val customValue: ByteArray
+                get() = id.value
 
             /**
              * Constructs a [CustomValueOption].
              *
-             * @param customValue The [id] and value of this [CustomValueOption].
+             * @param customValue The [ByteArray] [id] and value of this [CustomValueOption]. This
+             *     may not exceed [Id.MAX_LENGTH].
              */
-            public constructor(customValue: String) : super(customValue)
+            public constructor(customValue: ByteArray) : super(Id(customValue))
 
             internal constructor(
                 wireFormat: CustomValueOptionWireFormat
-            ) : super(wireFormat.mId)
+            ) : super(Id(wireFormat.mId))
 
             /** @hide */
             @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
             override fun toWireFormat(): CustomValueOptionWireFormat =
-                CustomValueOptionWireFormat(id)
+                CustomValueOptionWireFormat(id.value)
         }
 
-        override fun getOptionForId(optionId: String): Option =
-            options.find { it.id == optionId } ?: CustomValueOption(optionId)
+        override fun getOptionForId(optionId: ByteArray): Option =
+            options.find { it.id.value.contentEquals(optionId) } ?: CustomValueOption(optionId)
     }
 }
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
similarity index 61%
rename from wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
rename to wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index 3efa3ea..8d88841 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -19,6 +19,7 @@
 import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.fail
 import org.junit.Test
@@ -26,81 +27,84 @@
 import org.mockito.Mockito
 
 @RunWith(StyleTestRunner::class)
-class UserStyleRepositoryTest {
+public class CurrentUserStyleRepositoryTest {
     private val redStyleOption =
-        ListUserStyleSetting.ListOption("red_style", "Red", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
 
     private val greenStyleOption =
-        ListUserStyleSetting.ListOption("green_style", "Green", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
 
     private val blueStyleOption =
-        ListUserStyleSetting.ListOption("bluestyle", "Blue", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("bluestyle"), "Blue", icon = null)
 
     private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
 
     private val colorStyleSetting = ListUserStyleSetting(
-        "color_style_setting",
+        UserStyleSetting.Id("color_style_setting"),
         "Colors",
         "Watchface colorization", /* icon = */
         null,
         colorStyleList,
-        listOf(Layer.BASE_LAYER)
+        listOf(Layer.BASE)
     )
 
     private val classicStyleOption =
-        ListUserStyleSetting.ListOption("classic_style", "Classic", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
 
     private val modernStyleOption =
-        ListUserStyleSetting.ListOption("modern_style", "Modern", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
 
     private val gothicStyleOption =
-        ListUserStyleSetting.ListOption("gothic_style", "Gothic", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
 
     private val watchHandStyleList =
         listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
 
     private val watchHandStyleSetting = ListUserStyleSetting(
-        "hand_style_setting",
+        UserStyleSetting.Id("hand_style_setting"),
         "Hand Style",
         "Hand visual look", /* icon = */
         null,
         watchHandStyleList,
-        listOf(Layer.TOP_LAYER)
+        listOf(Layer.COMPLICATIONS_OVERLAY)
     )
     private val watchHandLengthStyleSetting =
         DoubleRangeUserStyleSetting(
-            "watch_hand_length_style_setting",
+            UserStyleSetting.Id("watch_hand_length_style_setting"),
             "Hand length",
             "Scale of watch hands",
             null,
             0.25,
             1.0,
-            listOf(Layer.TOP_LAYER),
+            listOf(Layer.COMPLICATIONS_OVERLAY),
             0.75
         )
 
-    private val mockListener1 = Mockito.mock(UserStyleRepository.UserStyleListener::class.java)
-    private val mockListener2 = Mockito.mock(UserStyleRepository.UserStyleListener::class.java)
-    private val mockListener3 = Mockito.mock(UserStyleRepository.UserStyleListener::class.java)
+    private val mockListener1 =
+        Mockito.mock(CurrentUserStyleRepository.UserStyleChangeListener::class.java)
+    private val mockListener2 =
+        Mockito.mock(CurrentUserStyleRepository.UserStyleChangeListener::class.java)
+    private val mockListener3 =
+        Mockito.mock(CurrentUserStyleRepository.UserStyleChangeListener::class.java)
 
     private val userStyleRepository =
-        UserStyleRepository(
+        CurrentUserStyleRepository(
             UserStyleSchema(
                 listOf(colorStyleSetting, watchHandStyleSetting, watchHandLengthStyleSetting)
             )
         )
 
     @Test
-    fun addUserStyleListener_firesImmediately() {
-        userStyleRepository.addUserStyleListener(mockListener1)
+    public fun addUserStyleListener_firesImmediately() {
+        userStyleRepository.addUserStyleChangeListener(mockListener1)
         Mockito.verify(mockListener1).onUserStyleChanged(userStyleRepository.userStyle)
     }
 
     @Test
-    fun assigning_userStyle_firesListeners() {
-        userStyleRepository.addUserStyleListener(mockListener1)
-        userStyleRepository.addUserStyleListener(mockListener2)
-        userStyleRepository.addUserStyleListener(mockListener3)
+    public fun assigning_userStyle_firesListeners() {
+        userStyleRepository.addUserStyleChangeListener(mockListener1)
+        userStyleRepository.addUserStyleChangeListener(mockListener2)
+        userStyleRepository.addUserStyleChangeListener(mockListener3)
 
         Mockito.verify(mockListener1).onUserStyleChanged(userStyleRepository.userStyle)
         Mockito.verify(mockListener2).onUserStyleChanged(userStyleRepository.userStyle)
@@ -124,7 +128,7 @@
         Mockito.verify(mockListener3).onUserStyleChanged(userStyleRepository.userStyle)
     }
 
-    fun assigning_userStyle() {
+    public fun assigning_userStyle() {
         val newStyle = UserStyle(
             hashMapOf(
                 colorStyleSetting to greenStyleOption,
@@ -141,22 +145,22 @@
     }
 
     @Test
-    fun assign_userStyle_with_distinctButMatchingRefs() {
+    public fun assign_userStyle_with_distinctButMatchingRefs() {
         val colorStyleSetting2 = ListUserStyleSetting(
-            "color_style_setting",
+            UserStyleSetting.Id("color_style_setting"),
             "Colors",
             "Watchface colorization", /* icon = */
             null,
             colorStyleList,
-            listOf(Layer.BASE_LAYER)
+            listOf(Layer.BASE)
         )
         val watchHandStyleSetting2 = ListUserStyleSetting(
-            "hand_style_setting",
+            UserStyleSetting.Id("hand_style_setting"),
             "Hand Style",
             "Hand visual look", /* icon = */
             null,
             watchHandStyleList,
-            listOf(Layer.TOP_LAYER)
+            listOf(Layer.COMPLICATIONS_OVERLAY)
         )
 
         val newStyle = UserStyle(
@@ -175,7 +179,7 @@
     }
 
     @Test
-    fun defaultValues() {
+    public fun defaultValues() {
         val watchHandLengthOption =
             userStyleRepository.userStyle.selectedOptions[watchHandLengthStyleSetting]!! as
                 DoubleRangeUserStyleSetting.DoubleRangeOption
@@ -183,80 +187,93 @@
     }
 
     @Test
-    fun userStyle_mapConstructor() {
+    public fun userStyle_mapConstructor() {
         val userStyle = UserStyle(
-            mapOf(
-                "color_style_setting" to "bluestyle",
-                "hand_style_setting" to "gothic_style"
+            UserStyleData(
+                mapOf(
+                    "color_style_setting" to "bluestyle".encodeToByteArray(),
+                    "hand_style_setting" to "gothic_style".encodeToByteArray()
+                )
             ),
             userStyleRepository.schema
         )
 
-        assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id).isEqualTo("bluestyle")
-        assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id).isEqualTo("gothic_style")
+        assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id.value.decodeToString())
+            .isEqualTo("bluestyle")
+        assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id.value.decodeToString())
+            .isEqualTo("gothic_style")
     }
 
     @Test
-    fun userStyle_mapConstructor_badColorStyle() {
+    public fun userStyle_mapConstructor_badColorStyle() {
         val userStyle = UserStyle(
-            mapOf(
-                "color_style_setting" to "I DO NOT EXIST",
-                "hand_style_setting" to "gothic_style"
+            UserStyleData(
+                mapOf(
+                    "color_style_setting" to "I DO NOT EXIST".encodeToByteArray(),
+                    "hand_style_setting" to "gothic_style".encodeToByteArray()
+                )
             ),
             userStyleRepository.schema
         )
 
-        assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id).isEqualTo("red_style")
-        assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id).isEqualTo("gothic_style")
+        assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id.value.decodeToString())
+            .isEqualTo("red_style")
+        assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id.value.decodeToString())
+            .isEqualTo("gothic_style")
     }
 
     @Test
-    fun userStyle_mapConstructor_missingColorStyle() {
+    public fun userStyle_mapConstructor_missingColorStyle() {
         val userStyle = UserStyle(
-            mapOf(
-                "hand_style_setting" to "gothic_style"
+            UserStyleData(
+                mapOf("hand_style_setting" to "gothic_style".encodeToByteArray())
             ),
             userStyleRepository.schema
         )
 
-        assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id).isEqualTo("red_style")
-        assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id).isEqualTo("gothic_style")
+        assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id.value.decodeToString())
+            .isEqualTo("red_style")
+        assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id.value.decodeToString())
+            .isEqualTo("gothic_style")
     }
 
     @Test
-    fun userStyle_mapConstructor_customValueUserStyleSetting() {
+    public fun userStyle_mapConstructor_customValueUserStyleSetting() {
         val customStyleSetting = CustomValueUserStyleSetting(
-            listOf(Layer.BASE_LAYER),
-            "default"
+            listOf(Layer.BASE),
+            "default".encodeToByteArray()
         )
 
-        val userStyleRepository = UserStyleRepository(
+        val userStyleRepository = CurrentUserStyleRepository(
             UserStyleSchema(
                 listOf(customStyleSetting)
             )
         )
 
         val userStyle = UserStyle(
-            mapOf(
-                CustomValueUserStyleSetting.CUSTOM_VALUE_USER_STYLE_SETTING_ID to "TEST 123"
+            UserStyleData(
+                mapOf(
+                    CustomValueUserStyleSetting.CUSTOM_VALUE_USER_STYLE_SETTING_ID
+                        to "TEST 123".encodeToByteArray()
+                )
             ),
             userStyleRepository.schema
         )
 
         val customValue = userStyle.selectedOptions[customStyleSetting]!! as
             UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption
-        assertThat(customValue.customValue).isEqualTo("TEST 123")
+        assertThat(customValue.customValue.decodeToString()).isEqualTo("TEST 123")
     }
 
     @Test
-    fun userStyle_multiple_CustomValueUserStyleSettings_notAllowed() {
+    public fun userStyle_multiple_CustomValueUserStyleSettings_notAllowed() {
         val customStyleSetting1 = CustomValueUserStyleSetting(
-            listOf(Layer.BASE_LAYER),
-            "default"
+            listOf(Layer.BASE),
+            "default".encodeToByteArray()
         )
         val customStyleSetting2 = CustomValueUserStyleSetting(
-            listOf(Layer.BASE_LAYER),
-            "default"
+            listOf(Layer.BASE),
+            "default".encodeToByteArray()
         )
 
         try {
@@ -273,24 +290,29 @@
     }
 
     @Test
-    fun setAndGetCustomStyleSetting() {
+    public fun setAndGetCustomStyleSetting() {
         val customStyleSetting = CustomValueUserStyleSetting(
-            listOf(Layer.BASE_LAYER),
-            "default"
+            listOf(Layer.BASE),
+            "default".encodeToByteArray()
         )
 
-        val userStyleRepository = UserStyleRepository(
+        val userStyleRepository = CurrentUserStyleRepository(
             UserStyleSchema(
                 listOf(customStyleSetting)
             )
         )
 
         userStyleRepository.userStyle = UserStyle(
-            mapOf(customStyleSetting to CustomValueUserStyleSetting.CustomValueOption("test"))
+            mapOf(
+                customStyleSetting to
+                    CustomValueUserStyleSetting.CustomValueOption("test".encodeToByteArray())
+            )
         )
 
         assertThat(
-            userStyleRepository.userStyle[customStyleSetting]?.toCustomValueOption()!!.customValue
+            userStyleRepository.userStyle[
+                customStyleSetting
+            ]?.toCustomValueOption()!!.customValue.decodeToString()
         ).isEqualTo("test")
     }
 }
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
index 57b4cdc..322f0f0 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
@@ -25,6 +25,7 @@
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
 import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
 import androidx.wear.watchface.style.data.UserStyleSettingWireFormat
 import androidx.wear.watchface.style.data.UserStyleWireFormat
@@ -36,27 +37,27 @@
 import org.junit.runner.RunWith
 
 @RunWith(StyleTestRunner::class)
-class StyleParcelableTest {
+public class StyleParcelableTest {
 
     private val icon1 = Icon.createWithContentUri("icon1")
     private val icon2 = Icon.createWithContentUri("icon2")
     private val icon3 = Icon.createWithContentUri("icon3")
     private val icon4 = Icon.createWithContentUri("icon4")
-    private val option1 = ListUserStyleSetting.ListOption("1", "one", icon1)
-    private val option2 = ListUserStyleSetting.ListOption("2", "two", icon2)
-    private val option3 = ListUserStyleSetting.ListOption("3", "three", icon3)
-    private val option4 = ListUserStyleSetting.ListOption("4", "four", icon4)
+    private val option1 = ListUserStyleSetting.ListOption(Option.Id("1"), "one", icon1)
+    private val option2 = ListUserStyleSetting.ListOption(Option.Id("2"), "two", icon2)
+    private val option3 = ListUserStyleSetting.ListOption(Option.Id("3"), "three", icon3)
+    private val option4 = ListUserStyleSetting.ListOption(Option.Id("4"), "four", icon4)
 
     @Test
-    fun parcelAndUnparcelStyleSettingAndOption() {
+    public fun parcelAndUnparcelStyleSettingAndOption() {
         val settingIcon = Icon.createWithContentUri("settingIcon")
         val styleSetting = ListUserStyleSetting(
-            "id",
+            UserStyleSetting.Id("id"),
             "displayName",
             "description",
             settingIcon,
             listOf(option1, option2, option3),
-            listOf(Layer.BASE_LAYER)
+            listOf(Layer.BASE)
         )
 
         val parcel = Parcel.obtain()
@@ -72,29 +73,29 @@
 
         assert(unparceled is ListUserStyleSetting)
 
-        assertThat(unparceled.id).isEqualTo("id")
+        assertThat(unparceled.id.value).isEqualTo("id")
         assertThat(unparceled.displayName).isEqualTo("displayName")
         assertThat(unparceled.description).isEqualTo("description")
         assertThat(unparceled.icon!!.uri.toString()).isEqualTo("settingIcon")
-        assertThat(unparceled.affectsLayers.size).isEqualTo(1)
-        assertThat(unparceled.affectsLayers.first()).isEqualTo(Layer.BASE_LAYER)
+        assertThat(unparceled.affectedLayers.size).isEqualTo(1)
+        assertThat(unparceled.affectedLayers.first()).isEqualTo(Layer.BASE)
         val optionArray =
             unparceled.options.filterIsInstance<ListUserStyleSetting.ListOption>()
                 .toTypedArray()
         assertThat(optionArray.size).isEqualTo(3)
-        assertThat(optionArray[0].id).isEqualTo("1")
+        assertThat(optionArray[0].id.value.decodeToString()).isEqualTo("1")
         assertThat(optionArray[0].displayName).isEqualTo("one")
         assertThat(optionArray[0].icon!!.uri.toString()).isEqualTo("icon1")
-        assertThat(optionArray[1].id).isEqualTo("2")
+        assertThat(optionArray[1].id.value.decodeToString()).isEqualTo("2")
         assertThat(optionArray[1].displayName).isEqualTo("two")
         assertThat(optionArray[1].icon!!.uri.toString()).isEqualTo("icon2")
-        assertThat(optionArray[2].id).isEqualTo("3")
+        assertThat(optionArray[2].id.value.decodeToString()).isEqualTo("3")
         assertThat(optionArray[2].displayName).isEqualTo("three")
         assertThat(optionArray[2].icon!!.uri.toString()).isEqualTo("icon3")
     }
 
     @Test
-    fun marshallAndUnmarshallOptions() {
+    public fun marshallAndUnmarshallOptions() {
         val wireFormat1 = option1.toWireFormat()
         val wireFormat2 = option2.toWireFormat()
         val wireFormat3 = option3.toWireFormat()
@@ -106,48 +107,48 @@
         val unmarshalled3 =
             UserStyleSetting.Option.createFromWireFormat(wireFormat3).toListOption()!!
 
-        assertThat(unmarshalled1.id).isEqualTo("1")
+        assertThat(unmarshalled1.id.value.decodeToString()).isEqualTo("1")
         assertThat(unmarshalled1.displayName).isEqualTo("one")
         assertThat(unmarshalled1.icon!!.uri.toString()).isEqualTo("icon1")
-        assertThat(unmarshalled2.id).isEqualTo("2")
+        assertThat(unmarshalled2.id.value.decodeToString()).isEqualTo("2")
         assertThat(unmarshalled2.displayName).isEqualTo("two")
         assertThat(unmarshalled2.icon!!.uri.toString()).isEqualTo("icon2")
-        assertThat(unmarshalled3.id).isEqualTo("3")
+        assertThat(unmarshalled3.id.value.decodeToString()).isEqualTo("3")
         assertThat(unmarshalled3.displayName).isEqualTo("three")
         assertThat(unmarshalled3.icon!!.uri.toString()).isEqualTo("icon3")
     }
 
     @Test
-    fun parcelAndUnparcelUserStyleSchema() {
+    public fun parcelAndUnparcelUserStyleSchema() {
         val settingIcon1 = Icon.createWithContentUri("settingIcon1")
         val settingIcon2 = Icon.createWithContentUri("settingIcon2")
         val styleSetting1 = ListUserStyleSetting(
-            "id1",
+            UserStyleSetting.Id("id1"),
             "displayName1",
             "description1",
             settingIcon1,
             listOf(option1, option2),
-            listOf(Layer.BASE_LAYER)
+            listOf(Layer.BASE)
         )
         val styleSetting2 = ListUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             settingIcon2,
             listOf(option3, option4),
-            listOf(Layer.TOP_LAYER)
+            listOf(Layer.COMPLICATIONS_OVERLAY)
         )
         val styleSetting3 = BooleanUserStyleSetting(
-            "id3",
+            UserStyleSetting.Id("id3"),
             "displayName3",
             "description3",
             null,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             true
         )
         val styleSetting4 = CustomValueUserStyleSetting(
-            listOf(Layer.BASE_LAYER),
-            "default"
+            listOf(Layer.BASE),
+            "default".encodeToByteArray()
         )
 
         val srcSchema = UserStyleSchema(
@@ -169,74 +170,77 @@
         parcel.recycle()
 
         assert(schema.userStyleSettings[0] is ListUserStyleSetting)
-        assertThat(schema.userStyleSettings[0].id).isEqualTo("id1")
+        assertThat(schema.userStyleSettings[0].id.value).isEqualTo("id1")
         assertThat(schema.userStyleSettings[0].displayName).isEqualTo("displayName1")
         assertThat(schema.userStyleSettings[0].description).isEqualTo("description1")
         assertThat(schema.userStyleSettings[0].icon!!.uri.toString()).isEqualTo("settingIcon1")
-        assertThat(schema.userStyleSettings[0].affectsLayers.size).isEqualTo(1)
-        assertThat(schema.userStyleSettings[0].affectsLayers.first()).isEqualTo(Layer.BASE_LAYER)
+        assertThat(schema.userStyleSettings[0].affectedLayers.size).isEqualTo(1)
+        assertThat(schema.userStyleSettings[0].affectedLayers.first()).isEqualTo(Layer.BASE)
         val optionArray1 =
             schema.userStyleSettings[0].options.filterIsInstance<ListUserStyleSetting.ListOption>()
                 .toTypedArray()
         assertThat(optionArray1.size).isEqualTo(2)
-        assertThat(optionArray1[0].id).isEqualTo("1")
+        assertThat(optionArray1[0].id.value.decodeToString()).isEqualTo("1")
         assertThat(optionArray1[0].displayName).isEqualTo("one")
         assertThat(optionArray1[0].icon!!.uri.toString()).isEqualTo("icon1")
-        assertThat(optionArray1[1].id).isEqualTo("2")
+        assertThat(optionArray1[1].id.value.decodeToString()).isEqualTo("2")
         assertThat(optionArray1[1].displayName).isEqualTo("two")
         assertThat(optionArray1[1].icon!!.uri.toString()).isEqualTo("icon2")
 
         assert(schema.userStyleSettings[1] is ListUserStyleSetting)
-        assertThat(schema.userStyleSettings[1].id).isEqualTo("id2")
+        assertThat(schema.userStyleSettings[1].id.value).isEqualTo("id2")
         assertThat(schema.userStyleSettings[1].displayName).isEqualTo("displayName2")
         assertThat(schema.userStyleSettings[1].description).isEqualTo("description2")
         assertThat(schema.userStyleSettings[1].icon!!.uri.toString()).isEqualTo("settingIcon2")
-        assertThat(schema.userStyleSettings[1].affectsLayers.size).isEqualTo(1)
-        assertThat(schema.userStyleSettings[1].affectsLayers.first()).isEqualTo(Layer.TOP_LAYER)
+        assertThat(schema.userStyleSettings[1].affectedLayers.size).isEqualTo(1)
+        assertThat(schema.userStyleSettings[1].affectedLayers.first()).isEqualTo(
+            Layer.COMPLICATIONS_OVERLAY
+        )
         val optionArray2 =
             schema.userStyleSettings[1].options.filterIsInstance<ListUserStyleSetting.ListOption>()
                 .toTypedArray()
         assertThat(optionArray2.size).isEqualTo(2)
-        assertThat(optionArray2[0].id).isEqualTo("3")
+        assertThat(optionArray2[0].id.value.decodeToString()).isEqualTo("3")
         assertThat(optionArray2[0].displayName).isEqualTo("three")
         assertThat(optionArray2[0].icon!!.uri.toString()).isEqualTo("icon3")
-        assertThat(optionArray2[1].id).isEqualTo("4")
+        assertThat(optionArray2[1].id.value.decodeToString()).isEqualTo("4")
         assertThat(optionArray2[1].displayName).isEqualTo("four")
         assertThat(optionArray2[1].icon!!.uri.toString()).isEqualTo("icon4")
 
         assert(schema.userStyleSettings[2] is BooleanUserStyleSetting)
-        assertThat(schema.userStyleSettings[2].id).isEqualTo("id3")
+        assertThat(schema.userStyleSettings[2].id.value).isEqualTo("id3")
         assertThat(schema.userStyleSettings[2].displayName).isEqualTo("displayName3")
         assertThat(schema.userStyleSettings[2].description).isEqualTo("description3")
         assertThat(schema.userStyleSettings[2].icon).isEqualTo(null)
-        assertThat(schema.userStyleSettings[2].affectsLayers.size).isEqualTo(1)
-        assertThat(schema.userStyleSettings[2].affectsLayers.first()).isEqualTo(Layer.BASE_LAYER)
+        assertThat(schema.userStyleSettings[2].affectedLayers.size).isEqualTo(1)
+        assertThat(schema.userStyleSettings[2].affectedLayers.first()).isEqualTo(Layer.BASE)
 
         assert(schema.userStyleSettings[3] is CustomValueUserStyleSetting)
-        assertThat(schema.userStyleSettings[3].getDefaultOption().id).isEqualTo("default")
-        assertThat(schema.userStyleSettings[3].affectsLayers.size).isEqualTo(1)
-        assertThat(schema.userStyleSettings[3].affectsLayers.first()).isEqualTo(Layer.BASE_LAYER)
+        assertThat(schema.userStyleSettings[3].getDefaultOption().id.value.decodeToString())
+            .isEqualTo("default")
+        assertThat(schema.userStyleSettings[3].affectedLayers.size).isEqualTo(1)
+        assertThat(schema.userStyleSettings[3].affectedLayers.first()).isEqualTo(Layer.BASE)
     }
 
     @Test
-    fun parcelAndUnparcelUserStyle() {
+    public fun parcelAndUnparcelUserStyle() {
         val settingIcon1 = Icon.createWithContentUri("settingIcon1")
         val settingIcon2 = Icon.createWithContentUri("settingIcon2")
         val styleSetting1 = ListUserStyleSetting(
-            "id1",
+            UserStyleSetting.Id("id1"),
             "displayName1",
             "description1",
             settingIcon1,
             listOf(option1, option2),
-            listOf(Layer.BASE_LAYER)
+            listOf(Layer.BASE)
         )
         val styleSetting2 = ListUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             settingIcon2,
             listOf(option3, option4),
-            listOf(Layer.TOP_LAYER)
+            listOf(Layer.COMPLICATIONS_OVERLAY)
         )
         val schema = UserStyleSchema(listOf(styleSetting1, styleSetting2))
         val userStyle = UserStyle(
@@ -252,133 +256,135 @@
         parcel.setDataPosition(0)
 
         val unparcelled =
-            UserStyle(UserStyleWireFormat.CREATOR.createFromParcel(parcel), schema)
+            UserStyle(UserStyleData(UserStyleWireFormat.CREATOR.createFromParcel(parcel)), schema)
         parcel.recycle()
 
         assertThat(unparcelled.selectedOptions.size).isEqualTo(2)
-        assertThat(unparcelled.selectedOptions[styleSetting1]!!.id).isEqualTo(option2.id)
-        assertThat(unparcelled.selectedOptions[styleSetting2]!!.id).isEqualTo(option3.id)
+        assertThat(unparcelled.selectedOptions[styleSetting1]!!.id.value.decodeToString())
+            .isEqualTo(option2.id.value.decodeToString())
+        assertThat(unparcelled.selectedOptions[styleSetting2]!!.id.value.decodeToString())
+            .isEqualTo(option3.id.value.decodeToString())
     }
 
     @Test
-    fun booleanUserStyleSetting_defaultValue() {
+    public fun booleanUserStyleSetting_defaultValue() {
         val booleanUserStyleSettingDefaultTrue = BooleanUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             null,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             true
         )
         assertTrue(booleanUserStyleSettingDefaultTrue.getDefaultValue())
 
         val booleanUserStyleSettingDefaultFalse = BooleanUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             null,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             false
         )
         assertFalse(booleanUserStyleSettingDefaultFalse.getDefaultValue())
     }
 
     @Test
-    fun doubleRangeUserStyleSetting_defaultValue() {
+    public fun doubleRangeUserStyleSetting_defaultValue() {
         val doubleRangeUserStyleSettingDefaultMin = DoubleRangeUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             null,
             -1.0,
             1.0,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             -1.0
         )
         assertThat(doubleRangeUserStyleSettingDefaultMin.getDefaultValue()).isEqualTo(-1.0)
 
         val doubleRangeUserStyleSettingDefaultMid = DoubleRangeUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             null,
             -1.0,
             1.0,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             0.5
         )
         assertThat(doubleRangeUserStyleSettingDefaultMid.getDefaultValue()).isEqualTo(0.5)
 
         val doubleRangeUserStyleSettingDefaultMax = DoubleRangeUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             null,
             -1.0,
             1.0,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             1.0
         )
         assertThat(doubleRangeUserStyleSettingDefaultMax.getDefaultValue()).isEqualTo(1.0)
     }
 
     @Test
-    fun longRangeUserStyleSetting_defaultValue() {
+    public fun longRangeUserStyleSetting_defaultValue() {
         val longRangeUserStyleSettingDefaultMin = LongRangeUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             null,
             -1,
             10,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             -1,
         )
         assertThat(longRangeUserStyleSettingDefaultMin.getDefaultValue()).isEqualTo(-1)
 
         val longRangeUserStyleSettingDefaultMid = LongRangeUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             null,
             -1,
             10,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             5
         )
         assertThat(longRangeUserStyleSettingDefaultMid.getDefaultValue()).isEqualTo(5)
 
         val longRangeUserStyleSettingDefaultMax = LongRangeUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             null,
             -1,
             10,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             10
         )
         assertThat(longRangeUserStyleSettingDefaultMax.getDefaultValue()).isEqualTo(10)
     }
 
     @Test
-    fun parcelAndUnparcelComplicationsUserStyleSetting() {
+    public fun parcelAndUnparcelComplicationsUserStyleSetting() {
         val leftComplicationID = 101
         val rightComplicationID = 102
         val src = ComplicationsUserStyleSetting(
-            "complications_style_setting",
+            UserStyleSetting.Id("complications_style_setting"),
             "Complications",
             "Number and position",
             icon = null,
             complicationConfig = listOf(
                 ComplicationsUserStyleSetting.ComplicationsOption(
-                    "LEFT_AND_RIGHT_COMPLICATIONS",
+                    Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
                     "Both",
                     null,
                     listOf()
                 ),
                 ComplicationsUserStyleSetting.ComplicationsOption(
-                    "NO_COMPLICATIONS",
+                    Option.Id("NO_COMPLICATIONS"),
                     "None",
                     null,
                     listOf(
@@ -393,7 +399,7 @@
                     )
                 ),
                 ComplicationsUserStyleSetting.ComplicationsOption(
-                    "LEFT_COMPLICATION",
+                    Option.Id("LEFT_COMPLICATION"),
                     "Left",
                     null,
                     listOf(
@@ -404,7 +410,7 @@
                     )
                 ),
                 ComplicationsUserStyleSetting.ComplicationsOption(
-                    "RIGHT_COMPLICATION",
+                    Option.Id("RIGHT_COMPLICATION"),
                     "Right",
                     null,
                     listOf(
@@ -430,15 +436,15 @@
         parcel.recycle()
 
         assert(unparceled is ComplicationsUserStyleSetting)
-        assertThat(unparceled.id).isEqualTo("complications_style_setting")
+        assertThat(unparceled.id.value).isEqualTo("complications_style_setting")
 
         val options = unparceled.options.filterIsInstance<
             ComplicationsUserStyleSetting.ComplicationsOption>()
         assertThat(options.size).isEqualTo(4)
-        assertThat(options[0].id).isEqualTo("LEFT_AND_RIGHT_COMPLICATIONS")
+        assertThat(options[0].id.value.decodeToString()).isEqualTo("LEFT_AND_RIGHT_COMPLICATIONS")
         assertThat(options[0].complicationOverlays.size).isEqualTo(0)
 
-        assertThat(options[1].id).isEqualTo("NO_COMPLICATIONS")
+        assertThat(options[1].id.value.decodeToString()).isEqualTo("NO_COMPLICATIONS")
         assertThat(options[1].complicationOverlays.size).isEqualTo(2)
         val options1Overlays = ArrayList(options[1].complicationOverlays)
         assertThat(options1Overlays[0].complicationId).isEqualTo(leftComplicationID)
@@ -446,13 +452,13 @@
         assertThat(options1Overlays[1].complicationId).isEqualTo(rightComplicationID)
         assertFalse(options1Overlays[1].enabled!!)
 
-        assertThat(options[2].id).isEqualTo("LEFT_COMPLICATION")
+        assertThat(options[2].id.value.decodeToString()).isEqualTo("LEFT_COMPLICATION")
         assertThat(options[2].complicationOverlays.size).isEqualTo(1)
         val options2Overlays = ArrayList(options[2].complicationOverlays)
         assertThat(options2Overlays[0].complicationId).isEqualTo(rightComplicationID)
         assertFalse(options2Overlays[0].enabled!!)
 
-        assertThat(options[3].id).isEqualTo("RIGHT_COMPLICATION")
+        assertThat(options[3].id.value.decodeToString()).isEqualTo("RIGHT_COMPLICATION")
         assertThat(options[3].complicationOverlays.size).isEqualTo(1)
         val options3Overlays = ArrayList(options[3].complicationOverlays)
         assertThat(options3Overlays[0].complicationId).isEqualTo(leftComplicationID)
@@ -460,36 +466,36 @@
     }
 
     @Test
-    fun styleSchemaToString() {
+    public fun styleSchemaToString() {
         val settingIcon1 = Icon.createWithContentUri("settingIcon1")
         val settingIcon2 = Icon.createWithContentUri("settingIcon2")
         val styleSetting1 = ListUserStyleSetting(
-            "id1",
+            UserStyleSetting.Id("id1"),
             "displayName1",
             "description1",
             settingIcon1,
             listOf(option1, option2),
-            listOf(Layer.BASE_LAYER)
+            listOf(Layer.BASE)
         )
         val styleSetting2 = ListUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             settingIcon2,
             listOf(option3, option4),
-            listOf(Layer.TOP_LAYER)
+            listOf(Layer.COMPLICATIONS_OVERLAY)
         )
         val styleSetting3 = BooleanUserStyleSetting(
-            "id3",
+            UserStyleSetting.Id("id3"),
             "displayName3",
             "description3",
             null,
-            listOf(Layer.BASE_LAYER),
+            listOf(Layer.BASE),
             true
         )
         val styleSetting4 = CustomValueUserStyleSetting(
-            listOf(Layer.BASE_LAYER),
-            "default"
+            listOf(Layer.BASE),
+            "default".encodeToByteArray()
         )
 
         val schema = UserStyleSchema(
@@ -508,24 +514,24 @@
 
     @Ignore
     @Test
-    fun userStyleToString() {
+    public fun userStyleToString() {
         val settingIcon1 = Icon.createWithContentUri("settingIcon1")
         val settingIcon2 = Icon.createWithContentUri("settingIcon2")
         val styleSetting1 = ListUserStyleSetting(
-            "id1",
+            UserStyleSetting.Id("id1"),
             "displayName1",
             "description1",
             settingIcon1,
             listOf(option1, option2),
-            listOf(Layer.BASE_LAYER)
+            listOf(Layer.BASE)
         )
         val styleSetting2 = ListUserStyleSetting(
-            "id2",
+            UserStyleSetting.Id("id2"),
             "displayName2",
             "description2",
             settingIcon2,
             listOf(option3, option4),
-            listOf(Layer.TOP_LAYER)
+            listOf(Layer.COMPLICATIONS_OVERLAY)
         )
         val style = UserStyle(
             mapOf(
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleTestRunner.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleTestRunner.kt
index fef3704..3d018cd 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleTestRunner.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleTestRunner.kt
@@ -22,7 +22,7 @@
 
 // Without this we get test failures with an error:
 // "failed to access class kotlin.jvm.internal.DefaultConstructorMarker".
-class StyleTestRunner(testClass: Class<*>) : RobolectricTestRunner(testClass) {
+public class StyleTestRunner(testClass: Class<*>) : RobolectricTestRunner(testClass) {
     override fun createClassLoaderConfig(method: FrameworkMethod): InstrumentationConfiguration =
         InstrumentationConfiguration.Builder(
             super.createClassLoaderConfig(method)
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
index e5965cb..742850d 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
@@ -16,91 +16,105 @@
 
 package androidx.wear.watchface.style
 
+import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.nio.ByteBuffer
 
 @RunWith(StyleTestRunner::class)
-class UserStyleSettingTest {
+public class UserStyleSettingTest {
+
+    private fun doubleToByteArray(value: Double) =
+        ByteArray(8).apply { ByteBuffer.wrap(this).putDouble(value) }
+
+    private fun byteArrayToDouble(value: ByteArray) = ByteBuffer.wrap(value).double
 
     @Test
-    fun rangedUserStyleSetting_getOptionForId_returns_default_for_bad_input() {
+    public fun rangedUserStyleSetting_getOptionForId_returns_default_for_bad_input() {
         val defaultValue = 0.75
         val rangedUserStyleSetting =
             DoubleRangeUserStyleSetting(
-                "example_setting",
+                UserStyleSetting.Id("example_setting"),
                 "Example Ranged Setting",
                 "An example setting",
                 null,
                 0.0,
                 1.0,
-                listOf(Layer.BASE_LAYER),
+                listOf(Layer.BASE),
                 defaultValue
             )
 
-        assertThat(rangedUserStyleSetting.getOptionForId("not a number").id)
-            .isEqualTo(defaultValue.toString())
+        assertThat(
+            rangedUserStyleSetting.getOptionForId("not a number".encodeToByteArray())
+                .toDoubleRangeOption()!!.value
+        ).isEqualTo(defaultValue)
 
-        assertThat(rangedUserStyleSetting.getOptionForId("-1").id)
-            .isEqualTo(defaultValue.toString())
+        assertThat(
+            rangedUserStyleSetting.getOptionForId("-1".encodeToByteArray())
+                .toDoubleRangeOption()!!.value
+        ).isEqualTo(defaultValue)
 
-        assertThat(rangedUserStyleSetting.getOptionForId("10").id)
-            .isEqualTo(defaultValue.toString())
+        assertThat(
+            rangedUserStyleSetting.getOptionForId("10".encodeToByteArray())
+                .toDoubleRangeOption()!!.value
+        ).isEqualTo(defaultValue)
     }
 
     @Test
-    fun rangedUserStyleSetting_getOptionForId() {
+    public fun byteArrayConversion() {
+        assertThat(BooleanUserStyleSetting.BooleanOption(true).value).isEqualTo(true)
+        assertThat(BooleanUserStyleSetting.BooleanOption(false).value).isEqualTo(false)
+        assertThat(DoubleRangeUserStyleSetting.DoubleRangeOption(123.4).value).isEqualTo(123.4)
+        assertThat(LongRangeUserStyleSetting.LongRangeOption(1234).value).isEqualTo(1234)
+    }
+
+    @Test
+    public fun rangedUserStyleSetting_getOptionForId() {
         val defaultValue = 0.75
         val rangedUserStyleSetting =
             DoubleRangeUserStyleSetting(
-                "example_setting",
+                UserStyleSetting.Id("example_setting"),
                 "Example Ranged Setting",
                 "An example setting",
                 null,
                 0.0,
                 1.0,
-                listOf(Layer.BASE_LAYER),
+                listOf(Layer.BASE),
                 defaultValue
             )
 
-        assertThat(rangedUserStyleSetting.getOptionForId("0").id)
-            .isEqualTo("0.0")
+        assertThat(
+            byteArrayToDouble(
+                rangedUserStyleSetting.getOptionForId(doubleToByteArray(0.0)).id.value
+            )
+        ).isEqualTo(0.0)
 
-        assertThat(rangedUserStyleSetting.getOptionForId("0.5").id)
-            .isEqualTo("0.5")
+        assertThat(
+            byteArrayToDouble(
+                rangedUserStyleSetting.getOptionForId(doubleToByteArray(0.5)).id.value
+            )
+        ).isEqualTo(0.5)
 
-        assertThat(rangedUserStyleSetting.getOptionForId("1").id)
-            .isEqualTo("1.0")
+        assertThat(
+            byteArrayToDouble(
+                rangedUserStyleSetting.getOptionForId(doubleToByteArray(1.0)).id.value
+            )
+        ).isEqualTo(1.0)
     }
 
     @Test
-    fun maximumUserStyleSettingIdLength() {
+    public fun maximumUserStyleSettingIdLength() {
         // OK.
-        DoubleRangeUserStyleSetting(
-            "x".repeat(UserStyleSetting.maxIdLength),
-            "",
-            "",
-            null,
-            0.0,
-            1.0,
-            emptyList(),
-            1.0
-        )
+        UserStyleSetting.Id("x".repeat(UserStyleSetting.Id.MAX_LENGTH))
 
         try {
             // Not OK.
-            DoubleRangeUserStyleSetting(
-                "x".repeat(UserStyleSetting.maxIdLength + 1),
-                "",
-                "",
-                null,
-                0.0,
-                1.0,
-                emptyList(),
-                1.0
-            )
+            UserStyleSetting.Id("x".repeat(UserStyleSetting.Id.MAX_LENGTH + 1))
             fail("Should have thrown an exception")
         } catch (e: Exception) {
             // Expected
@@ -108,21 +122,13 @@
     }
 
     @Test
-    fun maximumOptionIdLength() {
+    public fun maximumOptionIdLength() {
         // OK.
-        UserStyleSetting.ListUserStyleSetting.ListOption(
-            "x".repeat(UserStyleSetting.Option.maxIdLength),
-            "",
-            null
-        )
+        Option.Id("x".repeat(Option.Id.MAX_LENGTH))
 
         try {
             // Not OK.
-            UserStyleSetting.ListUserStyleSetting.ListOption(
-                "x".repeat(UserStyleSetting.Option.maxIdLength + 1),
-                "",
-                null
-            )
+            Option.Id("x".repeat(Option.Id.MAX_LENGTH + 1))
             fail("Should have thrown an exception")
         } catch (e: Exception) {
             // Expected
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 009f4e8..a489434 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -72,7 +72,7 @@
   }
 
   public final class ComplicationsManager {
-    ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.UserStyleRepository userStyleRepository);
+    ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository);
     method @UiThread public void addTapListener(androidx.wear.watchface.ComplicationsManager.TapCallback tapCallback);
     method @UiThread public void displayPressedAnimation(int complicationId);
     method public operator androidx.wear.watchface.Complication? get(int id);
@@ -176,14 +176,14 @@
   }
 
   public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
     method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
   }
 
   public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
     method public final android.opengl.EGLConfig getEglConfig();
     method public final android.opengl.EGLContext? getEglContext();
     method public final android.opengl.EGLDisplay? getEglDisplay();
@@ -203,18 +203,18 @@
   }
 
   public final class WatchFace {
-    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
-    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
+    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
+    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer);
+    method public androidx.wear.watchface.style.CurrentUserStyleRepository getCurrentUserStyleRepository();
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
     method public Long? getOverridePreviewReferenceTimeMillis();
-    method public androidx.wear.watchface.style.UserStyleRepository getUserStyleRepository();
     method public static boolean isLegacyWatchFaceOverlayStyleSupported();
     method public androidx.wear.watchface.WatchFace setLegacyWatchFaceStyle(androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle);
     method public androidx.wear.watchface.WatchFace setOverridePreviewReferenceTimeMillis(@IntRange(from=0) long previewReferenceTimeMillis);
     method public androidx.wear.watchface.WatchFace setTapListener(androidx.wear.watchface.WatchFace.TapListener? tapListener);
+    property public final androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository;
     property public final androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle;
     property public final Long? overridePreviewReferenceTimeMillis;
-    property public final androidx.wear.watchface.style.UserStyleRepository userStyleRepository;
     field public static final androidx.wear.watchface.WatchFace.Companion Companion;
   }
 
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 009f4e8..a489434 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -72,7 +72,7 @@
   }
 
   public final class ComplicationsManager {
-    ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.UserStyleRepository userStyleRepository);
+    ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository);
     method @UiThread public void addTapListener(androidx.wear.watchface.ComplicationsManager.TapCallback tapCallback);
     method @UiThread public void displayPressedAnimation(int complicationId);
     method public operator androidx.wear.watchface.Complication? get(int id);
@@ -176,14 +176,14 @@
   }
 
   public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
     method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
   }
 
   public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
     method public final android.opengl.EGLConfig getEglConfig();
     method public final android.opengl.EGLContext? getEglContext();
     method public final android.opengl.EGLDisplay? getEglDisplay();
@@ -203,18 +203,18 @@
   }
 
   public final class WatchFace {
-    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
-    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
+    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
+    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer);
+    method public androidx.wear.watchface.style.CurrentUserStyleRepository getCurrentUserStyleRepository();
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
     method public Long? getOverridePreviewReferenceTimeMillis();
-    method public androidx.wear.watchface.style.UserStyleRepository getUserStyleRepository();
     method public static boolean isLegacyWatchFaceOverlayStyleSupported();
     method public androidx.wear.watchface.WatchFace setLegacyWatchFaceStyle(androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle);
     method public androidx.wear.watchface.WatchFace setOverridePreviewReferenceTimeMillis(@IntRange(from=0) long previewReferenceTimeMillis);
     method public androidx.wear.watchface.WatchFace setTapListener(androidx.wear.watchface.WatchFace.TapListener? tapListener);
+    property public final androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository;
     property public final androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle;
     property public final Long? overridePreviewReferenceTimeMillis;
-    property public final androidx.wear.watchface.style.UserStyleRepository userStyleRepository;
     field public static final androidx.wear.watchface.WatchFace.Companion Companion;
   }
 
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 151e250..33f3560 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -72,7 +72,7 @@
   }
 
   public final class ComplicationsManager {
-    ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.UserStyleRepository userStyleRepository);
+    ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository);
     method @UiThread public void addTapListener(androidx.wear.watchface.ComplicationsManager.TapCallback tapCallback);
     method @UiThread public void displayPressedAnimation(int complicationId);
     method public operator androidx.wear.watchface.Complication? get(int id);
@@ -206,14 +206,14 @@
   }
 
   public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
     method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
   }
 
   public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
-    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
     method public final android.opengl.EGLConfig getEglConfig();
     method public final android.opengl.EGLContext? getEglContext();
     method public final android.opengl.EGLDisplay? getEglDisplay();
@@ -233,21 +233,21 @@
   }
 
   public final class WatchFace {
-    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
-    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
+    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
+    ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer);
+    method public androidx.wear.watchface.style.CurrentUserStyleRepository getCurrentUserStyleRepository();
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public static kotlinx.coroutines.CompletableDeferred<androidx.wear.watchface.WatchFace.EditorDelegate> getOrCreateEditorDelegate(android.content.ComponentName componentName);
     method public Long? getOverridePreviewReferenceTimeMillis();
-    method public androidx.wear.watchface.style.UserStyleRepository getUserStyleRepository();
     method public static boolean isLegacyWatchFaceOverlayStyleSupported();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public static void registerEditorDelegate(android.content.ComponentName componentName, androidx.wear.watchface.WatchFace.EditorDelegate editorDelegate);
     method public androidx.wear.watchface.WatchFace setLegacyWatchFaceStyle(androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle);
     method public androidx.wear.watchface.WatchFace setOverridePreviewReferenceTimeMillis(@IntRange(from=0) long previewReferenceTimeMillis);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.wear.watchface.WatchFace setSystemTimeProvider(androidx.wear.watchface.WatchFace.SystemTimeProvider systemTimeProvider);
     method public androidx.wear.watchface.WatchFace setTapListener(androidx.wear.watchface.WatchFace.TapListener? tapListener);
+    property public final androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository;
     property public final androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle;
     property public final Long? overridePreviewReferenceTimeMillis;
-    property public final androidx.wear.watchface.style.UserStyleRepository userStyleRepository;
     field public static final androidx.wear.watchface.WatchFace.Companion Companion;
   }
 
@@ -264,8 +264,8 @@
     method public androidx.wear.watchface.style.UserStyle getUserStyle();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public void onDestroy();
+    method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle p);
-    method public android.graphics.Bitmap takeScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract androidx.wear.watchface.ComplicationsManager complicationsManager;
     property public abstract long previewReferenceTimeMillis;
     property public abstract android.graphics.Rect screenBounds;
diff --git a/wear/wear-watchface/guava/src/androidTest/java/AsyncListenableWatchFaceServiceTest.kt b/wear/wear-watchface/guava/src/androidTest/java/AsyncListenableWatchFaceServiceTest.kt
index 1736917..3b2d55e 100644
--- a/wear/wear-watchface/guava/src/androidTest/java/AsyncListenableWatchFaceServiceTest.kt
+++ b/wear/wear-watchface/guava/src/androidTest/java/AsyncListenableWatchFaceServiceTest.kt
@@ -27,7 +27,7 @@
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.ListenableFuture
@@ -41,10 +41,10 @@
 private class FakeRenderer(
     surfaceHolder: SurfaceHolder,
     watchState: WatchState,
-    userStyleRepository: UserStyleRepository
+    currentUserStyleRepository: CurrentUserStyleRepository
 ) : Renderer.CanvasRenderer(
     surfaceHolder,
-    userStyleRepository,
+    currentUserStyleRepository,
     watchState,
     CanvasType.SOFTWARE,
     16
@@ -60,7 +60,7 @@
         watchState: WatchState
     ): ListenableFuture<WatchFace> {
         val future = SettableFuture.create<WatchFace>()
-        val userStyleRepository = UserStyleRepository(
+        val userStyleRepository = CurrentUserStyleRepository(
             UserStyleSchema(emptyList())
         )
         // Post a task to resolve the future.
diff --git a/wear/wear-watchface/guava/src/test/java/androidx/wear/watchface/ListenableWatchFaceServiceTest.kt b/wear/wear-watchface/guava/src/test/java/androidx/wear/watchface/ListenableWatchFaceServiceTest.kt
index 272465f..489aaac 100644
--- a/wear/wear-watchface/guava/src/test/java/androidx/wear/watchface/ListenableWatchFaceServiceTest.kt
+++ b/wear/wear-watchface/guava/src/test/java/androidx/wear/watchface/ListenableWatchFaceServiceTest.kt
@@ -20,7 +20,7 @@
 import android.graphics.Rect
 import android.icu.util.Calendar
 import android.view.SurfaceHolder
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.Futures
@@ -42,7 +42,7 @@
         watchState: WatchState
     ): ListenableFuture<WatchFace> {
         val userStyleRepository =
-            UserStyleRepository(UserStyleSchema(emptyList()))
+            CurrentUserStyleRepository(UserStyleSchema(emptyList()))
         return Futures.immediateFuture(
             WatchFace(
                 WatchFaceType.DIGITAL, userStyleRepository,
diff --git a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
index 5271dda..2becac8 100644
--- a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
+++ b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
@@ -27,7 +27,7 @@
 import androidx.wear.watchface.CanvasType;
 import androidx.wear.watchface.Renderer;
 import androidx.wear.watchface.WatchState;
-import androidx.wear.watchface.style.UserStyleRepository;
+import androidx.wear.watchface.style.CurrentUserStyleRepository;
 
 import org.jetbrains.annotations.NotNull;
 
@@ -47,9 +47,9 @@
 
     public WatchFaceRenderer(
             @NotNull SurfaceHolder surfaceHolder,
-            @NotNull UserStyleRepository userStyleRepository,
+            @NotNull CurrentUserStyleRepository currentUserStyleRepository,
             @NotNull WatchState watchState) {
-        super(surfaceHolder, userStyleRepository, watchState, CanvasType.HARDWARE,
+        super(surfaceHolder, currentUserStyleRepository, watchState, CanvasType.HARDWARE,
                 UPDATE_DELAY_MILLIS);
         mPaint = new Paint();
         mPaint.setTextAlign(Align.CENTER);
diff --git a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java
index 5ceede1..5f00cde 100644
--- a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java
+++ b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java
@@ -25,7 +25,7 @@
 import androidx.wear.watchface.WatchFace;
 import androidx.wear.watchface.WatchFaceType;
 import androidx.wear.watchface.WatchState;
-import androidx.wear.watchface.style.UserStyleRepository;
+import androidx.wear.watchface.style.CurrentUserStyleRepository;
 import androidx.wear.watchface.style.UserStyleSchema;
 import androidx.wear.watchface.style.UserStyleSetting;
 
@@ -43,15 +43,16 @@
     @Override
     protected ListenableFuture<WatchFace> createWatchFaceFuture(
             @NotNull SurfaceHolder surfaceHolder, @NotNull WatchState watchState) {
-        UserStyleRepository userStyleRepository =
-                new UserStyleRepository(
+        CurrentUserStyleRepository currentUserStyleRepository =
+                new CurrentUserStyleRepository(
                         new UserStyleSchema(Collections.<UserStyleSetting>emptyList()));
         ComplicationsManager complicationManager =
                 new ComplicationsManager(Collections.<Complication>emptyList(),
-                        userStyleRepository);
-        Renderer renderer = new WatchFaceRenderer(surfaceHolder, userStyleRepository, watchState);
+                        currentUserStyleRepository);
+        Renderer renderer =
+                new WatchFaceRenderer(surfaceHolder, currentUserStyleRepository, watchState);
         return Futures.immediateFuture(
-                new WatchFace(WatchFaceType.DIGITAL, userStyleRepository, renderer,
+                new WatchFace(WatchFaceType.DIGITAL, currentUserStyleRepository, renderer,
                         complicationManager));
     }
 }
diff --git a/wear/wear-watchface/samples/src/main/AndroidManifest.xml b/wear/wear-watchface/samples/src/main/AndroidManifest.xml
index 202b8d4..7112ec9 100644
--- a/wear/wear-watchface/samples/src/main/AndroidManifest.xml
+++ b/wear/wear-watchface/samples/src/main/AndroidManifest.xml
@@ -19,6 +19,7 @@
 
     <!-- Required for complications to receive complication data and open the provider chooser. -->
     <uses-feature android:name="android.hardware.type.watch" />
+    <meta-data android:name="com.google.android.wearable.standalone" android:value="true" />
 
     <uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND"/>
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
@@ -44,21 +45,12 @@
                 <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
             </intent-filter>
 
-            <intent-filter>
-                <action android:name="com.google.android.wearable.libraries.steampack.watchface.MockTime" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="text/plain" />
-            </intent-filter>
-
-            <meta-data
-                android:name="com.google.android.wearable.standalone"
-                android:value="true" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.preview"
-                android:resource="@drawable/watch_preview" />
+                android:resource="@drawable/analog_preview" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.preview_circular"
-                android:resource="@drawable/watch_preview" />
+                android:resource="@drawable/analog_preview" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
                 android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
@@ -78,21 +70,12 @@
                 <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
             </intent-filter>
 
-            <intent-filter>
-                <action android:name="com.google.android.wearable.libraries.steampack.watchface.MockTime" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="text/plain" />
-            </intent-filter>
-
-            <meta-data
-                android:name="com.google.android.wearable.standalone"
-                android:value="true" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.preview"
-                android:resource="@drawable/watch_preview" />
+                android:resource="@drawable/digital_preview" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.preview_circular"
-                android:resource="@drawable/watch_preview" />
+                android:resource="@drawable/digital_preview" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
                 android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
@@ -112,21 +95,12 @@
                 <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
             </intent-filter>
 
-            <intent-filter>
-                <action android:name="com.google.android.wearable.libraries.steampack.watchface.MockTime" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="text/plain" />
-            </intent-filter>
-
-            <meta-data
-                android:name="com.google.android.wearable.standalone"
-                android:value="true" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.preview"
-                android:resource="@drawable/watch_preview" />
+                android:resource="@drawable/gl_preview" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.preview_circular"
-                android:resource="@drawable/watch_preview" />
+                android:resource="@drawable/gl_preview" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
                 android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
@@ -146,21 +120,12 @@
                 <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
             </intent-filter>
 
-            <intent-filter>
-                <action android:name="com.google.android.wearable.libraries.steampack.watchface.MockTime" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <data android:mimeType="text/plain" />
-            </intent-filter>
-
-            <meta-data
-                android:name="com.google.android.wearable.standalone"
-                android:value="true" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.preview"
-                android:resource="@drawable/watch_preview" />
+                android:resource="@drawable/gl_background_preview" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.preview_circular"
-                android:resource="@drawable/watch_preview" />
+                android:resource="@drawable/gl_background_preview" />
             <meta-data
                 android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
                 android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index 6ee2ef4..7f0943b 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -43,13 +43,15 @@
 import androidx.wear.watchface.WatchState
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
 import kotlin.math.cos
 import kotlin.math.sin
 
@@ -110,57 +112,57 @@
 ): WatchFace {
     val watchFaceStyle = WatchFaceColorStyle.create(context, RED_STYLE)
     val colorStyleSetting = ListUserStyleSetting(
-        COLOR_STYLE_SETTING,
+        UserStyleSetting.Id(COLOR_STYLE_SETTING),
         context.getString(R.string.colors_style_setting),
         context.getString(R.string.colors_style_setting_description),
         icon = null,
         options = listOf(
             ListUserStyleSetting.ListOption(
-                RED_STYLE,
+                Option.Id(RED_STYLE),
                 context.getString(R.string.colors_style_red),
                 Icon.createWithResource(context, R.drawable.red_style)
             ),
             ListUserStyleSetting.ListOption(
-                GREEN_STYLE,
+                Option.Id(GREEN_STYLE),
                 context.getString(R.string.colors_style_green),
                 Icon.createWithResource(context, R.drawable.green_style)
             ),
             ListUserStyleSetting.ListOption(
-                BLUE_STYLE,
+                Option.Id(BLUE_STYLE),
                 context.getString(R.string.colors_style_blue),
                 Icon.createWithResource(context, R.drawable.blue_style)
             )
         ),
-        listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
+        listOf(Layer.BASE, Layer.COMPLICATIONS, Layer.COMPLICATIONS_OVERLAY)
     )
     val drawHourPipsStyleSetting = BooleanUserStyleSetting(
-        DRAW_HOUR_PIPS_STYLE_SETTING,
+        UserStyleSetting.Id(DRAW_HOUR_PIPS_STYLE_SETTING),
         context.getString(R.string.watchface_pips_setting),
         context.getString(R.string.watchface_pips_setting_description),
         null,
-        listOf(Layer.BASE_LAYER),
+        listOf(Layer.BASE),
         true
     )
     val watchHandLengthStyleSetting = DoubleRangeUserStyleSetting(
-        WATCH_HAND_LENGTH_STYLE_SETTING,
+        UserStyleSetting.Id(WATCH_HAND_LENGTH_STYLE_SETTING),
         context.getString(R.string.watchface_hand_length_setting),
         context.getString(R.string.watchface_hand_length_setting_description),
         null,
         0.25,
         1.0,
-        listOf(Layer.TOP_LAYER),
+        listOf(Layer.COMPLICATIONS_OVERLAY),
         0.75
     )
     // These are style overrides applied on top of the complications passed into
     // complicationsManager below.
     val complicationsStyleSetting = ComplicationsUserStyleSetting(
-        COMPLICATIONS_STYLE_SETTING,
+        UserStyleSetting.Id(COMPLICATIONS_STYLE_SETTING),
         context.getString(R.string.watchface_complications_setting),
         context.getString(R.string.watchface_complications_setting_description),
         icon = null,
         complicationConfig = listOf(
             ComplicationsUserStyleSetting.ComplicationsOption(
-                LEFT_AND_RIGHT_COMPLICATIONS,
+                Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
                 context.getString(R.string.watchface_complications_setting_both),
                 null,
                 // NB this list is empty because each [ComplicationOverlay] is applied on top of
@@ -168,7 +170,7 @@
                 listOf()
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
-                NO_COMPLICATIONS,
+                Option.Id(NO_COMPLICATIONS),
                 context.getString(R.string.watchface_complications_setting_none),
                 null,
                 listOf(
@@ -183,7 +185,7 @@
                 )
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
-                LEFT_COMPLICATION,
+                Option.Id(LEFT_COMPLICATION),
                 context.getString(R.string.watchface_complications_setting_left),
                 null,
                 listOf(
@@ -194,7 +196,7 @@
                 )
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
-                RIGHT_COMPLICATION,
+                Option.Id(RIGHT_COMPLICATION),
                 context.getString(R.string.watchface_complications_setting_right),
                 null,
                 listOf(
@@ -207,7 +209,7 @@
         ),
         listOf(Layer.COMPLICATIONS)
     )
-    val userStyleRepository = UserStyleRepository(
+    val userStyleRepository = CurrentUserStyleRepository(
         UserStyleSchema(
             listOf(
                 colorStyleSetting,
@@ -272,7 +274,7 @@
     surfaceHolder: SurfaceHolder,
     private val context: Context,
     private var watchFaceColorStyle: WatchFaceColorStyle,
-    userStyleRepository: UserStyleRepository,
+    currentUserStyleRepository: CurrentUserStyleRepository,
     watchState: WatchState,
     private val colorStyleSetting: ListUserStyleSetting,
     private val drawPipsStyleSetting: BooleanUserStyleSetting,
@@ -280,7 +282,7 @@
     private val complicationsManager: ComplicationsManager
 ) : Renderer.CanvasRenderer(
     surfaceHolder,
-    userStyleRepository,
+    currentUserStyleRepository,
     watchState,
     CanvasType.HARDWARE,
     FRAME_PERIOD_MS
@@ -315,14 +317,14 @@
     private var watchHandScale = 1.0f
 
     init {
-        userStyleRepository.addUserStyleListener(
-            object : UserStyleRepository.UserStyleListener {
+        currentUserStyleRepository.addUserStyleChangeListener(
+            object : CurrentUserStyleRepository.UserStyleChangeListener {
                 @SuppressLint("SyntheticAccessor")
                 override fun onUserStyleChanged(userStyle: UserStyle) {
                     watchFaceColorStyle =
                         WatchFaceColorStyle.create(
                             context,
-                            userStyle[colorStyleSetting]!!.id
+                            userStyle[colorStyleSetting]!!.toString()
                         )
 
                     // Apply the userStyle to the complications. ComplicationDrawables for each of
@@ -355,12 +357,12 @@
         // CanvasComplicationDrawable already obeys rendererParameters.
         drawComplications(canvas, calendar)
 
-        if (renderParameters.layerParameters[Layer.TOP_LAYER] != LayerMode.HIDE) {
+        if (renderParameters.layerParameters[Layer.COMPLICATIONS_OVERLAY] != LayerMode.HIDE) {
             drawClockHands(canvas, bounds, calendar, style)
         }
 
         if (renderParameters.drawMode != DrawMode.AMBIENT &&
-            renderParameters.layerParameters[Layer.BASE_LAYER] != LayerMode.HIDE &&
+            renderParameters.layerParameters[Layer.BASE] != LayerMode.HIDE &&
             drawHourPips
         ) {
             drawNumberStyleOuterElement(canvas, bounds, style)
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index ad7410d..35e7b94 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -56,9 +56,10 @@
 import androidx.wear.watchface.WatchState
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
 import kotlin.math.max
 import kotlin.math.min
 
@@ -471,30 +472,30 @@
     ): WatchFace {
         val watchFaceStyle = WatchFaceColorStyle.create(this, RED_STYLE)
         val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
-            COLOR_STYLE_SETTING,
+            UserStyleSetting.Id(COLOR_STYLE_SETTING),
             getString(R.string.colors_style_setting),
             getString(R.string.colors_style_setting_description),
             icon = null,
             options = listOf(
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    RED_STYLE,
+                    Option.Id(RED_STYLE),
                     getString(R.string.colors_style_red),
                     Icon.createWithResource(this, R.drawable.red_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    GREEN_STYLE,
+                    Option.Id(GREEN_STYLE),
                     getString(R.string.colors_style_green),
                     Icon.createWithResource(this, R.drawable.green_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    BLUE_STYLE,
+                    Option.Id(BLUE_STYLE),
                     getString(R.string.colors_style_blue),
                     Icon.createWithResource(this, R.drawable.blue_style)
                 )
             ),
-            listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
+            listOf(Layer.BASE, Layer.COMPLICATIONS, Layer.COMPLICATIONS_OVERLAY)
         )
-        val userStyleRepository = UserStyleRepository(
+        val userStyleRepository = CurrentUserStyleRepository(
             UserStyleSchema(listOf(colorStyleSetting))
         )
         val leftComplication = Complication.createRoundRectComplicationBuilder(
@@ -622,7 +623,7 @@
             renderer.oldBounds.set(0, 0, 0, 0)
         }
         return WatchFace(
-            WatchFaceType.ANALOG,
+            WatchFaceType.DIGITAL,
             userStyleRepository,
             renderer,
             complicationsManager
@@ -634,13 +635,13 @@
     surfaceHolder: SurfaceHolder,
     private val context: Context,
     private var watchFaceColorStyle: WatchFaceColorStyle,
-    userStyleRepository: UserStyleRepository,
+    currentUserStyleRepository: CurrentUserStyleRepository,
     watchState: WatchState,
     private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
     private val complicationsManager: ComplicationsManager
 ) : Renderer.CanvasRenderer(
     surfaceHolder,
-    userStyleRepository,
+    currentUserStyleRepository,
     watchState,
     CanvasType.HARDWARE,
     INTERACTIVE_UPDATE_RATE_MS
@@ -649,6 +650,7 @@
 
     private fun getBaseDigitPaint() = Paint().apply {
         typeface = Typeface.create(DIGITAL_TYPE_FACE, Typeface.NORMAL)
+        isAntiAlias = true
     }
 
     private val digitTextHoursPaint = getBaseDigitPaint()
@@ -746,14 +748,14 @@
 
     init {
         // Listen for style changes.
-        userStyleRepository.addUserStyleListener(
-            object : UserStyleRepository.UserStyleListener {
+        currentUserStyleRepository.addUserStyleChangeListener(
+            object : CurrentUserStyleRepository.UserStyleChangeListener {
                 @SuppressLint("SyntheticAccessor")
                 override fun onUserStyleChanged(userStyle: UserStyle) {
                     watchFaceColorStyle =
                         WatchFaceColorStyle.create(
                             context,
-                            userStyle[colorStyleSetting]!!.id
+                            userStyle[colorStyleSetting]!!.toString()
                         )
 
                     // Apply the userStyle to the complications. ComplicationDrawables for each of
@@ -777,6 +779,10 @@
 
             // Trigger recomputation of bounds.
             oldBounds.set(0, 0, 0, 0)
+            val antiAlias = !(it && watchState.hasLowBitAmbient)
+            digitTextHoursPaint.setAntiAlias(antiAlias)
+            digitTextMinutesPaint.setAntiAlias(antiAlias)
+            digitTextSecondsPaint.setAntiAlias(antiAlias)
         }
     }
 
@@ -821,13 +827,13 @@
 
         applyColorStyleAndDrawMode(renderParameters.drawMode)
 
-        if (renderParameters.layerParameters[Layer.BASE_LAYER] != LayerMode.HIDE) {
+        if (renderParameters.layerParameters[Layer.BASE] != LayerMode.HIDE) {
             drawBackground(canvas)
         }
 
         drawComplications(canvas, calendar)
 
-        if (renderParameters.layerParameters[Layer.BASE_LAYER] != LayerMode.HIDE) {
+        if (renderParameters.layerParameters[Layer.BASE] != LayerMode.HIDE) {
             val is24Hour: Boolean = DateFormat.is24HourFormat(context)
 
             nextSecondTime.timeInMillis = calendar.timeInMillis
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
index c6e47e1..d229d3b 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
@@ -30,7 +30,7 @@
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.android.asCoroutineDispatcher
@@ -56,7 +56,7 @@
         surfaceHolder: SurfaceHolder,
         watchState: WatchState
     ): WatchFace {
-        val styleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
+        val styleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
 
         // Create the renderer on the main thread. It's EGLContext is bound to this thread.
         val renderer = MainThreadRenderer(surfaceHolder, styleRepository, watchState)
@@ -154,9 +154,9 @@
 
 internal class MainThreadRenderer(
     surfaceHolder: SurfaceHolder,
-    userStyleRepository: UserStyleRepository,
+    currentUserStyleRepository: CurrentUserStyleRepository,
     watchState: WatchState
-) : Renderer.GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
+) : Renderer.GlesRenderer(surfaceHolder, currentUserStyleRepository, watchState, FRAME_PERIOD_MS) {
 
     internal var watchBodyTexture: Int = -1
     internal var watchHandTexture: Int = -1
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 21a2e63..ad3fd10 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -43,9 +43,11 @@
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
 import androidx.wear.watchface.style.Layer
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import java.nio.FloatBuffer
@@ -92,25 +94,25 @@
 ): WatchFace {
     val watchFaceStyle = WatchFaceColorStyle.create(context, "white_style")
     val colorStyleSetting = ListUserStyleSetting(
-        "color_style_setting",
+        UserStyleSetting.Id("color_style_setting"),
         "Colors",
         "Watchface colorization",
         icon = null,
         options = listOf(
             ListUserStyleSetting.ListOption(
-                "red_style",
+                Option.Id("red_style"),
                 "Red",
                 Icon.createWithResource(context, R.drawable.red_style)
             ),
             ListUserStyleSetting.ListOption(
-                "green_style",
+                Option.Id("green_style"),
                 "Green",
                 Icon.createWithResource(context, R.drawable.green_style)
             )
         ),
-        listOf(Layer.BASE_LAYER, Layer.TOP_LAYER)
+        listOf(Layer.BASE, Layer.COMPLICATIONS_OVERLAY)
     )
-    val userStyleRepository = UserStyleRepository(UserStyleSchema(listOf(colorStyleSetting)))
+    val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(listOf(colorStyleSetting)))
     val complicationsManager = ComplicationsManager(
         listOf(
             Complication.createRoundRectComplicationBuilder(
@@ -154,11 +156,11 @@
 
 class ExampleOpenGLRenderer(
     surfaceHolder: SurfaceHolder,
-    private val userStyleRepository: UserStyleRepository,
+    private val currentUserStyleRepository: CurrentUserStyleRepository,
     watchState: WatchState,
     private val colorStyleSetting: ListUserStyleSetting,
     private val complication: Complication
-) : Renderer.GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
+) : Renderer.GlesRenderer(surfaceHolder, currentUserStyleRepository, watchState, FRAME_PERIOD_MS) {
 
     /** Projection transformation matrix. Converts from 3D to 2D.  */
     private val projectionMatrix = FloatArray(16)
@@ -576,7 +578,7 @@
             GLES20.glClearColor(0f, 0f, 0f, 1f)
             ambientVpMatrix
         } else {
-            when (userStyleRepository.userStyle[colorStyleSetting]!!.id) {
+            when (currentUserStyleRepository.userStyle[colorStyleSetting]!!.toString()) {
                 "red_style" -> GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1f)
                 "green_style" -> GLES20.glClearColor(0.2f, 0.5f, 0.2f, 1f)
             }
@@ -612,7 +614,7 @@
         val hoursIndex = (hours / 12f * 360f).toInt()
 
         // Render hands.
-        if (renderParameters.layerParameters[Layer.TOP_LAYER] != LayerMode.HIDE) {
+        if (renderParameters.layerParameters[Layer.COMPLICATIONS_OVERLAY] != LayerMode.HIDE) {
             Matrix.multiplyMM(
                 mvpMatrix,
                 0,
@@ -643,13 +645,12 @@
                     0
                 )
                 secondHandTriangleMap[
-                    userStyleRepository.userStyle[colorStyleSetting]!!.id
-                ]
-                    ?.draw(mvpMatrix)
+                    currentUserStyleRepository.userStyle[colorStyleSetting]!!.toString()
+                ]?.draw(mvpMatrix)
             }
         }
 
-        if (renderParameters.layerParameters[Layer.BASE_LAYER] != LayerMode.HIDE) {
+        if (renderParameters.layerParameters[Layer.BASE] != LayerMode.HIDE) {
             majorTickTriangles.draw(vpMatrix)
             minorTickTriangles.draw(vpMatrix)
             coloredTriangleProgram.unbindAttribs()
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index 8df800e..07c4b8c 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -40,9 +40,11 @@
 import androidx.wear.watchface.WatchState
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
 
 @Sampled
 fun kDocCreateExampleWatchFaceService(): WatchFaceService {
@@ -52,52 +54,52 @@
             surfaceHolder: SurfaceHolder,
             watchState: WatchState
         ): WatchFace {
-            val userStyleRepository = UserStyleRepository(
+            val userStyleRepository = CurrentUserStyleRepository(
                 UserStyleSchema(
                     listOf(
                         ListUserStyleSetting(
-                            "color_style_setting",
+                            UserStyleSetting.Id("color_style_setting"),
                             "Colors",
                             "Watchface colorization",
                             icon = null,
                             options = listOf(
                                 ListUserStyleSetting.ListOption(
-                                    "red_style",
+                                    Option.Id("red_style"),
                                     "Red",
                                     icon = null
                                 ),
                                 ListUserStyleSetting.ListOption(
-                                    "green_style",
+                                    Option.Id("green_style"),
                                     "Green",
                                     icon = null
                                 ),
                                 ListUserStyleSetting.ListOption(
-                                    "bluestyle",
+                                    Option.Id("bluestyle"),
                                     "Blue",
                                     icon = null
                                 )
                             ),
-                            listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
+                            listOf(Layer.BASE, Layer.COMPLICATIONS, Layer.COMPLICATIONS_OVERLAY)
                         ),
                         ListUserStyleSetting(
-                            "hand_style_setting",
+                            UserStyleSetting.Id("hand_style_setting"),
                             "Hand Style",
                             "Hand visual look",
                             icon = null,
                             options = listOf(
                                 ListUserStyleSetting.ListOption(
-                                    "classic_style", "Classic", icon = null
+                                    Option.Id("classic_style"), "Classic", icon = null
                                 ),
                                 ListUserStyleSetting.ListOption(
-                                    "modern_style", "Modern", icon = null
+                                    Option.Id("modern_style"), "Modern", icon = null
                                 ),
                                 ListUserStyleSetting.ListOption(
-                                    "gothic_style",
+                                    Option.Id("gothic_style"),
                                     "Gothic",
                                     icon = null
                                 )
                             ),
-                            listOf(Layer.TOP_LAYER)
+                            listOf(Layer.COMPLICATIONS_OVERLAY)
                         )
                     )
                 )
@@ -150,8 +152,8 @@
                 /* interactiveUpdateRateMillis */ 16,
             ) {
                 init {
-                    userStyleRepository.addUserStyleListener(
-                        object : UserStyleRepository.UserStyleListener {
+                    userStyleRepository.addUserStyleChangeListener(
+                        object : CurrentUserStyleRepository.UserStyleChangeListener {
                             override fun onUserStyleChanged(userStyle: UserStyle) {
                                 // `userStyle` will contain two userStyle categories with options
                                 // from the lists above. ...
diff --git a/wear/wear-watchface/samples/src/main/res/drawable-nodpi/watch_preview.png b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/analog_preview.png
similarity index 100%
rename from wear/wear-watchface/samples/src/main/res/drawable-nodpi/watch_preview.png
rename to wear/wear-watchface/samples/src/main/res/drawable-nodpi/analog_preview.png
Binary files differ
diff --git a/wear/wear-watchface/samples/src/main/res/drawable-nodpi/digital_preview.png b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/digital_preview.png
new file mode 100644
index 0000000..5344bf5
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/digital_preview.png
Binary files differ
diff --git a/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_background_preview.png b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_background_preview.png
new file mode 100644
index 0000000..484797d
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_background_preview.png
Binary files differ
diff --git a/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_preview.png b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_preview.png
new file mode 100644
index 0000000..993076c
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_preview.png
Binary files differ
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index b210fe9..afdb21f 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -33,9 +33,9 @@
 import androidx.wear.watchface.control.IHeadlessWatchFace
 import androidx.wear.watchface.control.IWatchFaceControlService
 import androidx.wear.watchface.control.WatchFaceControlService
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams
+import androidx.wear.watchface.control.data.ComplicationRenderParams
 import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
@@ -112,8 +112,8 @@
     fun createHeadlessWatchFaceInstance() {
         val instance = createInstance(100, 100)
         val bitmap = SharedMemoryImage.ashmemReadImageBundle(
-            instance.takeWatchFaceScreenshot(
-                WatchfaceScreenshotParams(
+            instance.renderWatchFaceToBitmap(
+                WatchFaceRenderParams(
                     RenderParameters(
                         DrawMode.INTERACTIVE,
                         RenderParameters.DRAW_ALL_LAYERS,
@@ -155,8 +155,8 @@
     fun createHeadlessOpenglWatchFaceInstance() {
         val instance = createOpenGlInstance(400, 400)
         val bitmap = SharedMemoryImage.ashmemReadImageBundle(
-            instance.takeWatchFaceScreenshot(
-                WatchfaceScreenshotParams(
+            instance.renderWatchFaceToBitmap(
+                WatchFaceRenderParams(
                     RenderParameters(
                         DrawMode.INTERACTIVE,
                         RenderParameters.DRAW_ALL_LAYERS,
@@ -189,8 +189,8 @@
     fun testCommandTakeComplicationScreenShot() {
         val instance = createInstance(400, 400)
         val bitmap = SharedMemoryImage.ashmemReadImageBundle(
-            instance.takeComplicationScreenshot(
-                ComplicationScreenshotParams(
+            instance.renderComplicationToBitmap(
+                ComplicationRenderParams(
                     EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
                     RenderParameters(
                         DrawMode.AMBIENT,
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index ab591af..46ff4a8 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -45,14 +45,14 @@
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.TapType
 import androidx.wear.watchface.WatchFaceService
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace
 import androidx.wear.watchface.control.InteractiveInstanceManager
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
 import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
@@ -157,7 +157,7 @@
     private lateinit var canvasAnalogWatchFaceService: TestCanvasAnalogWatchFaceService
     private lateinit var glesWatchFaceService: TestGlesWatchFaceService
     private lateinit var engineWrapper: WatchFaceService.EngineWrapper
-    private lateinit var interactiveWatchFaceInstanceWCS: IInteractiveWatchFaceWCS
+    private lateinit var interactiveWatchFaceInstance: IInteractiveWatchFace
 
     @Before
     fun setUp() {
@@ -166,8 +166,8 @@
 
     @After
     fun shutDown() {
-        if (this::interactiveWatchFaceInstanceWCS.isInitialized) {
-            interactiveWatchFaceInstanceWCS.release()
+        if (this::interactiveWatchFaceInstance.isInitialized) {
+            interactiveWatchFaceInstance.release()
         }
     }
 
@@ -240,18 +240,18 @@
                             0,
                             0
                         ),
-                        SystemState(false, 0),
+                        WatchUiState(false, 0),
                         UserStyleWireFormat(emptyMap()),
                         null
                     ),
-                    object : IPendingInteractiveWatchFaceWCS.Stub() {
+                    object : IPendingInteractiveWatchFace.Stub() {
                         override fun getApiVersion() =
-                            IPendingInteractiveWatchFaceWCS.API_VERSION
+                            IPendingInteractiveWatchFace.API_VERSION
 
-                        override fun onInteractiveWatchFaceWcsCreated(
-                            iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
+                        override fun onInteractiveWatchFaceCreated(
+                            iInteractiveWatchFace: IInteractiveWatchFace
                         ) {
-                            interactiveWatchFaceInstanceWCS = iInteractiveWatchFaceWcs
+                            interactiveWatchFaceInstance = iInteractiveWatchFace
                             sendComplications()
                             // engineWrapper won't be initialized yet, so defer execution.
                             handler.post {
@@ -267,8 +267,8 @@
     }
 
     private fun sendComplications() {
-        interactiveWatchFaceInstanceWCS.updateComplicationData(
-            interactiveWatchFaceInstanceWCS.complicationDetails.map {
+        interactiveWatchFaceInstance.updateComplicationData(
+            interactiveWatchFaceInstance.complicationDetails.map {
                 IdAndComplicationDataWireFormat(
                     it.id,
                     complicationProviders[it.complicationState.fallbackSystemProvider]!!
@@ -278,13 +278,18 @@
     }
 
     private fun setAmbient(ambient: Boolean) {
-        val interactiveWatchFaceInstanceSysUi =
+        val interactiveWatchFaceInstance =
             InteractiveInstanceManager.getAndRetainInstance(
-                interactiveWatchFaceInstanceWCS.instanceId
-            )!!.createSysUiApi()
+                interactiveWatchFaceInstance.instanceId
+            )!!
 
-        interactiveWatchFaceInstanceSysUi.setSystemState(SystemState(ambient, 0))
-        interactiveWatchFaceInstanceSysUi.release()
+        interactiveWatchFaceInstance.setWatchUiState(
+            WatchUiState(
+                ambient,
+                0
+            )
+        )
+        interactiveWatchFaceInstance.release()
     }
 
     private fun waitForPendingTaskToRunOnHandler() {
@@ -328,8 +333,8 @@
         var bitmap: Bitmap? = null
         handler.post {
             bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
-                    WatchfaceScreenshotParams(
+                interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+                    WatchFaceRenderParams(
                         RenderParameters(
                             DrawMode.AMBIENT,
                             RenderParameters.DRAW_ALL_LAYERS,
@@ -361,8 +366,8 @@
         var bitmap: Bitmap? = null
         handler.post {
             bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
-                    WatchfaceScreenshotParams(
+                interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+                    WatchFaceRenderParams(
                         RenderParameters(
                             DrawMode.INTERACTIVE,
                             RenderParameters.DRAW_ALL_LAYERS,
@@ -390,9 +395,9 @@
         handler.post(this::initCanvasWatchFace)
         initLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)
         handler.post {
-            interactiveWatchFaceInstanceWCS.updateInstance(
+            interactiveWatchFaceInstance.updateWatchfaceInstance(
                 "newId",
-                UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE))
+                UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray()))
             )
             sendComplications()
             engineWrapper.draw()
@@ -411,14 +416,14 @@
         var bitmap: Bitmap? = null
         handler.post {
             bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
-                    WatchfaceScreenshotParams(
+                interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+                    WatchFaceRenderParams(
                         RenderParameters(
                             DrawMode.INTERACTIVE,
                             mapOf(
-                                Layer.BASE_LAYER to LayerMode.DRAW,
+                                Layer.BASE to LayerMode.DRAW,
                                 Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
-                                Layer.TOP_LAYER to LayerMode.DRAW
+                                Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
                             ),
                             null,
                             Color.RED
@@ -448,14 +453,14 @@
         var bitmap: Bitmap? = null
         handler.post {
             bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
-                    WatchfaceScreenshotParams(
+                interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+                    WatchFaceRenderParams(
                         RenderParameters(
                             DrawMode.INTERACTIVE,
                             mapOf(
-                                Layer.BASE_LAYER to LayerMode.DRAW,
+                                Layer.BASE to LayerMode.DRAW,
                                 Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
-                                Layer.TOP_LAYER to LayerMode.DRAW
+                                Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
                             ),
                             EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
                             Color.RED
@@ -503,8 +508,8 @@
         var bitmap: Bitmap? = null
         handler.post {
             bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
-                    WatchfaceScreenshotParams(
+                interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+                    WatchFaceRenderParams(
                         RenderParameters(
                             DrawMode.INTERACTIVE,
                             RenderParameters.DRAW_ALL_LAYERS,
@@ -559,9 +564,9 @@
                         0,
                         0
                     ),
-                    SystemState(false, 0),
+                    WatchUiState(false, 0),
                     UserStyleWireFormat(
-                        mapOf(COLOR_STYLE_SETTING to GREEN_STYLE)
+                        mapOf(COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray())
                     ),
                     null
                 )
@@ -591,16 +596,16 @@
 
         ComplicationTapActivity.newCountDown()
         handler.post {
-            val interactiveWatchFaceInstanceSysUi =
+            val interactiveWatchFaceInstance =
                 InteractiveInstanceManager.getAndRetainInstance(
-                    interactiveWatchFaceInstanceWCS.instanceId
-                )!!.createSysUiApi()
-            interactiveWatchFaceInstanceSysUi.sendTouchEvent(
+                    interactiveWatchFaceInstance.instanceId
+                )!!
+            interactiveWatchFaceInstance.sendTouchEvent(
                 85,
                 165,
-                TapType.TAP
+                TapType.UP
             )
-            interactiveWatchFaceInstanceSysUi.release()
+            interactiveWatchFaceInstance.release()
         }
 
         assertThat(ComplicationTapActivity.awaitIntent()).isNotNull()
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
index 2eb3ae2..f39f8e4 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
@@ -33,7 +33,7 @@
 import androidx.wear.complications.data.EmptyComplicationData
 import androidx.wear.watchface.data.ComplicationBoundsType
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
 import java.lang.ref.WeakReference
 
@@ -47,12 +47,12 @@
  * supported, however complications can be enabled and disabled by [ComplicationsUserStyleSetting].
  *
  * @param complicationCollection The complications associated with the watch face, may be empty.
- * @param userStyleRepository The [UserStyleRepository] used to listen for
+ * @param currentUserStyleRepository The [CurrentUserStyleRepository] used to listen for
  *     [ComplicationsUserStyleSetting] changes and apply them.
  */
 public class ComplicationsManager(
     complicationCollection: Collection<Complication>,
-    private val userStyleRepository: UserStyleRepository
+    private val currentUserStyleRepository: CurrentUserStyleRepository
 ) {
     /**
      * Interface used to report user taps on the complication. See [addTapListener] and
@@ -100,9 +100,9 @@
     @VisibleForTesting
     internal constructor(
         complicationCollection: Collection<Complication>,
-        userStyleRepository: UserStyleRepository,
+        currentUserStyleRepository: CurrentUserStyleRepository,
         renderer: Renderer
-    ) : this(complicationCollection, userStyleRepository) {
+    ) : this(complicationCollection, currentUserStyleRepository) {
         this.renderer = renderer
     }
 
@@ -122,7 +122,7 @@
         }
 
         val complicationsStyleCategory =
-            userStyleRepository.schema.userStyleSettings.firstOrNull {
+            currentUserStyleRepository.schema.userStyleSettings.firstOrNull {
                 it is ComplicationsUserStyleSetting
             }
 
@@ -131,8 +131,8 @@
         if (complicationsStyleCategory != null) {
             // Ensure we apply any initial StyleCategoryOption overlay by initializing with null.
             var previousOption: ComplicationsUserStyleSetting.ComplicationsOption? = null
-            userStyleRepository.addUserStyleListener(
-                object : UserStyleRepository.UserStyleListener {
+            currentUserStyleRepository.addUserStyleChangeListener(
+                object : CurrentUserStyleRepository.UserStyleChangeListener {
                     override fun onUserStyleChanged(userStyle: UserStyle) {
                         val newlySelectedOption =
                             userStyle[complicationsStyleCategory]?.toComplicationsOption()!!
@@ -230,7 +230,7 @@
                         complication.id,
                         complication.defaultProviderPolicy.providersAsList(),
                         complication.defaultProviderPolicy.systemProviderFallback,
-                        complication.defaultProviderType.asWireComplicationType()
+                        complication.defaultProviderType.toWireComplicationType()
                     )
                 }
 
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
index 4870e75..92fd683 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
@@ -39,7 +39,7 @@
 import androidx.wear.utility.TraceEvent
 import androidx.wear.watchface.Renderer.CanvasRenderer
 import androidx.wear.watchface.Renderer.GlesRenderer
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import java.nio.ByteBuffer
 
 /**
@@ -92,7 +92,7 @@
  * The base class for [CanvasRenderer] and [GlesRenderer].
  *
  * @param surfaceHolder The [SurfaceHolder] that [renderInternal] will draw into.
- * @param userStyleRepository The associated [UserStyleRepository].
+ * @param currentUserStyleRepository The associated [CurrentUserStyleRepository].
  * @param watchState The associated [WatchState].
  * @param interactiveDrawModeUpdateDelayMillis The interval in milliseconds between frames in
  *     interactive [DrawMode]s. To render at 60hz set to 16. Note when battery is low, the frame
@@ -103,7 +103,7 @@
  */
 public sealed class Renderer(
     public val surfaceHolder: SurfaceHolder,
-    private val userStyleRepository: UserStyleRepository,
+    private val currentUserStyleRepository: CurrentUserStyleRepository,
     internal val watchState: WatchState,
     @IntRange(from = 0, to = 60000)
     public var interactiveDrawModeUpdateDelayMillis: Long,
@@ -169,7 +169,7 @@
 
     /**
      * Renders the watch face into the [surfaceHolder] using the current [renderParameters]
-     * with the user style specified by the [userStyleRepository].
+     * with the user style specified by the [currentUserStyleRepository].
      *
      * @param calendar The Calendar to use when rendering the watch face
      * @return A [Bitmap] containing a screenshot of the watch face
@@ -180,7 +180,7 @@
 
     /**
      * Renders the watch face into a Bitmap with the user style specified by the
-     * [userStyleRepository].
+     * [currentUserStyleRepository].
      *
      * @param calendar The Calendar to use when rendering the watch face
      * @param renderParameters The [RenderParameters] to use when rendering the watch face
@@ -265,7 +265,7 @@
      *
      * @param surfaceHolder The [SurfaceHolder] from which a [Canvas] to will be obtained and passed
      *     into [render].
-     * @param userStyleRepository The watch face's associated [UserStyleRepository].
+     * @param currentUserStyleRepository The watch face's associated [CurrentUserStyleRepository].
      * @param watchState The watch face's associated [WatchState].
      * @param canvasType The type of canvas to request.
      * @param interactiveDrawModeUpdateDelayMillis The interval in milliseconds between frames in
@@ -277,14 +277,14 @@
      */
     public abstract class CanvasRenderer(
         surfaceHolder: SurfaceHolder,
-        userStyleRepository: UserStyleRepository,
+        currentUserStyleRepository: CurrentUserStyleRepository,
         watchState: WatchState,
         @CanvasType private val canvasType: Int,
         @IntRange(from = 0, to = 60000)
         interactiveDrawModeUpdateDelayMillis: Long
     ) : Renderer(
         surfaceHolder,
-        userStyleRepository,
+        currentUserStyleRepository,
         watchState,
         interactiveDrawModeUpdateDelayMillis
     ) {
@@ -364,7 +364,7 @@
      *
      * @param surfaceHolder The [SurfaceHolder] whose [android.view.Surface] [render] will draw
      *     into.
-     * @param userStyleRepository The associated [UserStyleRepository].
+     * @param currentUserStyleRepository The associated [CurrentUserStyleRepository].
      * @param watchState The associated [WatchState].
      * @param interactiveDrawModeUpdateDelayMillis The interval in milliseconds between frames in
      *     interactive [DrawMode]s. To render at 60hz set to 16. Note when battery is low, the
@@ -379,7 +379,7 @@
      */
     public abstract class GlesRenderer @JvmOverloads constructor(
         surfaceHolder: SurfaceHolder,
-        userStyleRepository: UserStyleRepository,
+        currentUserStyleRepository: CurrentUserStyleRepository,
         watchState: WatchState,
         @IntRange(from = 0, to = 60000)
         interactiveDrawModeUpdateDelayMillis: Long,
@@ -387,7 +387,7 @@
         private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
     ) : Renderer(
         surfaceHolder,
-        userStyleRepository,
+        currentUserStyleRepository,
         watchState,
         interactiveDrawModeUpdateDelayMillis
     ) {
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index a26ac652..e541719 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -31,6 +31,7 @@
 import android.os.BatteryManager
 import android.os.Build
 import android.support.wearable.watchface.WatchFaceStyle
+import android.util.Base64
 import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
 import android.view.ViewConfiguration
 import androidx.annotation.ColorInt
@@ -45,8 +46,9 @@
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleData
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import kotlinx.coroutines.CompletableDeferred
 import java.io.FileNotFoundException
@@ -84,13 +86,13 @@
 }
 
 private fun readPrefs(context: Context, fileName: String): UserStyleWireFormat {
-    val hashMap = HashMap<String, String>()
+    val hashMap = HashMap<String, ByteArray>()
     try {
         val reader = InputStreamReader(context.openFileInput(fileName)).buffered()
         while (true) {
             val key = reader.readLine() ?: break
             val value = reader.readLine() ?: break
-            hashMap[key] = value
+            hashMap[key] = Base64.decode(value, Base64.NO_WRAP)
         }
         reader.close()
     } catch (e: FileNotFoundException) {
@@ -102,9 +104,9 @@
 private fun writePrefs(context: Context, fileName: String, style: UserStyle) {
     val writer = context.openFileOutput(fileName, Context.MODE_PRIVATE).bufferedWriter()
     for ((key, value) in style.selectedOptions) {
-        writer.write(key.id)
+        writer.write(key.id.value)
         writer.newLine()
-        writer.write(value.id)
+        writer.write(Base64.encodeToString(value.id.value, Base64.NO_WRAP))
         writer.newLine()
     }
     writer.close()
@@ -116,16 +118,16 @@
  *
  * @param watchFaceType The type of watch face, whether it's digital or analog. Used to determine
  *     the default time for editor preview screenshots.
- * @param userStyleRepository The [UserStyleRepository] for this WatchFace.
+ * @param currentUserStyleRepository The [CurrentUserStyleRepository] for this WatchFace.
  * @param renderer The [Renderer] for this WatchFace.
  * @param complicationsManager The [ComplicationsManager] for this WatchFace.
  */
 public class WatchFace @JvmOverloads constructor(
     @WatchFaceType internal var watchFaceType: Int,
-    public val userStyleRepository: UserStyleRepository,
+    public val currentUserStyleRepository: CurrentUserStyleRepository,
     internal val renderer: Renderer,
     internal var complicationsManager: ComplicationsManager =
-        ComplicationsManager(emptyList(), userStyleRepository)
+        ComplicationsManager(emptyList(), currentUserStyleRepository)
 ) {
     internal var tapListener: TapListener? = null
 
@@ -220,8 +222,8 @@
         /** The UTC reference time to use for previews in milliseconds since the epoch. */
         public val previewReferenceTimeMillis: Long
 
-        /** Takes a screenshot with the [UserStyleRepository]'s [UserStyle]. */
-        public fun takeScreenshot(
+        /** Renders the watchface to a [Bitmap] with the [CurrentUserStyleRepository]'s [UserStyle]. */
+        public fun renderWatchFaceToBitmap(
             renderParameters: RenderParameters,
             calendarTimeMillis: Long,
             idToComplicationData: Map<Int, ComplicationData>?
@@ -266,9 +268,9 @@
      *          only the vertical gravity is respected.
      * @param tapEventsAccepted Controls whether this watch face accepts tap events. Watchfaces
      *     that set this {@code true} are indicating they are prepared to receive
-     *     [IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH],
-     *     [IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH_CANCEL], and
-     *     [IInteractiveWatchFaceSysUI.TAP_TYPE_TAP] events.
+     *     [IInteractiveWatchFaceSysUI.TAP_TYPE_DOWN],
+     *     [IInteractiveWatchFaceSysUI.TAP_TYPE_CANCEL], and
+     *     [IInteractiveWatchFaceSysUI.TAP_TYPE_UP] events.
      * @param accentColor The accent color which will be used when drawing the unread notification
      *     indicator. Default color is white.
      * @throws IllegalArgumentException if [viewProtectionMode] has an unexpected value
@@ -392,7 +394,7 @@
 
     private val systemTimeProvider = watchface.systemTimeProvider
     private val legacyWatchFaceStyle = watchface.legacyWatchFaceStyle
-    internal val userStyleRepository = watchface.userStyleRepository
+    internal val userStyleRepository = watchface.currentUserStyleRepository
     internal val renderer = watchface.renderer
     internal val complicationsManager = watchface.complicationsManager
     private val tapListener = watchface.tapListener
@@ -508,19 +510,19 @@
         val storedUserStyle = watchFaceHostApi.getInitialUserStyle()
         if (storedUserStyle != null) {
             userStyleRepository.userStyle =
-                UserStyle(storedUserStyle, userStyleRepository.schema)
+                UserStyle(UserStyleData(storedUserStyle), userStyleRepository.schema)
         } else {
             // The system doesn't support preference persistence we need to do it ourselves.
             val preferencesFile =
                 "watchface_prefs_${watchFaceHostApi.getContext().javaClass.name}.txt"
 
             userStyleRepository.userStyle = UserStyle(
-                readPrefs(watchFaceHostApi.getContext(), preferencesFile),
+                UserStyleData(readPrefs(watchFaceHostApi.getContext(), preferencesFile)),
                 userStyleRepository.schema
             )
 
-            userStyleRepository.addUserStyleListener(
-                object : UserStyleRepository.UserStyleListener {
+            userStyleRepository.addUserStyleChangeListener(
+                object : CurrentUserStyleRepository.UserStyleChangeListener {
                     @SuppressLint("SyntheticAccessor")
                     override fun onUserStyleChanged(userStyle: UserStyle) {
                         writePrefs(watchFaceHostApi.getContext(), preferencesFile, userStyle)
@@ -647,7 +649,7 @@
         override val previewReferenceTimeMillis
             get() = [email protected]
 
-        override fun takeScreenshot(
+        override fun renderWatchFaceToBitmap(
             renderParameters: RenderParameters,
             calendarTimeMillis: Long,
             idToComplicationData: Map<Int, ComplicationData>?
@@ -899,13 +901,13 @@
         // TODO(alexclarke): Revisit this
         var tapType = originalTapType
         when (tapType) {
-            TapType.TOUCH -> {
+            TapType.DOWN -> {
                 lastTappedPosition = Point(x, y)
             }
-            TapType.TOUCH_CANCEL -> {
+            TapType.CANCEL -> {
                 lastTappedPosition?.let { safeLastTappedPosition ->
                     if ((safeLastTappedPosition.x == x) && (safeLastTappedPosition.y == y)) {
-                        tapType = TapType.TAP
+                        tapType = TapType.UP
                     }
                 }
                 lastTappedPosition = null
@@ -913,7 +915,7 @@
         }
 
         when (tapType) {
-            TapType.TAP -> {
+            TapType.UP -> {
                 if (tappedComplication.id != lastTappedComplicationId &&
                     lastTappedComplicationId != null
                 ) {
@@ -938,7 +940,7 @@
                     }
                 }
             }
-            TapType.TOUCH -> {
+            TapType.DOWN -> {
                 // Make sure the user isn't doing a swipe.
                 if (tappedComplication.id != lastTappedComplicationId &&
                     lastTappedComplicationId != null
@@ -970,8 +972,8 @@
         writer.println("pendingUpdateTime=${pendingUpdateTime.isPending()}")
         writer.println("lastTappedComplicationId=$lastTappedComplicationId")
         writer.println("lastTappedPosition=$lastTappedPosition")
-        writer.println("userStyleRepository.userStyle=${userStyleRepository.userStyle}")
-        writer.println("userStyleRepository.schema=${userStyleRepository.schema}")
+        writer.println("currentUserStyleRepository.userStyle=${userStyleRepository.userStyle}")
+        writer.println("currentUserStyleRepository.schema=${userStyleRepository.schema}")
         watchState.dump(writer)
         complicationsManager.dump(writer)
         renderer.dump(writer)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 6df1310..61b0832 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -49,29 +49,31 @@
 import androidx.wear.complications.SystemProviders.ProviderId
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.complications.data.ComplicationType
-import androidx.wear.complications.data.asApiComplicationData
+import androidx.wear.complications.data.toApiComplicationData
 import androidx.wear.utility.AsyncTraceEvent
 import androidx.wear.utility.TraceEvent
 import androidx.wear.watchface.control.HeadlessWatchFaceImpl
-import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI
+import androidx.wear.watchface.control.IInteractiveWatchFace
 import androidx.wear.watchface.control.InteractiveInstanceManager
 import androidx.wear.watchface.control.InteractiveWatchFaceImpl
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams
+import androidx.wear.watchface.control.data.ComplicationRenderParams
 import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
 import androidx.wear.watchface.data.ComplicationStateWireFormat
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.data.IdAndComplicationStateWireFormat
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
 import androidx.wear.watchface.editor.EditorService
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.UserStyleData
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.launch
 import java.io.FileDescriptor
 import java.io.FileNotFoundException
@@ -93,29 +95,31 @@
 /** @hide */
 @IntDef(
     value = [
-        TapType.TOUCH,
-        TapType.TOUCH_CANCEL,
-        TapType.TAP
+        TapType.DOWN,
+        TapType.UP,
+        TapType.CANCEL
     ]
 )
 public annotation class TapType {
     public companion object {
-        /** Used in [WatchFaceImpl#onTapCommand] to indicate a "down" touch event on the watch face. */
-        public const val TOUCH: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH
+        /**
+         * Used in [WatchFaceImpl#onTapCommand] to indicate a "down" touch event on the watch face.
+         */
+        public const val DOWN: Int = IInteractiveWatchFace.TAP_TYPE_DOWN
 
         /**
-         * Used in [WatchFaceImpl#onTapCommand] to indicate that a previous TapType.TOUCH touch event
-         * has been canceled. This generally happens when the watch face is touched but then a
+         * Used in [WatchFaceImpl#onTapCommand] to indicate that a previous [TapType.DOWN] touch
+         * event has been canceled. This generally happens when the watch face is touched but then a
          * move or long press occurs.
          */
-        public const val TOUCH_CANCEL: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH_CANCEL
+        public const val CANCEL: Int = IInteractiveWatchFace.TAP_TYPE_CANCEL
 
         /**
          * Used in [WatchFaceImpl#onTapCommand] to indicate that an "up" event on the watch face has
-         * occurred that has not been consumed by another activity. A TapType.TOUCH will always
-         * occur first. This event will not occur if a TapType.TOUCH_CANCEL is sent.
+         * occurred that has not been consumed by another activity. A [TapType.DOWN] will always
+         * occur first. This event will not occur if a [TapType.CANCEL] is sent.
          */
-        public const val TAP: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TAP
+        public const val UP: Int = IInteractiveWatchFace.TAP_TYPE_UP
     }
 }
 
@@ -136,7 +140,7 @@
  *
  * Watch face styling (color and visual look of watch face elements such as numeric fonts, watch
  * hands and ticks, etc...) is directly supported via [UserStyleSetting] and
- * [UserStyleRepository].
+ * [CurrentUserStyleRepository].
  *
  * To aid debugging watch face animations, WatchFaceService allows you to speed up or slow down
  * time, and to loop between two instants.  This is controlled by MOCK_TIME_INTENT intents
@@ -208,7 +212,10 @@
         /** Whether to log every frame. */
         private const val LOG_VERBOSE = false
 
-        /** Whether to enable tracing for each call to [Engine.onDraw]. */
+        /**
+         * Whether to enable tracing for each call to [WatchFaceImpl.onDraw()] and
+         * [WatchFaceImpl.onSurfaceRedrawNeeded()]
+         */
         private const val TRACE_DRAW = false
 
         // Reference time for editor screenshots for analog watch faces.
@@ -366,7 +373,7 @@
         // Only valid after onSetBinder has been called.
         private var systemApiVersion = -1
 
-        internal var firstSetSystemState = true
+        internal var firstSetWatchUiState = true
         internal var immutableSystemStateDone = false
         private var ignoreNextOnVisibilityChanged = false
 
@@ -403,7 +410,7 @@
                             createInteractiveInstance(
                                 directBootParams!!,
                                 "DirectBoot"
-                            ).createWCSApi()
+                            )
                             asyncTraceEvent.close()
                         }
                     }
@@ -421,11 +428,11 @@
                 // workaround the workaround...
                 ignoreNextOnVisibilityChanged = true
                 coroutineScope.launch {
-                    pendingWallpaperInstance.callback.onInteractiveWatchFaceWcsCreated(
+                    pendingWallpaperInstance.callback.onInteractiveWatchFaceCreated(
                         createInteractiveInstance(
                             pendingWallpaperInstance.params,
                             "Boot with pendingWallpaperInstance"
-                        ).createWCSApi()
+                        )
                     )
                     asyncTraceEvent.close()
                     val params = pendingWallpaperInstance.params
@@ -475,20 +482,20 @@
         }
 
         @UiThread
-        fun setSystemState(systemState: SystemState) {
-            if (firstSetSystemState ||
-                systemState.inAmbientMode != mutableWatchState.isAmbient.value
+        fun setWatchUiState(watchUiState: WatchUiState) {
+            if (firstSetWatchUiState ||
+                watchUiState.inAmbientMode != mutableWatchState.isAmbient.value
             ) {
-                mutableWatchState.isAmbient.value = systemState.inAmbientMode
+                mutableWatchState.isAmbient.value = watchUiState.inAmbientMode
             }
 
-            if (firstSetSystemState ||
-                systemState.interruptionFilter != mutableWatchState.interruptionFilter.value
+            if (firstSetWatchUiState ||
+                watchUiState.interruptionFilter != mutableWatchState.interruptionFilter.value
             ) {
-                mutableWatchState.interruptionFilter.value = systemState.interruptionFilter
+                mutableWatchState.interruptionFilter.value = watchUiState.interruptionFilter
             }
 
-            firstSetSystemState = false
+            firstSetWatchUiState = false
         }
 
         @UiThread
@@ -496,7 +503,7 @@
             userStyle: UserStyleWireFormat
         ): Unit = TraceEvent("EngineWrapper.setUserStyle").use {
             watchFaceImpl.onSetStyleInternal(
-                UserStyle(userStyle, watchFaceImpl.userStyleRepository.schema)
+                UserStyle(UserStyleData(userStyle), watchFaceImpl.userStyleRepository.schema)
             )
             onUserStyleChanged()
         }
@@ -550,11 +557,11 @@
                             ComplicationType.toWireTypes(it.value.supportedTypes),
                             it.value.defaultProviderPolicy.providersAsList(),
                             it.value.defaultProviderPolicy.systemProviderFallback,
-                            it.value.defaultProviderType.asWireComplicationType(),
+                            it.value.defaultProviderType.toWireComplicationType(),
                             it.value.enabled,
                             it.value.initiallyEnabled,
-                            it.value.renderer.getData()?.type?.asWireComplicationType()
-                                ?: ComplicationType.NO_DATA.asWireComplicationType(),
+                            it.value.renderer.getData()?.type?.toWireComplicationType()
+                                ?: ComplicationType.NO_DATA.toWireComplicationType(),
                             it.value.fixedComplicationProvider,
                             it.value.configExtras
                         )
@@ -570,7 +577,7 @@
                 for (idAndComplicationData in complicationDatumWireFormats) {
                     watchFaceImpl.onComplicationDataUpdate(
                         idAndComplicationData.id,
-                        idAndComplicationData.complicationData.asApiComplicationData()
+                        idAndComplicationData.complicationData.toApiComplicationData()
                     )
                 }
             } else {
@@ -578,7 +585,7 @@
                     pendingComplicationDataUpdates.add(
                         PendingComplicationData(
                             idAndComplicationData.id,
-                            idAndComplicationData.complicationData.asApiComplicationData()
+                            idAndComplicationData.complicationData.toApiComplicationData()
                         )
                     )
                 }
@@ -605,13 +612,13 @@
 
         @UiThread
         @RequiresApi(27)
-        fun takeWatchFaceScreenshot(
-            params: WatchfaceScreenshotParams
-        ): Bundle = TraceEvent("EngineWrapper.takeWatchFaceScreenshot").use {
+        fun renderWatchFaceToBitmap(
+            params: WatchFaceRenderParams
+        ): Bundle = TraceEvent("EngineWrapper.renderWatchFaceToBitmap").use {
             val oldStyle = HashMap(watchFaceImpl.userStyleRepository.userStyle.selectedOptions)
             params.userStyle?.let {
                 watchFaceImpl.onSetStyleInternal(
-                    UserStyle(it, watchFaceImpl.userStyleRepository.schema)
+                    UserStyle(UserStyleData(it), watchFaceImpl.userStyleRepository.schema)
                 )
             }
 
@@ -624,7 +631,7 @@
             params.idAndComplicationDatumWireFormats?.let {
                 for (idAndData in it) {
                     watchFaceImpl.complicationsManager[idAndData.id]!!.renderer
-                        .loadData(idAndData.complicationData.asApiComplicationData(), false)
+                        .loadData(idAndData.complicationData.toApiComplicationData(), false)
                 }
             }
 
@@ -651,9 +658,9 @@
 
         @UiThread
         @RequiresApi(27)
-        fun takeComplicationScreenshot(
-            params: ComplicationScreenshotParams
-        ): Bundle? = TraceEvent("EngineWrapper.takeComplicationScreenshot").use {
+        fun renderComplicationToBitmap(
+            params: ComplicationRenderParams
+        ): Bundle? = TraceEvent("EngineWrapper.renderComplicationToBitmap").use {
             val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
                 timeInMillis = params.calendarTimeMillis
             }
@@ -662,7 +669,7 @@
                 val newStyle = params.userStyle
                 if (newStyle != null) {
                     watchFaceImpl.onSetStyleInternal(
-                        UserStyle(newStyle, watchFaceImpl.userStyleRepository.schema)
+                        UserStyle(UserStyleData(newStyle), watchFaceImpl.userStyleRepository.schema)
                     )
                 }
 
@@ -678,7 +685,7 @@
                 if (screenshotComplicationData != null) {
                     prevData = it.renderer.getData()
                     it.renderer.loadData(
-                        screenshotComplicationData.asApiComplicationData(),
+                        screenshotComplicationData.toApiComplicationData(),
                         false
                     )
                 }
@@ -771,6 +778,7 @@
 
         override fun onDestroy(): Unit = TraceEvent("EngineWrapper.onDestroy").use {
             destroyed = true
+            coroutineScope.cancel()
             uiThreadHandler.removeCallbacks(invalidateRunnable)
             if (this::choreographer.isInitialized) {
                 choreographer.removeFrameCallback(frameCallback)
@@ -827,15 +835,15 @@
                     }
                 Constants.COMMAND_TAP ->
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_TAP") {
-                        sendTouchEvent(x, y, TapType.TAP)
+                        sendTouchEvent(x, y, TapType.UP)
                     }
                 Constants.COMMAND_TOUCH ->
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_TOUCH") {
-                        sendTouchEvent(x, y, TapType.TOUCH)
+                        sendTouchEvent(x, y, TapType.DOWN)
                     }
                 Constants.COMMAND_TOUCH_CANCEL ->
                     uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_TOUCH_CANCEL") {
-                        sendTouchEvent(x, y, TapType.TOUCH_CANCEL)
+                        sendTouchEvent(x, y, TapType.CANCEL)
                     }
                 else -> {
                 }
@@ -852,8 +860,8 @@
                 return
             }
 
-            setSystemState(
-                SystemState(
+            setWatchUiState(
+                WatchUiState(
                     extras.getBoolean(
                         Constants.EXTRA_AMBIENT_MODE,
                         mutableWatchState.isAmbient.getValueOr(false)
@@ -983,7 +991,7 @@
             require(!mutableWatchState.isHeadless)
 
             setImmutableSystemState(params.deviceConfig)
-            setSystemState(params.systemState)
+            setWatchUiState(params.watchUiState)
             initialUserStyle = params.userStyle
 
             val watchState = mutableWatchState.asWatchState()
@@ -1015,7 +1023,7 @@
                     "Miss match between pendingWallpaperInstance id $it.params.instanceId and " +
                         "constructed instance id $params.instanceId"
                 }
-                it.callback.onInteractiveWatchFaceWcsCreated(instance.createWCSApi())
+                it.callback.onInteractiveWatchFaceCreated(instance)
             }
 
             return instance
@@ -1178,7 +1186,7 @@
                 extras.getParcelable(Constants.EXTRA_COMPLICATION_DATA)!!
             setComplicationData(
                 extras.getInt(Constants.EXTRA_COMPLICATION_ID),
-                complicationData.asApiComplicationData()
+                complicationData.toApiComplicationData()
             )
         }
 
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index 3fb6e55..effcd4a 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -21,8 +21,8 @@
 import androidx.annotation.UiThread
 import androidx.wear.watchface.IndentingPrintWriter
 import androidx.wear.watchface.WatchFaceService
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.ComplicationRenderParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
 import androidx.wear.watchface.runOnHandlerWithTracing
 
 /**
@@ -63,9 +63,9 @@
 
     override fun getApiVersion() = IHeadlessWatchFace.API_VERSION
 
-    override fun takeWatchFaceScreenshot(params: WatchfaceScreenshotParams) =
-        uiThreadHandler.runOnHandlerWithTracing("HeadlessWatchFaceImpl.takeWatchFaceScreenshot") {
-            engine!!.takeWatchFaceScreenshot(params)
+    override fun renderWatchFaceToBitmap(params: WatchFaceRenderParams) =
+        uiThreadHandler.runOnHandlerWithTracing("HeadlessWatchFaceImpl.renderWatchFaceToBitmap") {
+            engine!!.renderWatchFaceToBitmap(params)
         }
 
     override fun getPreviewReferenceTimeMillis() = engine!!.watchFaceImpl.previewReferenceTimeMillis
@@ -75,11 +75,11 @@
             engine!!.getComplicationState()
         }
 
-    override fun takeComplicationScreenshot(params: ComplicationScreenshotParams) =
+    override fun renderComplicationToBitmap(params: ComplicationRenderParams) =
         uiThreadHandler.runOnHandlerWithTracing(
-            "HeadlessWatchFaceImpl.takeComplicationScreenshot"
+            "HeadlessWatchFaceImpl.renderComplicationToBitmap"
         ) {
-            engine!!.takeComplicationScreenshot(params)
+            engine!!.renderComplicationToBitmap(params)
         }
 
     override fun getUserStyleSchema() =
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
index 6f759ee..0fe64b8 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
@@ -43,7 +43,7 @@
 
     class PendingWallpaperInteractiveWatchFaceInstance(
         val params: WallpaperInteractiveWatchFaceInstanceParams,
-        val callback: IPendingInteractiveWatchFaceWCS
+        val callback: IPendingInteractiveWatchFace
     )
 
     companion object {
@@ -108,11 +108,11 @@
         @RequiresApi(27)
         fun getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
             value: PendingWallpaperInteractiveWatchFaceInstance
-        ): IInteractiveWatchFaceWCS? {
+        ): IInteractiveWatchFace? {
             synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
                 val instance = instances[value.params.instanceId]
                 return if (instance != null) {
-                    instance.impl.createWCSApi()
+                    instance.impl
                 } else {
                     pendingWallpaperInteractiveWatchFaceInstance = value
                     null
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index 0d70ea3..4f537a63 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -20,9 +20,9 @@
 import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
 import androidx.annotation.RequiresApi
 import androidx.wear.watchface.WatchFaceService
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
 import androidx.wear.watchface.runOnHandlerWithTracing
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 
@@ -32,118 +32,88 @@
     internal val engine: WatchFaceService.EngineWrapper,
     internal var instanceId: String,
     private val uiThreadHandler: Handler
-) {
-    fun createSysUiApi() = SysUiApi(engine, this, uiThreadHandler)
+) : IInteractiveWatchFace.Stub() {
 
-    fun createWCSApi() = WCSApi(engine, this, uiThreadHandler)
-}
-
-/** The interface for SysUI. */
-@RequiresApi(27)
-internal class SysUiApi(
-    private val engine: WatchFaceService.EngineWrapper,
-    private val instance: InteractiveWatchFaceImpl,
-    private val uiThreadHandler: Handler
-) : IInteractiveWatchFaceSysUI.Stub() {
-    override fun getApiVersion() = IInteractiveWatchFaceSysUI.API_VERSION
+    override fun getApiVersion() = IInteractiveWatchFace.API_VERSION
 
     override fun sendTouchEvent(xPos: Int, yPos: Int, tapType: Int) {
-        uiThreadHandler.runOnHandlerWithTracing("SysUiApi.sendTouchEvent") {
+        uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.sendTouchEvent") {
             engine.sendTouchEvent(xPos, yPos, tapType)
         }
     }
 
     override fun getContentDescriptionLabels(): Array<ContentDescriptionLabel> =
-        uiThreadHandler.runOnHandlerWithTracing("SysUiApi.getContentDescriptionLabels") {
+        uiThreadHandler.runOnHandlerWithTracing(
+            "InteractiveWatchFaceImpl.getContentDescriptionLabels"
+        ) {
             engine.watchFaceImpl.complicationsManager.getContentDescriptionLabels()
         }
 
-    override fun takeWatchFaceScreenshot(params: WatchfaceScreenshotParams) =
-        uiThreadHandler.runOnHandlerWithTracing("SysUiApi.takeWatchFaceScreenshot") {
-            engine.takeWatchFaceScreenshot(params)
+    override fun renderWatchFaceToBitmap(params: WatchFaceRenderParams) =
+        uiThreadHandler.runOnHandlerWithTracing(
+            "InteractiveWatchFaceImpl.renderWatchFaceToBitmap"
+        ) {
+            engine.renderWatchFaceToBitmap(params)
         }
 
     override fun getPreviewReferenceTimeMillis() = engine.watchFaceImpl.previewReferenceTimeMillis
 
-    override fun setSystemState(systemState: SystemState) {
-        uiThreadHandler.runOnHandlerWithTracing("SysUiApi.setSystemState") {
-            engine.setSystemState(systemState)
+    override fun setWatchUiState(watchUiState: WatchUiState) {
+        uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.setSystemState") {
+            engine.setWatchUiState(watchUiState)
         }
     }
 
-    override fun getInstanceId(): String = instance.instanceId
+    override fun getInstanceId(): String = instanceId
 
     override fun ambientTickUpdate() {
-        uiThreadHandler.runOnHandlerWithTracing("SysUiApi.ambientTickUpdate") {
+        uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.ambientTickUpdate") {
             engine.ambientTickUpdate()
         }
     }
 
     override fun release() {
-        uiThreadHandler.runOnHandlerWithTracing("SysUiApi.release") {
+        uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.release") {
             InteractiveInstanceManager.releaseInstance(instanceId)
         }
     }
-}
-
-/** The interface for WCS. */
-@RequiresApi(27)
-internal class WCSApi(
-    private val engine: WatchFaceService.EngineWrapper,
-    private val instance: InteractiveWatchFaceImpl,
-    private val uiThreadHandler: Handler
-) : IInteractiveWatchFaceWCS.Stub() {
-    override fun getApiVersion() = IInteractiveWatchFaceWCS.API_VERSION
 
     override fun updateComplicationData(
         complicationDatumWireFormats: MutableList<IdAndComplicationDataWireFormat>
     ) {
-        uiThreadHandler.runOnHandlerWithTracing("WCSApi.updateComplicationData") {
+        uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.updateComplicationData") {
             engine.setComplicationDataList(complicationDatumWireFormats)
         }
     }
 
-    override fun takeWatchFaceScreenshot(params: WatchfaceScreenshotParams) =
-        uiThreadHandler.runOnHandlerWithTracing("WCSApi.takeWatchFaceScreenshot") {
-            engine.takeWatchFaceScreenshot(params)
-        }
-
-    override fun getPreviewReferenceTimeMillis() = engine.watchFaceImpl.previewReferenceTimeMillis
-
-    override fun updateInstance(
+    override fun updateWatchfaceInstance(
         newInstanceId: String,
         userStyle: UserStyleWireFormat
     ) {
-        uiThreadHandler.runOnHandlerWithTracing("WCSApi.updateInstance") {
-            if (instance.instanceId != newInstanceId) {
-                InteractiveInstanceManager.renameInstance(instance.instanceId, newInstanceId)
-                instance.instanceId = newInstanceId
+        uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.updateInstance") {
+            if (instanceId != newInstanceId) {
+                InteractiveInstanceManager.renameInstance(instanceId, newInstanceId)
+                instanceId = newInstanceId
             }
             engine.setUserStyle(userStyle)
             engine.clearComplicationData()
         }
     }
 
-    override fun getInstanceId(): String = instance.instanceId
-
-    override fun release() {
-        uiThreadHandler.runOnHandlerWithTracing("WCSApi.release") {
-            InteractiveInstanceManager.releaseInstance(instanceId)
-        }
-    }
-
     override fun getComplicationDetails() =
-        uiThreadHandler.runOnHandlerWithTracing("WCSApi.getComplicationDetails") {
+        uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.getComplicationDetails") {
             engine.getComplicationState()
         }
 
     override fun getUserStyleSchema() =
-        uiThreadHandler.runOnHandlerWithTracing("WCSApi.getUserStyleSchema") {
+        uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.getUserStyleSchema") {
             engine.watchFaceImpl.userStyleRepository.schema.toWireFormat()
         }
 
     override fun bringAttentionToComplication(id: Int) {
-        uiThreadHandler.runOnHandlerWithTracing("WCSApi.bringAttentionToComplication") {
+        uiThreadHandler.runOnHandlerWithTracing(
+            "InteractiveWatchFaceImpl.bringAttentionToComplication"
+        ) {
             engine.watchFaceImpl.complicationsManager.displayPressedAnimation(id)
         }
     }
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index 5a07abc..d0974e9 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -103,10 +103,10 @@
 ) : IWatchFaceControlService.Stub() {
     override fun getApiVersion() = IWatchFaceControlService.API_VERSION
 
-    override fun getInteractiveWatchFaceInstanceSysUI(instanceId: String) =
-        TraceEvent("IWatchFaceInstanceServiceStub.getInteractiveWatchFaceInstanceSysUI").use {
+    override fun getInteractiveWatchFaceInstance(instanceId: String) =
+        TraceEvent("IWatchFaceInstanceServiceStub.getInteractiveWatchFaceInstance").use {
             // This call is thread safe so we don't need to trampoline via the UI thread.
-            InteractiveInstanceManager.getAndRetainInstance(instanceId)?.createSysUiApi()
+            InteractiveInstanceManager.getAndRetainInstance(instanceId)
         }
 
     override fun createHeadlessWatchFaceInstance(
@@ -144,25 +144,25 @@
         }
     }
 
-    override fun getOrCreateInteractiveWatchFaceWCS(
+    override fun getOrCreateInteractiveWatchFace(
         params: WallpaperInteractiveWatchFaceInstanceParams,
-        callback: IPendingInteractiveWatchFaceWCS
-    ): IInteractiveWatchFaceWCS? {
+        callback: IPendingInteractiveWatchFace
+    ): IInteractiveWatchFace? {
         val asyncTraceEvent =
             AsyncTraceEvent("IWatchFaceInstanceServiceStub.getOrCreateInteractiveWatchFaceWCS")
         return InteractiveInstanceManager
             .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
                 InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
                     params,
-                    // Wrapped IPendingInteractiveWatchFaceWCS to support tracing.
-                    object : IPendingInteractiveWatchFaceWCS.Stub() {
+                    // Wrapped IPendingInteractiveWatchFace to support tracing.
+                    object : IPendingInteractiveWatchFace.Stub() {
                         override fun getApiVersion() = callback.apiVersion
 
-                        override fun onInteractiveWatchFaceWcsCreated(
-                            iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS?
+                        override fun onInteractiveWatchFaceCreated(
+                            iInteractiveWatchFaceWcs: IInteractiveWatchFace?
                         ) {
                             asyncTraceEvent.close()
-                            callback.onInteractiveWatchFaceWcsCreated(iInteractiveWatchFaceWcs)
+                            callback.onInteractiveWatchFaceCreated(iInteractiveWatchFaceWcs)
                         }
                     }
                 )
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
index 719ede7..2583b43 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
@@ -21,14 +21,14 @@
 import android.os.Looper
 import android.view.SurfaceHolder
 import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace
 import androidx.wear.watchface.control.InteractiveInstanceManager
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
 import androidx.wear.watchface.data.DeviceConfig
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
@@ -94,7 +94,7 @@
     private val surfaceHolder = mock<SurfaceHolder>()
     private var looperTimeMillis = 0L
     private val pendingTasks = PriorityQueue<Task>()
-    private val userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
+    private val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
     private val initParams = WallpaperInteractiveWatchFaceInstanceParams(
         "instanceId",
         DeviceConfig(
@@ -103,7 +103,7 @@
             0,
             0
         ),
-        SystemState(false, 0),
+        WatchUiState(false, 0),
         UserStyle(emptyMap()).toWireFormat(),
         null
     )
@@ -221,7 +221,7 @@
         engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
         runPostedTasksFor(0)
 
-        var pendingInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS? = null
+        var pendingInteractiveWatchFaceWcs: IInteractiveWatchFace? = null
 
         // There shouldn't be an existing instance, so we expect null.
         assertNull(
@@ -229,12 +229,12 @@
                 .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
                     InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
                         initParams,
-                        object : IPendingInteractiveWatchFaceWCS.Stub() {
+                        object : IPendingInteractiveWatchFace.Stub() {
                             override fun getApiVersion() =
-                                IPendingInteractiveWatchFaceWCS.API_VERSION
+                                IPendingInteractiveWatchFace.API_VERSION
 
-                            override fun onInteractiveWatchFaceWcsCreated(
-                                iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS?
+                            override fun onInteractiveWatchFaceCreated(
+                                iInteractiveWatchFaceWcs: IInteractiveWatchFace?
                             ) {
                                 pendingInteractiveWatchFaceWcs = iInteractiveWatchFaceWcs
                             }
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
index 5796be5..15e579e 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
@@ -31,10 +31,10 @@
 import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
 import android.view.SurfaceHolder
 import androidx.test.core.app.ApplicationProvider
-import androidx.wear.complications.data.asApiComplicationData
+import androidx.wear.complications.data.toApiComplicationData
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import org.junit.runners.model.FrameworkMethod
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.internal.bytecode.InstrumentationConfiguration
@@ -43,7 +43,7 @@
     @WatchFaceType private val watchFaceType: Int,
     private val complicationsManager: ComplicationsManager,
     private val renderer: TestRenderer,
-    private val userStyleRepository: UserStyleRepository,
+    private val currentUserStyleRepository: CurrentUserStyleRepository,
     private val watchState: MutableWatchState,
     private val handler: Handler,
     private val tapListener: WatchFace.TapListener?,
@@ -57,8 +57,8 @@
     var lastUserStyle: UserStyle? = null
 
     init {
-        userStyleRepository.addUserStyleListener(
-            object : UserStyleRepository.UserStyleListener {
+        currentUserStyleRepository.addUserStyleChangeListener(
+            object : CurrentUserStyleRepository.UserStyleChangeListener {
                 override fun onUserStyleChanged(userStyle: UserStyle) {
                     lastUserStyle = userStyle
                 }
@@ -94,7 +94,7 @@
         watchState: WatchState
     ) = WatchFace(
         watchFaceType,
-        userStyleRepository,
+        currentUserStyleRepository,
         renderer,
         complicationsManager
     ).setSystemTimeProvider(object : WatchFace.SystemTimeProvider {
@@ -179,16 +179,20 @@
             watchFaceComplicationId, providers, fallbackSystemProvider, type
         )
     }
+
+    override fun reserved8() {
+        iWatchFaceService.reserved8()
+    }
 }
 
 open class TestRenderer(
     surfaceHolder: SurfaceHolder,
-    userStyleRepository: UserStyleRepository,
+    currentUserStyleRepository: CurrentUserStyleRepository,
     watchState: WatchState,
     interactiveFrameRateMs: Long
 ) : Renderer.CanvasRenderer(
     surfaceHolder,
-    userStyleRepository,
+    currentUserStyleRepository,
     watchState,
     CanvasType.HARDWARE,
     interactiveFrameRateMs
@@ -214,7 +218,7 @@
                 ApplicationProvider.getApplicationContext(), 0,
                 Intent("Fake intent"), 0
             )
-        ).build().asApiComplicationData()
+        ).build().toApiComplicationData()
 
 /**
  * We need to prevent roboloetric from instrumenting our classes or things break...
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index ad7f0a6..98fa565 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -42,22 +42,24 @@
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.complications.data.ShortTextComplicationData
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace
 import androidx.wear.watchface.control.InteractiveInstanceManager
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
 import androidx.wear.watchface.data.ComplicationBoundsType
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
 import org.junit.After
@@ -115,48 +117,48 @@
     private val complicationDrawableBackground = ComplicationDrawable(context)
 
     private val redStyleOption =
-        ListUserStyleSetting.ListOption("red_style", "Red", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
 
     private val greenStyleOption =
-        ListUserStyleSetting.ListOption("green_style", "Green", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
 
     private val blueStyleOption =
-        ListUserStyleSetting.ListOption("bluestyle", "Blue", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("bluestyle"), "Blue", icon = null)
 
     private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
 
     private val colorStyleSetting = ListUserStyleSetting(
-        "color_style_setting",
+        UserStyleSetting.Id("color_style_setting"),
         "Colors",
         "Watchface colorization", /* icon = */
         null,
         colorStyleList,
-        listOf(Layer.BASE_LAYER)
+        listOf(Layer.BASE)
     )
 
     private val classicStyleOption =
-        ListUserStyleSetting.ListOption("classic_style", "Classic", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
 
     private val modernStyleOption =
-        ListUserStyleSetting.ListOption("modern_style", "Modern", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
 
     private val gothicStyleOption =
-        ListUserStyleSetting.ListOption("gothic_style", "Gothic", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
 
     private val watchHandStyleList =
         listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
 
     private val watchHandStyleSetting = ListUserStyleSetting(
-        "hand_style_setting",
+        UserStyleSetting.Id("hand_style_setting"),
         "Hand Style",
         "Hand visual look", /* icon = */
         null,
         watchHandStyleList,
-        listOf(Layer.TOP_LAYER)
+        listOf(Layer.COMPLICATIONS_OVERLAY)
     )
 
     private val badStyleOption =
-        ListUserStyleSetting.ListOption("bad_option", "Bad", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("bad_option"), "Bad", icon = null)
 
     private val leftComplication =
         Complication.createRoundRectComplicationBuilder(
@@ -217,7 +219,7 @@
             .build()
 
     private val leftAndRightComplicationsOption = ComplicationsOption(
-        LEFT_AND_RIGHT_COMPLICATIONS,
+        Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
         "Both",
         null,
         listOf(
@@ -228,7 +230,7 @@
         )
     )
     private val noComplicationsOption = ComplicationsOption(
-        NO_COMPLICATIONS,
+        Option.Id(NO_COMPLICATIONS),
         "Both",
         null,
         listOf(
@@ -239,7 +241,7 @@
         )
     )
     private val leftComplicationsOption = ComplicationsOption(
-        LEFT_COMPLICATION,
+        Option.Id(LEFT_COMPLICATION),
         "Left",
         null,
         listOf(
@@ -250,7 +252,7 @@
         )
     )
     private val rightComplicationsOption = ComplicationsOption(
-        RIGHT_COMPLICATION,
+        Option.Id(RIGHT_COMPLICATION),
         "Right",
         null,
         listOf(
@@ -261,7 +263,7 @@
         )
     )
     private val complicationsStyleSetting = ComplicationsUserStyleSetting(
-        "complications_style_setting",
+        UserStyleSetting.Id("complications_style_setting"),
         "Complications",
         "Number and position",
         icon = null,
@@ -276,11 +278,11 @@
 
     private lateinit var renderer: TestRenderer
     private lateinit var complicationsManager: ComplicationsManager
-    private lateinit var userStyleRepository: UserStyleRepository
+    private lateinit var currentUserStyleRepository: CurrentUserStyleRepository
     private lateinit var watchFaceImpl: WatchFaceImpl
     private lateinit var testWatchFaceService: TestWatchFaceService
     private lateinit var engineWrapper: WatchFaceService.EngineWrapper
-    private lateinit var interactiveWatchFaceInstanceWCS: IInteractiveWatchFaceWCS
+    private lateinit var interactiveWatchFaceInstance: IInteractiveWatchFace
 
     private class Task(val runTimeMillis: Long, val runnable: Runnable) : Comparable<Task> {
         override fun compareTo(other: Task) = runTimeMillis.compareTo(other.runTimeMillis)
@@ -320,11 +322,11 @@
         hasBurnInProtection: Boolean = false,
         tapListener: WatchFace.TapListener? = null
     ) {
-        userStyleRepository = UserStyleRepository(userStyleSchema)
-        complicationsManager = ComplicationsManager(complications, userStyleRepository)
+        currentUserStyleRepository = CurrentUserStyleRepository(userStyleSchema)
+        complicationsManager = ComplicationsManager(complications, currentUserStyleRepository)
         renderer = TestRenderer(
             surfaceHolder,
-            userStyleRepository,
+            currentUserStyleRepository,
             watchState.asWatchState(),
             INTERACTIVE_UPDATE_RATE_MS
         )
@@ -332,7 +334,7 @@
             watchFaceType,
             complicationsManager,
             renderer,
-            userStyleRepository,
+            currentUserStyleRepository,
             watchState,
             handler,
             tapListener,
@@ -358,11 +360,11 @@
         userStyleSchema: UserStyleSchema,
         wallpaperInteractiveWatchFaceInstanceParams: WallpaperInteractiveWatchFaceInstanceParams
     ) {
-        userStyleRepository = UserStyleRepository(userStyleSchema)
-        complicationsManager = ComplicationsManager(complications, userStyleRepository)
+        currentUserStyleRepository = CurrentUserStyleRepository(userStyleSchema)
+        complicationsManager = ComplicationsManager(complications, currentUserStyleRepository)
         renderer = TestRenderer(
             surfaceHolder,
-            userStyleRepository,
+            currentUserStyleRepository,
             watchState.asWatchState(),
             INTERACTIVE_UPDATE_RATE_MS
         )
@@ -370,7 +372,7 @@
             watchFaceType,
             complicationsManager,
             renderer,
-            userStyleRepository,
+            currentUserStyleRepository,
             watchState,
             handler,
             null,
@@ -382,14 +384,14 @@
             .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
                 InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
                     wallpaperInteractiveWatchFaceInstanceParams,
-                    object : IPendingInteractiveWatchFaceWCS.Stub() {
+                    object : IPendingInteractiveWatchFace.Stub() {
                         override fun getApiVersion() =
-                            IPendingInteractiveWatchFaceWCS.API_VERSION
+                            IPendingInteractiveWatchFace.API_VERSION
 
-                        override fun onInteractiveWatchFaceWcsCreated(
-                            iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
+                        override fun onInteractiveWatchFaceCreated(
+                            iInteractiveWatchFace: IInteractiveWatchFace
                         ) {
-                            interactiveWatchFaceInstanceWCS = iInteractiveWatchFaceWcs
+                            interactiveWatchFaceInstance = iInteractiveWatchFace
                         }
                     }
                 )
@@ -499,8 +501,8 @@
 
     @After
     public fun validate() {
-        if (this::interactiveWatchFaceInstanceWCS.isInitialized) {
-            interactiveWatchFaceInstanceWCS.release()
+        if (this::interactiveWatchFaceInstance.isInitialized) {
+            interactiveWatchFaceInstance.release()
         }
 
         if (this::engineWrapper.isInitialized && !engineWrapper.destroyed) {
@@ -639,8 +641,8 @@
 
     private fun tapAt(x: Int, y: Int) {
         // The eventTime is ignored.
-        watchFaceImpl.onTapCommand(TapType.TOUCH, x, y)
-        watchFaceImpl.onTapCommand(TapType.TAP, x, y)
+        watchFaceImpl.onTapCommand(TapType.DOWN, x, y)
+        watchFaceImpl.onTapCommand(TapType.UP, x, y)
     }
 
     private fun doubleTapAt(x: Int, y: Int, delayMillis: Long) {
@@ -658,8 +660,8 @@
     }
 
     private fun tapCancelAt(x: Int, y: Int) {
-        watchFaceImpl.onTapCommand(TapType.TOUCH, x, y)
-        watchFaceImpl.onTapCommand(TapType.TOUCH_CANCEL, x, y)
+        watchFaceImpl.onTapCommand(TapType.DOWN, x, y)
+        watchFaceImpl.onTapCommand(TapType.CANCEL, x, y)
     }
 
     @Test
@@ -792,13 +794,13 @@
 
         testWatchFaceService.reset()
         // Tap down left Complication
-        watchFaceImpl.onTapCommand(TapType.TOUCH, 30, 50)
+        watchFaceImpl.onTapCommand(TapType.DOWN, 30, 50)
 
         // Tap down at right complication
-        watchFaceImpl.onTapCommand(TapType.TOUCH, 70, 50)
+        watchFaceImpl.onTapCommand(TapType.DOWN, 70, 50)
 
         // Now Tap cancel at the second position
-        watchFaceImpl.onTapCommand(TapType.TOUCH_CANCEL, 70, 50)
+        watchFaceImpl.onTapCommand(TapType.CANCEL, 70, 50)
         runPostedTasksFor(ViewConfiguration.getDoubleTapTimeout().toLong())
         assertThat(testWatchFaceService.complicationSingleTapped).isEqualTo(RIGHT_COMPLICATION_ID)
         assertThat(testWatchFaceService.singleTapCount).isEqualTo(1)
@@ -814,9 +816,9 @@
 
         testWatchFaceService.reset()
         // Tap down at a position in left Complication
-        watchFaceImpl.onTapCommand(TapType.TOUCH, 30, 50)
+        watchFaceImpl.onTapCommand(TapType.DOWN, 30, 50)
         // Tap cancel at different position stillin left Complication
-        watchFaceImpl.onTapCommand(TapType.TOUCH_CANCEL, 32, 50)
+        watchFaceImpl.onTapCommand(TapType.CANCEL, 32, 50)
 
         runPostedTasksFor(ViewConfiguration.getDoubleTapTimeout().toLong())
         assertThat(testWatchFaceService.complicationSingleTapped).isNull()
@@ -860,8 +862,8 @@
         // Tap on nothing.
         tapAt(1, 1)
 
-        verify(tapListener).onTap(TapType.TOUCH, 1, 1)
-        verify(tapListener).onTap(TapType.TAP, 1, 1)
+        verify(tapListener).onTap(TapType.DOWN, 1, 1)
+        verify(tapListener).onTap(TapType.UP, 1, 1)
     }
 
     @Test
@@ -876,8 +878,8 @@
         // Tap right complication.
         tapAt(70, 50)
 
-        verify(tapListener, times(0)).onTap(TapType.TOUCH, 70, 50)
-        verify(tapListener, times(0)).onTap(TapType.TAP, 70, 50)
+        verify(tapListener, times(0)).onTap(TapType.DOWN, 70, 50)
+        verify(tapListener, times(0)).onTap(TapType.UP, 70, 50)
     }
 
     @Test
@@ -1062,7 +1064,7 @@
         )
 
         // This should get persisted.
-        userStyleRepository.userStyle = UserStyle(
+        currentUserStyleRepository.userStyle = UserStyle(
             hashMapOf(
                 colorStyleSetting to blueStyleOption,
                 watchHandStyleSetting to gothicStyleOption
@@ -1070,7 +1072,7 @@
         )
         engineWrapper.onDestroy()
 
-        val userStyleRepository2 = UserStyleRepository(
+        val userStyleRepository2 = CurrentUserStyleRepository(
             UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting))
         )
 
@@ -1125,7 +1127,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(
                     hashMapOf(
                         colorStyleSetting to blueStyleOption,
@@ -1137,11 +1139,11 @@
         )
 
         // The style option above should get applied during watch face creation.
-        assertThat(userStyleRepository.userStyle.selectedOptions[colorStyleSetting]!!.id)
+        assertThat(currentUserStyleRepository.userStyle.selectedOptions[colorStyleSetting]!!.id)
             .isEqualTo(
                 blueStyleOption.id
             )
-        assertThat(userStyleRepository.userStyle.selectedOptions[watchHandStyleSetting]!!.id)
+        assertThat(currentUserStyleRepository.userStyle.selectedOptions[watchHandStyleSetting]!!.id)
             .isEqualTo(
                 gothicStyleOption.id
             )
@@ -1161,13 +1163,13 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(hashMapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
                 null
             )
         )
 
-        assertThat(userStyleRepository.userStyle.selectedOptions[watchHandStyleSetting])
+        assertThat(currentUserStyleRepository.userStyle.selectedOptions[watchHandStyleSetting])
             .isEqualTo(watchHandStyleList.first())
     }
 
@@ -1215,7 +1217,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(hashMapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
                 null
             )
@@ -1367,18 +1369,19 @@
     @Test
     public fun getOptionForIdentifier_ListViewStyleSetting() {
         // Check the correct Options are returned for known option names.
-        assertThat(colorStyleSetting.getOptionForId(redStyleOption.id)).isEqualTo(
+        assertThat(colorStyleSetting.getOptionForId(redStyleOption.id.value)).isEqualTo(
             redStyleOption
         )
-        assertThat(colorStyleSetting.getOptionForId(greenStyleOption.id)).isEqualTo(
+        assertThat(colorStyleSetting.getOptionForId(greenStyleOption.id.value)).isEqualTo(
             greenStyleOption
         )
-        assertThat(colorStyleSetting.getOptionForId(blueStyleOption.id)).isEqualTo(
+        assertThat(colorStyleSetting.getOptionForId(blueStyleOption.id.value)).isEqualTo(
             blueStyleOption
         )
 
         // For unknown option names the first element in the list should be returned.
-        assertThat(colorStyleSetting.getOptionForId("unknown")).isEqualTo(colorStyleList.first())
+        assertThat(colorStyleSetting.getOptionForId("unknown".encodeToByteArray()))
+            .isEqualTo(colorStyleList.first())
     }
 
     @Test
@@ -1406,7 +1409,7 @@
     @Test
     public fun requestStyleBeforeSetBinder() {
         var userStyleRepository =
-            UserStyleRepository(UserStyleSchema(emptyList()))
+            CurrentUserStyleRepository(UserStyleSchema(emptyList()))
         var testRenderer = TestRenderer(
             surfaceHolder,
             userStyleRepository,
@@ -1420,7 +1423,7 @@
                 userStyleRepository
             ),
             testRenderer,
-            UserStyleRepository(UserStyleSchema(emptyList())),
+            CurrentUserStyleRepository(UserStyleSchema(emptyList())),
             watchState,
             handler,
             null,
@@ -1518,7 +1521,7 @@
                 1000,
                 2000,
             ),
-            SystemState(false, 0),
+            WatchUiState(false, 0),
             UserStyle(
                 hashMapOf(
                     colorStyleSetting to blueStyleOption,
@@ -1548,7 +1551,7 @@
                 1000,
                 2000,
             ),
-            SystemState(false, 0),
+            WatchUiState(false, 0),
             UserStyle(
                 hashMapOf(
                     colorStyleSetting to blueStyleOption,
@@ -1626,7 +1629,7 @@
 
     @Test
     public fun shouldAnimateOverrideControlsEnteringAmbientMode() {
-        var userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
+        var userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
         var testRenderer = object : TestRenderer(
             surfaceHolder,
             userStyleRepository,
@@ -1640,7 +1643,7 @@
             WatchFaceType.ANALOG,
             ComplicationsManager(emptyList(), userStyleRepository),
             testRenderer,
-            UserStyleRepository(UserStyleSchema(emptyList())),
+            CurrentUserStyleRepository(UserStyleSchema(emptyList())),
             watchState,
             handler,
             null,
@@ -1679,9 +1682,9 @@
         )
 
         // Select a new style which turns off both complications.
-        val newStyleA = HashMap(userStyleRepository.userStyle.selectedOptions)
+        val newStyleA = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
         newStyleA[complicationsStyleSetting] = noComplicationsOption
-        userStyleRepository.userStyle = UserStyle(newStyleA)
+        currentUserStyleRepository.userStyle = UserStyle(newStyleA)
 
         runPostedTasksFor(0)
 
@@ -1697,9 +1700,9 @@
         reset(iWatchFaceService)
 
         // Select a new style which turns on only the left complication.
-        val newStyleB = HashMap(userStyleRepository.userStyle.selectedOptions)
+        val newStyleB = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
         newStyleB[complicationsStyleSetting] = leftComplicationsOption
-        userStyleRepository.userStyle = UserStyle(newStyleB)
+        currentUserStyleRepository.userStyle = UserStyle(newStyleB)
 
         runPostedTasksFor(0)
 
@@ -1717,26 +1720,26 @@
     @Test
     public fun partialComplicationOverrides() {
         val bothComplicationsOption = ComplicationsOption(
-            LEFT_AND_RIGHT_COMPLICATIONS,
+            Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
             "Left And Right",
             null,
             // An empty list means use the initial config.
             emptyList()
         )
         val leftOnlyComplicationsOption = ComplicationsOption(
-            LEFT_COMPLICATION,
+            Option.Id(LEFT_COMPLICATION),
             "Left",
             null,
             listOf(ComplicationOverlay.Builder(RIGHT_COMPLICATION_ID).setEnabled(false).build())
         )
         val rightOnlyComplicationsOption = ComplicationsOption(
-            RIGHT_COMPLICATION,
+            Option.Id(RIGHT_COMPLICATION),
             "Right",
             null,
             listOf(ComplicationOverlay.Builder(LEFT_COMPLICATION_ID).setEnabled(false).build())
         )
         val complicationsStyleSetting = ComplicationsUserStyleSetting(
-            "complications_style_setting",
+            UserStyleSetting.Id("complications_style_setting"),
             "Complications",
             "Number and position",
             icon = null,
@@ -1759,9 +1762,9 @@
         assertTrue(rightComplication.enabled)
 
         // Select left complication only.
-        val newStyleA = HashMap(userStyleRepository.userStyle.selectedOptions)
+        val newStyleA = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
         newStyleA[complicationsStyleSetting] = leftOnlyComplicationsOption
-        userStyleRepository.userStyle = UserStyle(newStyleA)
+        currentUserStyleRepository.userStyle = UserStyle(newStyleA)
 
         runPostedTasksFor(0)
 
@@ -1769,9 +1772,9 @@
         assertFalse(rightComplication.enabled)
 
         // Select right complication only.
-        val newStyleB = HashMap(userStyleRepository.userStyle.selectedOptions)
+        val newStyleB = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
         newStyleB[complicationsStyleSetting] = rightOnlyComplicationsOption
-        userStyleRepository.userStyle = UserStyle(newStyleB)
+        currentUserStyleRepository.userStyle = UserStyle(newStyleB)
 
         runPostedTasksFor(0)
 
@@ -1779,9 +1782,9 @@
         assertTrue(rightComplication.enabled)
 
         // Select both complications.
-        val newStyleC = HashMap(userStyleRepository.userStyle.selectedOptions)
+        val newStyleC = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
         newStyleC[complicationsStyleSetting] = bothComplicationsOption
-        userStyleRepository.userStyle = UserStyle(newStyleC)
+        currentUserStyleRepository.userStyle = UserStyle(newStyleC)
 
         runPostedTasksFor(0)
 
@@ -1792,20 +1795,20 @@
     @Test
     public fun partialComplicationOverrideAppliedToInitialStyle() {
         val bothComplicationsOption = ComplicationsOption(
-            LEFT_AND_RIGHT_COMPLICATIONS,
+            Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
             "Left And Right",
             null,
             // An empty list means use the initial config.
             emptyList()
         )
         val leftOnlyComplicationsOption = ComplicationsOption(
-            LEFT_COMPLICATION,
+            Option.Id(LEFT_COMPLICATION),
             "Left",
             null,
             listOf(ComplicationOverlay.Builder(RIGHT_COMPLICATION_ID).setEnabled(false).build())
         )
         val complicationsStyleSetting = ComplicationsUserStyleSetting(
-            "complications_style_setting",
+            UserStyleSetting.Id("complications_style_setting"),
             "Complications",
             "Number and position",
             icon = null,
@@ -1841,7 +1844,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null
             )
@@ -1858,7 +1861,7 @@
             rightComplicationData = it.asWireComplicationData()
         }
 
-        interactiveWatchFaceInstanceWCS.updateComplicationData(
+        interactiveWatchFaceInstance.updateComplicationData(
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
@@ -1895,13 +1898,13 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null
             )
         )
 
-        interactiveWatchFaceInstanceWCS.updateComplicationData(
+        interactiveWatchFaceInstance.updateComplicationData(
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
@@ -1916,7 +1919,7 @@
         assertThat(leftComplication.isActiveAt(0)).isTrue()
 
         // Send empty data.
-        interactiveWatchFaceInstanceWCS.updateComplicationData(
+        interactiveWatchFaceInstance.updateComplicationData(
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
@@ -1928,7 +1931,7 @@
         assertThat(leftComplication.isActiveAt(0)).isFalse()
 
         // Send a complication that is active for a time range.
-        interactiveWatchFaceInstanceWCS.updateComplicationData(
+        interactiveWatchFaceInstance.updateComplicationData(
             listOf(
                 IdAndComplicationDataWireFormat(
                     LEFT_COMPLICATION_ID,
@@ -1961,14 +1964,14 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null
             )
         )
 
         // Send a complication with an invalid id - this should get ignored.
-        interactiveWatchFaceInstanceWCS.updateComplicationData(
+        interactiveWatchFaceInstance.updateComplicationData(
             listOf(
                 IdAndComplicationDataWireFormat(
                     RIGHT_COMPLICATION_ID,
@@ -1984,7 +1987,7 @@
     public fun invalidateRendererBeforeFullInit() {
         renderer = TestRenderer(
             surfaceHolder,
-            UserStyleRepository(UserStyleSchema(emptyList())),
+            CurrentUserStyleRepository(UserStyleSchema(emptyList())),
             watchState.asWatchState(),
             INTERACTIVE_UPDATE_RATE_MS
         )
@@ -2007,7 +2010,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null
             )
@@ -2033,7 +2036,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null
             )
@@ -2075,11 +2078,11 @@
 
     @Test
     public fun isAmbientInitalisedEvenWithoutPropertiesSent() {
-        userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
-        complicationsManager = ComplicationsManager(emptyList(), userStyleRepository)
+        currentUserStyleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
+        complicationsManager = ComplicationsManager(emptyList(), currentUserStyleRepository)
         renderer = TestRenderer(
             surfaceHolder,
-            userStyleRepository,
+            currentUserStyleRepository,
             watchState.asWatchState(),
             INTERACTIVE_UPDATE_RATE_MS
         )
@@ -2087,7 +2090,7 @@
             WatchFaceType.ANALOG,
             complicationsManager,
             renderer,
-            userStyleRepository,
+            currentUserStyleRepository,
             watchState,
             handler,
             tapListener,
@@ -2120,7 +2123,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(hashMapOf(colorStyleSetting to blueStyleOption)).toWireFormat(),
                 null
             )
@@ -2135,9 +2138,9 @@
         val params = RenderParameters(
             DrawMode.INTERACTIVE,
             mapOf(
-                Layer.BASE_LAYER to LayerMode.DRAW,
+                Layer.BASE to LayerMode.DRAW,
                 Layer.COMPLICATIONS to LayerMode.DRAW,
-                Layer.TOP_LAYER to LayerMode.DRAW
+                Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
             ),
             null
         )
@@ -2153,9 +2156,9 @@
             RenderParameters(
                 DrawMode.INTERACTIVE,
                 mapOf(
-                    Layer.BASE_LAYER to LayerMode.DRAW,
+                    Layer.BASE to LayerMode.DRAW,
                     Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
-                    Layer.TOP_LAYER to LayerMode.DRAW
+                    Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
                 ),
                 null
             )
@@ -2207,7 +2210,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 listOf(
                     IdAndComplicationDataWireFormat(
@@ -2241,7 +2244,7 @@
 
     @Test
     public fun directBoot() {
-        val userStyleRepository = UserStyleRepository(
+        val userStyleRepository = CurrentUserStyleRepository(
             UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting))
         )
         val testRenderer = TestRenderer(
@@ -2268,7 +2271,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(
                     hashMapOf(
                         colorStyleSetting to blueStyleOption,
@@ -2294,7 +2297,7 @@
 
     @Test
     public fun headlessFlagPreventsDirectBoot() {
-        val userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
+        val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
         val testRenderer = TestRenderer(
             surfaceHolder,
             userStyleRepository,
@@ -2319,7 +2322,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(
                     hashMapOf(
                         colorStyleSetting to blueStyleOption,
@@ -2352,7 +2355,7 @@
                     0,
                     0
                 ),
-                SystemState(false, 0),
+                WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 listOf(
                     IdAndComplicationDataWireFormat(
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProxyOverrideActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProxyOverrideActivity.java
index 5d58caf..29f1844 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProxyOverrideActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProxyOverrideActivity.java
@@ -113,7 +113,9 @@
 
     @Override
     protected void onDestroy() {
-        mProxy.shutdown();
+        if (mProxy != null) {
+            mProxy.shutdown();
+        }
         super.onDestroy();
     }
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptReferenceImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
similarity index 61%
rename from webkit/webkit/src/main/java/androidx/webkit/internal/ScriptReferenceImpl.java
rename to webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
index 328af59..e882b7b 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptReferenceImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
@@ -19,30 +19,30 @@
 import androidx.annotation.NonNull;
 import androidx.webkit.ScriptHandler;
 
-import org.chromium.support_lib_boundary.ScriptReferenceBoundaryInterface;
+import org.chromium.support_lib_boundary.ScriptHandlerBoundaryInterface;
 import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
 
 import java.lang.reflect.InvocationHandler;
 
 /**
- * Internal implementation of {@link androidx.webkit.ScriptReference}.
+ * Internal implementation of {@link androidx.webkit.ScriptHandler}.
  */
-public class ScriptReferenceImpl extends ScriptHandler {
-    private ScriptReferenceBoundaryInterface mBoundaryInterface;
+public class ScriptHandlerImpl extends ScriptHandler {
+    private ScriptHandlerBoundaryInterface mBoundaryInterface;
 
-    private ScriptReferenceImpl(@NonNull ScriptReferenceBoundaryInterface boundaryInterface) {
+    private ScriptHandlerImpl(@NonNull ScriptHandlerBoundaryInterface boundaryInterface) {
         mBoundaryInterface = boundaryInterface;
     }
 
     /**
-     * Create an AndroidX ScriptReference from the given InvocationHandler.
+     * Create an AndroidX ScriptHandler from the given InvocationHandler.
      */
-    public static @NonNull ScriptReferenceImpl toScriptHandler(
-            @NonNull /* ScriptReference */ InvocationHandler invocationHandler) {
-        final ScriptReferenceBoundaryInterface boundaryInterface =
+    public static @NonNull ScriptHandlerImpl toScriptHandler(
+            @NonNull /* ScriptHandler */ InvocationHandler invocationHandler) {
+        final ScriptHandlerBoundaryInterface boundaryInterface =
                 BoundaryInterfaceReflectionUtil.castToSuppLibClass(
-                        ScriptReferenceBoundaryInterface.class, invocationHandler);
-        return new ScriptReferenceImpl(boundaryInterface);
+                        ScriptHandlerBoundaryInterface.class, invocationHandler);
+        return new ScriptHandlerImpl(boundaryInterface);
     }
 
     /**
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
index 69a2c41..919c549 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
@@ -102,9 +102,9 @@
      * Adapter method for {@link WebViewCompat#addWebMessageListener(android.webkit.WebView,
      * String, Set)}
      */
-    public @NonNull ScriptReferenceImpl addDocumentStartJavaScript(
+    public @NonNull ScriptHandlerImpl addDocumentStartJavaScript(
             @NonNull String script, @NonNull String[] allowedOriginRules) {
-        return ScriptReferenceImpl.toScriptHandler(
+        return ScriptHandlerImpl.toScriptHandler(
                 mImpl.addDocumentStartJavaScript(script, allowedOriginRules));
     }
 
diff --git a/window/window-extensions/api/current.txt b/window/window-extensions/api/current.txt
index 78bdc7f..486715e 100644
--- a/window/window-extensions/api/current.txt
+++ b/window/window-extensions/api/current.txt
@@ -21,7 +21,6 @@
     method public int getState();
     method public int getType();
     field public static final int STATE_FLAT = 1; // 0x1
-    field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
diff --git a/window/window-extensions/api/public_plus_experimental_current.txt b/window/window-extensions/api/public_plus_experimental_current.txt
index 78bdc7f..486715e 100644
--- a/window/window-extensions/api/public_plus_experimental_current.txt
+++ b/window/window-extensions/api/public_plus_experimental_current.txt
@@ -21,7 +21,6 @@
     method public int getState();
     method public int getType();
     field public static final int STATE_FLAT = 1; // 0x1
-    field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
diff --git a/window/window-extensions/api/restricted_current.txt b/window/window-extensions/api/restricted_current.txt
index 78bdc7f..486715e 100644
--- a/window/window-extensions/api/restricted_current.txt
+++ b/window/window-extensions/api/restricted_current.txt
@@ -21,7 +21,6 @@
     method public int getState();
     method public int getType();
     field public static final int STATE_FLAT = 1; // 0x1
-    field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
diff --git a/window/window-extensions/build.gradle b/window/window-extensions/build.gradle
index 5b1a650..384720b 100644
--- a/window/window-extensions/build.gradle
+++ b/window/window-extensions/build.gradle
@@ -31,12 +31,6 @@
     id("com.android.library")
 }
 
-android {
-    defaultConfig {
-        minSdkVersion 14
-    }
-}
-
 dependencies {
     implementation("androidx.annotation:annotation:1.1.0")
 
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
index fdd42fa..5c46471 100644
--- a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
@@ -77,7 +77,7 @@
         Rect rect = new Rect(1, 0, 1, 10);
         int type = ExtensionFoldingFeature.TYPE_FOLD;
         int originalState = ExtensionFoldingFeature.STATE_FLAT;
-        int otherState = ExtensionFoldingFeature.STATE_FLIPPED;
+        int otherState = ExtensionFoldingFeature.STATE_HALF_OPENED;
 
         ExtensionFoldingFeature original = new ExtensionFoldingFeature(rect, type,
                 originalState);
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
index bc7e5b5..05232380 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
@@ -65,19 +65,10 @@
      */
     public static final int STATE_HALF_OPENED = 2;
 
-    /**
-     * The foldable device's hinge is flipped with the flexible screen parts or physical screens
-     * facing opposite directions. See the
-     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
-     * section in the official documentation for visual samples and references.
-     */
-    public static final int STATE_FLIPPED = 3;
-
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
             STATE_HALF_OPENED,
-            STATE_FLAT,
-            STATE_FLIPPED
+            STATE_FLAT
     })
     @interface State {}
 
@@ -156,8 +147,6 @@
         switch (state) {
             case STATE_FLAT:
                 return "FLAT";
-            case STATE_FLIPPED:
-                return "FLIPPED";
             case STATE_HALF_OPENED:
                 return "HALF_OPENED";
             default:
diff --git a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
index b24539b..57ce8be 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
@@ -162,7 +162,6 @@
     private fun FoldingFeature.stateString(): String {
         return when (state) {
             FoldingFeature.STATE_FLAT -> "FLAT"
-            FoldingFeature.STATE_FLIPPED -> "FLIPPED"
             FoldingFeature.STATE_HALF_OPENED -> "HALF_OPENED"
             else -> "Unknown feature state ($state)"
         }
diff --git a/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt b/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt
index b0d6db3..4a77e93 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt
@@ -18,18 +18,13 @@
 
 import android.app.Activity
 import android.graphics.Point
-import android.util.DisplayMetrics
+import androidx.window.WindowManager
 
 /**
  * Return a [Point] whose dimensions match the metrics of the window.
  * @return [Point] whose dimensions match the metrics of the window.
  */
-@Suppress("DEPRECATION")
 internal fun Activity.calculateWindowSizeExt(): Point {
-    val displayMetrics = DisplayMetrics()
-    // TODO(b/159454816) Replace with window metrics.
-    this.windowManager.defaultDisplay
-        .getMetrics(displayMetrics)
-
-    return Point(displayMetrics.widthPixels, displayMetrics.heightPixels)
+    val bounds = WindowManager(this).currentWindowMetrics.bounds
+    return Point(bounds.width(), bounds.height())
 }
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
index af7dbca..cf7b124 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
@@ -19,11 +19,9 @@
 package androidx.window.sample.backend
 
 import android.app.Activity
-import android.content.Context
 import android.graphics.Point
 import android.graphics.Rect
 import androidx.core.util.Consumer
-import androidx.window.DeviceState
 import androidx.window.DisplayFeature
 import androidx.window.FoldingFeature
 import androidx.window.WindowBackend
@@ -76,14 +74,6 @@
         return WindowLayoutInfo.Builder().setDisplayFeatures(featureList).build()
     }
 
-    @Deprecated("Added for compatibility with WindowBackend in sample")
-    override fun registerLayoutChangeCallback(
-        context: Context,
-        executor: Executor,
-        callback: Consumer<WindowLayoutInfo>
-    ) {
-    }
-
     private fun foldRect(windowSize: Point): Rect {
         return when (foldAxis) {
             FoldAxis.LONG_DIMENSION -> longDimensionFold(windowSize)
@@ -107,15 +97,6 @@
         }
     }
 
-    override fun registerDeviceStateChangeCallback(
-        executor: Executor,
-        callback: Consumer<DeviceState>
-    ) {
-    }
-
-    override fun unregisterDeviceStateChangeCallback(callback: Consumer<DeviceState>) {
-    }
-
     override fun registerLayoutChangeCallback(
         activity: Activity,
         executor: Executor,
diff --git a/window/window-sidecar/build.gradle b/window/window-sidecar/build.gradle
index d8b8fd3..36be655 100644
--- a/window/window-sidecar/build.gradle
+++ b/window/window-sidecar/build.gradle
@@ -24,12 +24,6 @@
     id("com.android.library")
 }
 
-android {
-    defaultConfig {
-        minSdkVersion 14
-    }
-}
-
 dependencies {
     implementation("androidx.annotation:annotation:1.1.0")
 }
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index e8f74ed..bc9a317 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -43,17 +43,13 @@
     field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
     field public static final int ORIENTATION_VERTICAL = 0; // 0x0
     field public static final int STATE_FLAT = 1; // 0x1
-    field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
 
   public interface WindowBackend {
-    method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index e8f74ed..bc9a317 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -43,17 +43,13 @@
     field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
     field public static final int ORIENTATION_VERTICAL = 0; // 0x0
     field public static final int STATE_FLAT = 1; // 0x1
-    field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
 
   public interface WindowBackend {
-    method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index e8f74ed..bc9a317 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -43,17 +43,13 @@
     field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
     field public static final int ORIENTATION_VERTICAL = 0; // 0x0
     field public static final int STATE_FLAT = 1; // 0x1
-    field public static final int STATE_FLIPPED = 3; // 0x3
     field public static final int STATE_HALF_OPENED = 2; // 0x2
     field public static final int TYPE_FOLD = 1; // 0x1
     field public static final int TYPE_HINGE = 2; // 0x2
   }
 
   public interface WindowBackend {
-    method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
-    method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
     method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
   }
 
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 706f6f6..4a24e84 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -35,9 +35,6 @@
 }
 
 android {
-    defaultConfig {
-        minSdkVersion 14
-    }
     buildTypes.all {
         consumerProguardFiles "proguard-rules.pro"
     }
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
index e78d762..0d41ab5 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
@@ -141,7 +141,7 @@
         Rect bounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, WINDOW_BOUNDS.width(), 1);
         ExtensionDisplayFeature extensionDisplayFeature =
                 new ExtensionFoldingFeature(bounds, ExtensionFoldingFeature.TYPE_HINGE,
-                        ExtensionFoldingFeature.STATE_FLIPPED);
+                        ExtensionFoldingFeature.STATE_HALF_OPENED);
         List<ExtensionDisplayFeature> displayFeatures = new ArrayList<>();
         displayFeatures.add(extensionDisplayFeature);
         ExtensionWindowLayoutInfo extensionWindowLayoutInfo =
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
index 7a55cb9..2648fb6 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
@@ -16,19 +16,12 @@
 
 package androidx.window;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -42,8 +35,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 
-import com.google.common.collect.BoundType;
-import com.google.common.collect.Range;
 import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.Before;
@@ -104,33 +95,6 @@
     }
 
     @Test
-    public void testRegisterDeviceStateChangeCallback_noExtension() {
-        // Verify method with extension
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        assumeTrue(backend.mWindowExtension == null);
-        SimpleConsumer<DeviceState> simpleConsumer = new SimpleConsumer<>();
-
-        backend.registerDeviceStateChangeCallback(directExecutor(), simpleConsumer);
-
-        DeviceState deviceState = simpleConsumer.lastValue();
-        assertNotNull(deviceState);
-        assertThat(deviceState.getPosture()).isIn(Range.range(
-                DeviceState.POSTURE_UNKNOWN, BoundType.CLOSED,
-                DeviceState.POSTURE_MAX_KNOWN, BoundType.CLOSED));
-        DeviceState initialLastReportedState = backend.mLastReportedDeviceState;
-
-        // Verify method without extension
-        backend.mWindowExtension = null;
-        SimpleConsumer<DeviceState> noExtensionConsumer = new SimpleConsumer<>();
-        backend.registerDeviceStateChangeCallback(directExecutor(), noExtensionConsumer);
-        deviceState = noExtensionConsumer.lastValue();
-        assertNotNull(deviceState);
-        assertEquals(DeviceState.POSTURE_UNKNOWN, deviceState.getPosture());
-        // Verify that last reported state does not change when using the getter
-        assertEquals(initialLastReportedState, backend.mLastReportedDeviceState);
-    }
-
-    @Test
     public void testRegisterLayoutChangeCallback() {
         ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
@@ -232,117 +196,6 @@
         assertEquals(expected, consumer.mValues);
     }
 
-    @Test
-    public void testRegisterDeviceChangeCallback() {
-        ExtensionInterfaceCompat mockInterface = mock(
-                ExtensionInterfaceCompat.class);
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = mockInterface;
-
-        // Check registering the device state change callback
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-
-        assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
-        verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(false));
-
-        // Check unregistering the device state change callback
-        backend.unregisterDeviceStateChangeCallback(consumer);
-
-        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
-        verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(true));
-    }
-
-    @Test
-    public void testDeviceChangeCallback() {
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
-        // Check that callbacks from the extension are propagated correctly
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-        DeviceState deviceState = newTestDeviceState();
-        ExtensionWindowBackend.ExtensionListenerImpl backendListener =
-                backend.new ExtensionListenerImpl();
-        backendListener.onDeviceStateChanged(deviceState);
-
-        verify(consumer).accept(eq(deviceState));
-        assertEquals(deviceState, backend.mLastReportedDeviceState);
-
-        // Test that the same value wouldn't be reported again
-        reset(consumer);
-        backendListener.onDeviceStateChanged(deviceState);
-        verify(consumer, never()).accept(any());
-    }
-
-    @Test
-    public void testDeviceChangeChangeCallback_callsExtensionOnce() {
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
-        // Check registering the layout change callback
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-        mActivityTestRule.launchActivity(new Intent());
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-        backend.registerDeviceStateChangeCallback(Runnable::run, mock(DeviceStateConsumer.class));
-
-        assertEquals(2, backend.mDeviceStateChangeCallbacks.size());
-        verify(backend.mWindowExtension).onDeviceStateListenersChanged(false);
-
-        // Check unregistering the layout change callback
-        backend.unregisterDeviceStateChangeCallback(consumer);
-
-        assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
-        verify(backend.mWindowExtension, times(0))
-                .onDeviceStateListenersChanged(true);
-    }
-
-    @Test
-    public void testDeviceChangeChangeCallback_clearListeners() {
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
-        // Check registering the layout change callback
-        Consumer<DeviceState> firstConsumer = mock(DeviceStateConsumer.class);
-        Consumer<DeviceState> secondConsumer = mock(DeviceStateConsumer.class);
-        mActivityTestRule.launchActivity(new Intent());
-        backend.registerDeviceStateChangeCallback(Runnable::run, firstConsumer);
-        backend.registerDeviceStateChangeCallback(Runnable::run, secondConsumer);
-
-        // Check unregistering the layout change callback
-        backend.unregisterDeviceStateChangeCallback(firstConsumer);
-        backend.unregisterDeviceStateChangeCallback(secondConsumer);
-
-        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
-        verify(backend.mWindowExtension).onDeviceStateListenersChanged(true);
-    }
-
-    @Test
-    public void testDeviceChangeCallback_relayLastEmittedValue() {
-        DeviceState expectedState = newTestDeviceState();
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-        backend.mLastReportedDeviceState = expectedState;
-
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-
-        verify(consumer).accept(expectedState);
-    }
-
-    @Test
-    public void testDeviceChangeCallback_clearLastEmittedValue() {
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-        backend.unregisterDeviceStateChangeCallback(consumer);
-
-        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
-        assertNull(backend.mLastReportedDeviceState);
-    }
-
     private static WindowLayoutInfo newTestWindowLayoutInfo() {
         WindowLayoutInfo.Builder builder = new WindowLayoutInfo.Builder();
         WindowLayoutInfo windowLayoutInfo = builder.build();
diff --git a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
index fcf70d6..ffe4c7f 100644
--- a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
+++ b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
@@ -23,7 +23,6 @@
 import static androidx.window.FoldingFeature.OcclusionType;
 import static androidx.window.FoldingFeature.Orientation;
 import static androidx.window.FoldingFeature.STATE_FLAT;
-import static androidx.window.FoldingFeature.STATE_FLIPPED;
 import static androidx.window.FoldingFeature.STATE_HALF_OPENED;
 import static androidx.window.FoldingFeature.TYPE_FOLD;
 import static androidx.window.FoldingFeature.TYPE_HINGE;
@@ -61,22 +60,22 @@
 
     @Test(expected = IllegalArgumentException.class)
     public void testHorizontalHingeWithNonZeroOrigin() {
-        new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_HINGE, STATE_FLIPPED);
+        new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_HINGE, STATE_HALF_OPENED);
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testVerticalHingeWithNonZeroOrigin() {
-        new FoldingFeature(new Rect(10, 1, 19, 29), TYPE_HINGE, STATE_FLIPPED);
+        new FoldingFeature(new Rect(10, 1, 19, 29), TYPE_HINGE, STATE_HALF_OPENED);
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testHorizontalFoldWithNonZeroOrigin() {
-        new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_FOLD, STATE_FLIPPED);
+        new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_FOLD, STATE_HALF_OPENED);
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testVerticalFoldWithNonZeroOrigin() {
-        new FoldingFeature(new Rect(10, 1, 10, 20), TYPE_FOLD, STATE_FLIPPED);
+        new FoldingFeature(new Rect(10, 1, 10, 20), TYPE_FOLD, STATE_HALF_OPENED);
     }
 
     @Test(expected = IllegalArgumentException.class)
@@ -145,7 +144,7 @@
         Rect rect = new Rect(1, 0, 1, 10);
         int type = TYPE_FOLD;
         int originalState = STATE_FLAT;
-        int otherState = STATE_FLIPPED;
+        int otherState = STATE_HALF_OPENED;
 
         FoldingFeature original = new FoldingFeature(rect, type, originalState);
         FoldingFeature other = new FoldingFeature(rect, type, otherState);
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
index 99e5f35..1ce4467 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
@@ -431,16 +431,10 @@
                 .get(0);
         assertEquals(FoldingFeature.STATE_HALF_OPENED, capturedFoldingFeature.getState());
 
-        reset(mockCallback);
-        fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_FLIPPED));
-        verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
-        capturedFoldingFeature = (FoldingFeature) windowLayoutCaptor.getValue().getDisplayFeatures()
-                .get(0);
-        assertEquals(FoldingFeature.STATE_FLIPPED, capturedFoldingFeature.getState());
-
-        // No display features must be reported in closed state
+        // No display features must be reported in closed state or flipped state.
         reset(mockCallback);
         fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_CLOSED));
+        fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_FLIPPED));
         verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
         assertTrue(windowLayoutCaptor.getValue().getDisplayFeatures().isEmpty());
     }
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
index 4730074..34bbc2a 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
@@ -20,7 +20,6 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.Activity;
-import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
 
@@ -77,20 +76,6 @@
             mWindowLayoutInfo = windowLayoutInfo;
         }
 
-        /**
-         * Throws an exception if used.
-         * @deprecated will be removed in next alpha
-         * @param context any {@link Activity}
-         * @param executor any {@link Executor}
-         * @param callback any {@link Consumer}
-         */
-        @Override
-        @Deprecated // TODO(b/173739071) Remove in next alpha.
-        public void registerLayoutChangeCallback(@NonNull Context context,
-                @NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
-            throw new RuntimeException("Deprecated method");
-        }
-
         @Override
         public void registerLayoutChangeCallback(@NonNull Activity activity,
                 @NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
@@ -102,15 +87,5 @@
             // Empty
         }
 
-        @Override
-        public void registerDeviceStateChangeCallback(@NonNull Executor executor,
-                @NonNull Consumer<DeviceState> callback) {
-            throw new UnsupportedOperationException("Deprecated method");
-        }
-
-        @Override
-        public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
-            throw new UnsupportedOperationException("Deprecated method");
-        }
     }
 }
diff --git a/window/window/src/main/java/androidx/window/ExtensionAdapter.java b/window/window/src/main/java/androidx/window/ExtensionAdapter.java
index 4285b53..2a66c34 100644
--- a/window/window/src/main/java/androidx/window/ExtensionAdapter.java
+++ b/window/window/src/main/java/androidx/window/ExtensionAdapter.java
@@ -116,9 +116,6 @@
             case ExtensionFoldingFeature.STATE_FLAT:
                 state = FoldingFeature.STATE_FLAT;
                 break;
-            case ExtensionFoldingFeature.STATE_FLIPPED:
-                state = FoldingFeature.STATE_FLIPPED;
-                break;
             case ExtensionFoldingFeature.STATE_HALF_OPENED:
                 state = FoldingFeature.STATE_HALF_OPENED;
                 break;
diff --git a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
index 692e3c7..0476c5d 100644
--- a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
+++ b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
@@ -21,7 +21,6 @@
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Context;
-import android.content.ContextWrapper;
 import android.util.Log;
 
 import androidx.annotation.GuardedBy;
@@ -96,45 +95,6 @@
     }
 
     @Override
-    public void registerLayoutChangeCallback(@NonNull Context context, @NonNull Executor executor,
-            @NonNull Consumer<WindowLayoutInfo> callback) {
-        registerLayoutChangeCallback(assertActivityContext(context), executor, callback);
-    }
-
-    /**
-     * Unwraps the hierarchy of {@link ContextWrapper}-s until {@link Activity} is reached.
-     * @return Base {@link Activity} context or {@code null} if not available.
-     * @deprecated added temporarily to make migration easier. Will be removed in next relesae.
-     */
-    @Nullable
-    @Deprecated // TODO(b/173739071) remove
-    private static Activity getActivityFromContext(Context context) {
-        while (context instanceof ContextWrapper) {
-            if (context instanceof Activity) {
-                return (Activity) context;
-            }
-            context = ((ContextWrapper) context).getBaseContext();
-        }
-        return null;
-    }
-
-    /**
-     * @deprecated added temporarily to make migration easier. Will be removed in next release.
-     * @param context any {@link Context}
-     * @return {@link Activity} if associated with {@link Context} throw
-     * {@link IllegalArgumentException} otherwise.
-     */
-    @Deprecated
-    private Activity assertActivityContext(Context context) {
-        Activity activity = getActivityFromContext(context);
-        if (activity == null) {
-            throw new IllegalArgumentException("Used non-visual Context with WindowManager. "
-                    + "Please use an Activity or a ContextWrapper around an Activity instead.");
-        }
-        return activity;
-    }
-
-    @Override
     public void registerLayoutChangeCallback(@NonNull Activity activity,
             @NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
         synchronized (sLock) {
@@ -211,55 +171,6 @@
         mWindowExtension.onWindowLayoutChangeListenerRemoved(activity);
     }
 
-    @Override
-    public void registerDeviceStateChangeCallback(@NonNull Executor executor,
-            @NonNull Consumer<DeviceState> callback) {
-        synchronized (sLock) {
-            final DeviceStateChangeCallbackWrapper callbackWrapper =
-                    new DeviceStateChangeCallbackWrapper(executor, callback);
-            if (mWindowExtension == null) {
-                if (DEBUG) {
-                    Log.d(TAG, "Extension not loaded, skipping callback registration.");
-                }
-                callback.accept(new DeviceState(DeviceState.POSTURE_UNKNOWN));
-                return;
-            }
-
-            if (mDeviceStateChangeCallbacks.isEmpty()) {
-                mWindowExtension.onDeviceStateListenersChanged(false /* isEmpty */);
-            }
-
-            mDeviceStateChangeCallbacks.add(callbackWrapper);
-            if (mLastReportedDeviceState != null) {
-                callbackWrapper.accept(mLastReportedDeviceState);
-            }
-        }
-    }
-
-    @Override
-    public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
-        synchronized (sLock) {
-            if (mWindowExtension == null) {
-                if (DEBUG) {
-                    Log.d(TAG, "Extension not loaded, skipping callback un-registration.");
-                }
-                return;
-            }
-
-            for (DeviceStateChangeCallbackWrapper callbackWrapper : mDeviceStateChangeCallbacks) {
-                if (callbackWrapper.mCallback.equals(callback)) {
-                    mDeviceStateChangeCallbacks.remove(callbackWrapper);
-                    if (mDeviceStateChangeCallbacks.isEmpty()) {
-                        mWindowExtension.onDeviceStateListenersChanged(true /* isEmpty */);
-                        // Clear device state so we do not replay stale data.
-                        mLastReportedDeviceState = null;
-                    }
-                    return;
-                }
-            }
-        }
-    }
-
     @VisibleForTesting
     class ExtensionListenerImpl implements ExtensionInterfaceCompat.ExtensionCallbackInterface {
         @Override
diff --git a/window/window/src/main/java/androidx/window/FoldingFeature.java b/window/window/src/main/java/androidx/window/FoldingFeature.java
index 388cff7..cee6744 100644
--- a/window/window/src/main/java/androidx/window/FoldingFeature.java
+++ b/window/window/src/main/java/androidx/window/FoldingFeature.java
@@ -65,14 +65,6 @@
     public static final int STATE_HALF_OPENED = 2;
 
     /**
-     * The foldable device is flipped with the flexible screen parts or physical screens facing
-     * opposite directions. See the
-     * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
-     * section in the official documentation for visual samples and references.
-     */
-    public static final int STATE_FLIPPED = 3;
-
-    /**
      * The {@link FoldingFeature} does not occlude the content in any way. One example is a flat
      * continuous fold where content can stretch across the fold. Another example is a hinge that
      * has width or height equal to 0. In this case the content is physically split across both
@@ -116,7 +108,6 @@
     @IntDef({
             STATE_HALF_OPENED,
             STATE_FLAT,
-            STATE_FLIPPED,
     })
     @interface State {}
 
@@ -193,7 +184,7 @@
         if (mType == TYPE_HINGE) {
             return true;
         }
-        if (mType == TYPE_FOLD && (mState == STATE_FLIPPED || mState == STATE_HALF_OPENED)) {
+        if (mType == TYPE_FOLD && mState == STATE_HALF_OPENED) {
             return true;
         }
         return false;
@@ -260,14 +251,13 @@
     }
 
     /**
-     * Verifies the state is {@link FoldingFeature#STATE_FLAT},
-     * {@link FoldingFeature#STATE_HALF_OPENED} or {@link FoldingFeature#STATE_FLIPPED}.
+     * Verifies the state is {@link FoldingFeature#STATE_FLAT} or
+     * {@link FoldingFeature#STATE_HALF_OPENED}.
      */
     private static void validateState(int state) {
-        if (state != STATE_FLAT && state != STATE_HALF_OPENED && state != STATE_FLIPPED) {
+        if (state != STATE_FLAT && state != STATE_HALF_OPENED) {
             throw new IllegalArgumentException("State must be either " + stateToString(STATE_FLAT)
-                    + ", " + stateToString(STATE_HALF_OPENED) + ", or "
-                    + stateToString(STATE_FLIPPED));
+                    + " or " + stateToString(STATE_HALF_OPENED));
         }
     }
 
@@ -312,8 +302,6 @@
         switch (state) {
             case STATE_FLAT:
                 return "FLAT";
-            case STATE_FLIPPED:
-                return "FLIPPED";
             case STATE_HALF_OPENED:
                 return "HALF_OPENED";
             default:
diff --git a/window/window/src/main/java/androidx/window/SidecarAdapter.java b/window/window/src/main/java/androidx/window/SidecarAdapter.java
index d93fcbd..31dd6c8 100644
--- a/window/window/src/main/java/androidx/window/SidecarAdapter.java
+++ b/window/window/src/main/java/androidx/window/SidecarAdapter.java
@@ -274,10 +274,8 @@
         switch (devicePosture) {
             case SidecarDeviceState.POSTURE_CLOSED:
             case SidecarDeviceState.POSTURE_UNKNOWN:
-                return null;
             case SidecarDeviceState.POSTURE_FLIPPED:
-                state = FoldingFeature.STATE_FLIPPED;
-                break;
+                return null;
             case SidecarDeviceState.POSTURE_HALF_OPENED:
                 state = FoldingFeature.STATE_HALF_OPENED;
                 break;
diff --git a/window/window/src/main/java/androidx/window/WindowBackend.java b/window/window/src/main/java/androidx/window/WindowBackend.java
index b45817b..3eb4869 100644
--- a/window/window/src/main/java/androidx/window/WindowBackend.java
+++ b/window/window/src/main/java/androidx/window/WindowBackend.java
@@ -17,7 +17,6 @@
 package androidx.window;
 
 import android.app.Activity;
-import android.content.Context;
 
 import androidx.annotation.NonNull;
 import androidx.core.util.Consumer;
@@ -38,27 +37,8 @@
             @NonNull Consumer<WindowLayoutInfo> callback);
 
     /**
-     * Registers a callback for layout changes of the window for {@link Activity} associated with
-     * the supplied {@link Context}. Must be called only after the it is attached to the window.
-     * @deprecated will be removed in the next alpha.
-     */
-    @Deprecated
-    void registerLayoutChangeCallback(@NonNull Context context, @NonNull Executor executor,
-            @NonNull Consumer<WindowLayoutInfo> callback);
-
-    /**
      * Unregisters a callback for window layout changes of the {@link Activity} window.
      */
     void unregisterLayoutChangeCallback(@NonNull Consumer<WindowLayoutInfo> callback);
 
-    /**
-     * Registers a callback for device state changes.
-     */
-    void registerDeviceStateChangeCallback(@NonNull Executor executor,
-            @NonNull Consumer<DeviceState> callback);
-
-    /**
-     * Unregisters a callback for device state changes.
-     */
-    void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback);
 }
diff --git a/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java b/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
index ac8d77a..48f0b1e 100644
--- a/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
+++ b/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
@@ -16,12 +16,8 @@
 
 package androidx.window;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -35,9 +31,6 @@
 import androidx.annotation.NonNull;
 import androidx.core.util.Consumer;
 
-import com.google.common.collect.BoundType;
-import com.google.common.collect.Range;
-
 import org.junit.Before;
 import org.junit.Test;
 
@@ -68,33 +61,6 @@
     }
 
     @Test
-    public void testRegisterDeviceStateChangeCallback_noExtension() {
-        // Verify method with extension
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = null;
-        SimpleConsumer<DeviceState> simpleConsumer = new SimpleConsumer<>();
-
-        backend.registerDeviceStateChangeCallback(directExecutor(), simpleConsumer);
-
-        DeviceState deviceState = simpleConsumer.lastValue();
-        assertNotNull(deviceState);
-        assertThat(deviceState.getPosture()).isIn(Range.range(
-                DeviceState.POSTURE_UNKNOWN, BoundType.CLOSED,
-                DeviceState.POSTURE_MAX_KNOWN, BoundType.CLOSED));
-        DeviceState initialLastReportedState = backend.mLastReportedDeviceState;
-
-        // Verify method without extension
-        backend.mWindowExtension = null;
-        SimpleConsumer<DeviceState> noExtensionConsumer = new SimpleConsumer<>();
-        backend.registerDeviceStateChangeCallback(directExecutor(), noExtensionConsumer);
-        deviceState = noExtensionConsumer.lastValue();
-        assertNotNull(deviceState);
-        assertEquals(DeviceState.POSTURE_UNKNOWN, deviceState.getPosture());
-        // Verify that last reported state does not change when using the getter
-        assertEquals(initialLastReportedState, backend.mLastReportedDeviceState);
-    }
-
-    @Test
     public void testRegisterLayoutChangeCallback() {
         ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
         backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
@@ -187,113 +153,6 @@
         verify(backend.mWindowExtension).onWindowLayoutChangeListenerRemoved(activity);
     }
 
-    @Test
-    public void testRegisterDeviceChangeCallback() {
-        ExtensionInterfaceCompat mockInterface = mock(ExtensionInterfaceCompat.class);
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = mockInterface;
-
-        // Check registering the device state change callback
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-
-        assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
-        verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(false));
-
-        // Check unregistering the device state change callback
-        backend.unregisterDeviceStateChangeCallback(consumer);
-
-        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
-        verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(true));
-    }
-
-    @Test
-    public void testDeviceChangeCallback() {
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
-        // Check that callbacks from the extension are propagated correctly
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-        DeviceState deviceState = newTestDeviceState();
-        ExtensionWindowBackend.ExtensionListenerImpl backendListener =
-                backend.new ExtensionListenerImpl();
-        backendListener.onDeviceStateChanged(deviceState);
-
-        verify(consumer, times(1)).accept(eq(deviceState));
-        assertEquals(deviceState, backend.mLastReportedDeviceState);
-
-        // Test that the same value wouldn't be reported again
-        backendListener.onDeviceStateChanged(deviceState);
-        verify(consumer, times(1)).accept(any());
-    }
-
-    @Test
-    public void testDeviceChangeChangeCallback_callsExtensionOnce() {
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
-        // Check registering the layout change callback
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-        backend.registerDeviceStateChangeCallback(Runnable::run, mock(DeviceStateConsumer.class));
-
-        assertEquals(2, backend.mDeviceStateChangeCallbacks.size());
-        verify(backend.mWindowExtension).onDeviceStateListenersChanged(false);
-
-        // Check unregistering the layout change callback
-        backend.unregisterDeviceStateChangeCallback(consumer);
-
-        assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
-        verify(backend.mWindowExtension, times(0))
-                .onDeviceStateListenersChanged(true);
-    }
-
-    @Test
-    public void testDeviceChangeChangeCallback_clearListeners() {
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
-        // Check registering the layout change callback
-        Consumer<DeviceState> firstConsumer = mock(DeviceStateConsumer.class);
-        Consumer<DeviceState> secondConsumer = mock(DeviceStateConsumer.class);
-        backend.registerDeviceStateChangeCallback(Runnable::run, firstConsumer);
-        backend.registerDeviceStateChangeCallback(Runnable::run, secondConsumer);
-
-        // Check unregistering the layout change callback
-        backend.unregisterDeviceStateChangeCallback(firstConsumer);
-        backend.unregisterDeviceStateChangeCallback(secondConsumer);
-
-        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
-        verify(backend.mWindowExtension).onDeviceStateListenersChanged(true);
-    }
-
-    @Test
-    public void testDeviceChangeCallback_relayLastEmittedValue() {
-        DeviceState expectedState = newTestDeviceState();
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-        backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-        backend.mLastReportedDeviceState = expectedState;
-
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-
-        verify(consumer).accept(expectedState);
-    }
-
-    @Test
-    public void testDeviceChangeCallback_clearLastEmittedValue() {
-        ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
-        Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-
-        backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-        backend.unregisterDeviceStateChangeCallback(consumer);
-
-        assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
-        assertNull(backend.mLastReportedDeviceState);
-    }
-
     private static WindowLayoutInfo newTestWindowLayoutInfo() {
         WindowLayoutInfo.Builder builder = new WindowLayoutInfo.Builder();
         return builder.build();
diff --git a/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java b/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
index bc14ba6..e889f98 100644
--- a/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
+++ b/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
@@ -17,7 +17,6 @@
 package androidx.window;
 
 import static androidx.window.FoldingFeature.STATE_FLAT;
-import static androidx.window.FoldingFeature.STATE_FLIPPED;
 import static androidx.window.FoldingFeature.STATE_HALF_OPENED;
 import static androidx.window.FoldingFeature.TYPE_FOLD;
 import static androidx.window.FoldingFeature.TYPE_HINGE;
@@ -93,7 +92,6 @@
 
         states.add(new FoldingFeature(bounds, type, STATE_FLAT));
         states.add(new FoldingFeature(bounds, type, STATE_HALF_OPENED));
-        states.add(new FoldingFeature(bounds, type, STATE_FLIPPED));
 
         return states;
     }
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 87093e5..a84b95f 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -68,6 +68,7 @@
     implementation(project(":work:work-runtime-ktx"))
     implementation(project(":work:work-multiprocess"))
     implementation(project(":work:work-gcm"))
+    implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
     implementation("androidx.arch.core:core-runtime:2.1.0")
     implementation("androidx.recyclerview:recyclerview:1.1.0")
     implementation(MATERIAL)
diff --git a/work/integration-tests/testapp/src/main/AndroidManifest.xml b/work/integration-tests/testapp/src/main/AndroidManifest.xml
index 8eb67e8..17f5540 100644
--- a/work/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/work/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -16,6 +16,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     package="androidx.work.integration.testapp">
+
     <application
         android:name=".TestApplication"
         android:allowBackup="true"
@@ -37,14 +38,26 @@
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
         </activity>
-        <service
-            android:name=".RemoteService"
-            android:exported="false"
-            android:process=":remote" />
+
         <provider
             android:name="androidx.startup.InitializationProvider"
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
+
+        <service
+            android:name="androidx.work.multiprocess.RemoteWorkerService"
+            android:exported="false"
+            android:process=":worker1" />
+
+        <service
+            android:name=".RemoteWorkerService2"
+            android:exported="false"
+            android:process=":worker2" />
+
+        <service
+            android:name=".RemoteService"
+            android:exported="false"
+            android:process=":remote" />
     </application>
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 </manifest>
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
index f792754..f65cae7 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
@@ -18,6 +18,8 @@
 
 import static androidx.work.ExistingWorkPolicy.KEEP;
 import static androidx.work.ExistingWorkPolicy.REPLACE;
+import static androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_CLASS_NAME;
+import static androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME;
 
 import android.app.PendingIntent;
 import android.app.job.JobInfo;
@@ -35,6 +37,7 @@
 import android.widget.EditText;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.lifecycle.Observer;
@@ -52,6 +55,7 @@
 import androidx.work.impl.workers.ConstraintTrackingWorker;
 import androidx.work.integration.testapp.imageprocessing.ImageProcessingActivity;
 import androidx.work.integration.testapp.sherlockholmes.AnalyzeSherlockHolmesActivity;
+import androidx.work.multiprocess.RemoteWorkerService;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -62,6 +66,7 @@
  */
 public class MainActivity extends AppCompatActivity {
 
+    private static final String PACKAGE_NAME = "androidx.work.integration.testapp";
     private static final String TAG = "MainActivity";
     private static final String CONSTRAINT_TRACKING_TAG = "ConstraintTrackingWorker";
     private static final String UNIQUE_WORK_NAME = "importantUniqueWork";
@@ -504,6 +509,36 @@
                     }
                 });
 
+        findViewById(R.id.enqueue_remote_worker_1).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                String serviceName = RemoteWorkerService.class.getName();
+                ComponentName componentName = new ComponentName(PACKAGE_NAME, serviceName);
+                OneTimeWorkRequest request = buildOneTimeWorkRemoteWorkRequest(componentName);
+                WorkManager.getInstance(MainActivity.this)
+                        .enqueue(request);
+            }
+        });
+
+        findViewById(R.id.enqueue_remote_worker_2).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                String serviceName = RemoteWorkerService2.class.getName();
+                ComponentName componentName = new ComponentName(PACKAGE_NAME, serviceName);
+                OneTimeWorkRequest request = buildOneTimeWorkRemoteWorkRequest(componentName);
+                WorkManager.getInstance(MainActivity.this)
+                        .enqueue(request);
+            }
+        });
+
+        findViewById(R.id.cancel_remote_workers).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                WorkManager.getInstance(MainActivity.this)
+                        .cancelAllWorkByTag(RemoteWorker.class.getName());
+            }
+        });
+
         findViewById(R.id.crash_app).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -540,6 +575,17 @@
         } else {
             hundredJobExceptionButton.setVisibility(View.GONE);
         }
+    }
 
+    @NonNull
+    OneTimeWorkRequest buildOneTimeWorkRemoteWorkRequest(@NonNull ComponentName componentName) {
+        Data data = new Data.Builder()
+                .putString(ARGUMENT_PACKAGE_NAME, componentName.getPackageName())
+                .putString(ARGUMENT_CLASS_NAME, componentName.getClassName())
+                .build();
+
+        return new OneTimeWorkRequest.Builder(RemoteWorker.class)
+                .setInputData(data)
+                .build();
     }
 }
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt
new file mode 100644
index 0000000..4e01eb24
--- /dev/null
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.integration.testapp
+
+import android.content.Context
+import android.util.Log
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import androidx.work.WorkerParameters
+import androidx.work.multiprocess.RemoteListenableWorker
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+class RemoteWorker(private val context: Context, private val parameters: WorkerParameters) :
+    RemoteListenableWorker(context, parameters) {
+    private var job: Job? = null
+    override fun startRemoteWork(): ListenableFuture<Result> {
+        return CallbackToFutureAdapter.getFuture { completer ->
+            Log.d(TAG, "Starting Remote Worker.")
+            val scope = CoroutineScope(Dispatchers.Default)
+
+            job = scope.launch {
+                delay(30 * 1000)
+            }
+            job?.invokeOnCompletion {
+                Log.d(TAG, "Done.")
+                completer.set(Result.success())
+            }
+        }
+    }
+
+    override fun onStopped() {
+        job?.cancel()
+    }
+
+    companion object {
+        private const val TAG = "RemoteWorker"
+    }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorkerService2.kt
similarity index 76%
copy from compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
copy to work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorkerService2.kt
index 5ece3c2..256bc17 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorkerService2.kt
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.catalog
+package androidx.work.integration.testapp
 
-import androidx.compose.integration.demos.common.ActivityDemo
+import androidx.work.multiprocess.RemoteWorkerService
 
-val MaterialCatalog = ActivityDemo("Material Catalog", CatalogActivity::class)
+class RemoteWorkerService2 : RemoteWorkerService()
\ No newline at end of file
diff --git a/work/integration-tests/testapp/src/main/res/layout/activity_main.xml b/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
index b5fd175..123781e 100644
--- a/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -287,6 +287,30 @@
             android:layout_marginLeft="16dp"
             android:layout_marginStart="16dp"/>
 
+        <Button android:text="@string/run_remote_worker_1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/enqueue_remote_worker_1"
+            android:layout_marginTop="12dp"
+            android:layout_marginLeft="16dp"
+            android:layout_marginStart="16dp"/>
+
+        <Button android:text="@string/run_remote_worker_2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/enqueue_remote_worker_2"
+            android:layout_marginTop="12dp"
+            android:layout_marginLeft="16dp"
+            android:layout_marginStart="16dp"/>
+
+        <Button android:text="@string/cancel_remote_workers"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:id="@+id/cancel_remote_workers"
+            android:layout_marginTop="12dp"
+            android:layout_marginLeft="16dp"
+            android:layout_marginStart="16dp"/>
+
         <Button android:text="@string/crash_app"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/work/integration-tests/testapp/src/main/res/values/strings.xml b/work/integration-tests/testapp/src/main/res/values/strings.xml
index 11703b4..3543341 100644
--- a/work/integration-tests/testapp/src/main/res/values/strings.xml
+++ b/work/integration-tests/testapp/src/main/res/values/strings.xml
@@ -34,6 +34,9 @@
     <string name="cancel_work_tag_multiprocess">Cancel Work By Tag (Multi-process)</string>
     <string name="cancel_all_work_multiprocess">Cancel All Work (Multi-process)</string>
     <string name="query_work_multiprocess">Query Work (Multi-process)</string>
+    <string name="run_remote_worker_1">Run Remote Worker 1</string>
+    <string name="run_remote_worker_2">Run Remote Worker 2</string>
+    <string name="cancel_remote_workers">Cancel Remote Workers</string>
     <string name="crash_app">Crash App</string>
     <string name="create_hundred_job_exception">Create 100 Job Exception</string>
     <string name="keep">Use KEEP</string>
diff --git a/work/workmanager-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java b/work/workmanager-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
index 39094ae..43acc2e 100644
--- a/work/workmanager-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
+++ b/work/workmanager-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
@@ -17,9 +17,13 @@
 package androidx.work.impl.background.gcm;
 
 
+import static androidx.work.NetworkType.TEMPORARILY_UNMETERED;
+
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import android.os.Build;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.work.Constraints;
@@ -98,6 +102,13 @@
                     break;
                 case NOT_REQUIRED:
                     builder.setRequiredNetwork(Task.NETWORK_STATE_ANY);
+                    break;
+                default:
+                    if (Build.VERSION.SDK_INT >= 30) {
+                        if (networkType == TEMPORARILY_UNMETERED) {
+                            builder.setRequiredNetwork(Task.NETWORK_STATE_ANY);
+                        }
+                    }
             }
 
             // Charging constraints
diff --git a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/BasicTest.kt b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/BasicTest.kt
index af939ff..9071e03 100644
--- a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/BasicTest.kt
+++ b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/BasicTest.kt
@@ -17,6 +17,7 @@
 package androidx.work.inspection
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.work.inspection.WorkManagerInspectorProtocol.Command
 import androidx.work.inspection.WorkManagerInspectorProtocol.TrackWorkManagerCommand
@@ -38,6 +39,7 @@
         testEnvironment.assertNoQueuedEvents()
     }
 
+    @FlakyTest
     @Test
     fun sendUnsetCommand() = runBlocking {
         testEnvironment.sendCommand(Command.getDefaultInstance())
diff --git a/work/workmanager-lint/src/main/java/androidx/work/lint/RemoveWorkManagerInitializerDetector.kt b/work/workmanager-lint/src/main/java/androidx/work/lint/RemoveWorkManagerInitializerDetector.kt
index c8a8c1b..8519043 100644
--- a/work/workmanager-lint/src/main/java/androidx/work/lint/RemoveWorkManagerInitializerDetector.kt
+++ b/work/workmanager-lint/src/main/java/androidx/work/lint/RemoveWorkManagerInitializerDetector.kt
@@ -46,7 +46,7 @@
 
     companion object {
 
-        private const val DESCRIPTION = "Remove androidx.work.impl.WorkManagerInitializer from " +
+        private const val DESCRIPTION = "Remove androidx.work.WorkManagerInitializer from " +
             "your AndroidManifest.xml when using on-demand initialization."
 
         val ISSUE = Issue.create(
@@ -105,7 +105,7 @@
         val metadataElements = element.getElementsByTagName("meta-data")
         val metadata = metadataElements.find { node ->
             val name = node.attributes.getNamedItemNS(ANDROID_URI, ATTR_NAME)?.textContent
-            name == "androidx.work.impl.WorkManagerInitializer"
+            name == "androidx.work.WorkManagerInitializer"
         }
         if (metadata != null && !removedDefaultInitializer) {
             location = context.getLocation(metadata)
diff --git a/work/workmanager-lint/src/test/java/androidx/work/lint/RemoveWorkManagerInitializerDetectorTest.kt b/work/workmanager-lint/src/test/java/androidx/work/lint/RemoveWorkManagerInitializerDetectorTest.kt
index 7a6cb1d..309580d 100644
--- a/work/workmanager-lint/src/test/java/androidx/work/lint/RemoveWorkManagerInitializerDetectorTest.kt
+++ b/work/workmanager-lint/src/test/java/androidx/work/lint/RemoveWorkManagerInitializerDetectorTest.kt
@@ -159,7 +159,7 @@
             .run()
             .expect(
                 """
-                project0: Error: Remove androidx.work.impl.WorkManagerInitializer from your AndroidManifest.xml when using on-demand initialization. [RemoveWorkManagerInitializer]
+                project0: Error: Remove androidx.work.WorkManagerInitializer from your AndroidManifest.xml when using on-demand initialization. [RemoveWorkManagerInitializer]
                 1 errors, 0 warnings
                 """.trimIndent()
             )
@@ -196,7 +196,7 @@
                           android:name="androidx.startup.InitializationProvider"
                           android:authorities="com.example.workmanager-init">
                           <meta-data
-                            android:name="androidx.work.impl.WorkManagerInitializer"
+                            android:name="androidx.work.WorkManagerInitializer"
                             android:value="@string/androidx_startup" />
                       </provider>
                   </application>
@@ -216,7 +216,7 @@
             .run()
             .expect(
                 """
-                AndroidManifest.xml:8: Error: Remove androidx.work.impl.WorkManagerInitializer from your AndroidManifest.xml when using on-demand initialization. [RemoveWorkManagerInitializer]
+                AndroidManifest.xml:8: Error: Remove androidx.work.WorkManagerInitializer from your AndroidManifest.xml when using on-demand initialization. [RemoveWorkManagerInitializer]
                            <meta-data
                            ^
                 1 errors, 0 warnings
diff --git a/work/workmanager-multiprocess/api/current.txt b/work/workmanager-multiprocess/api/current.txt
index e6f50d0..bcdaaae 100644
--- a/work/workmanager-multiprocess/api/current.txt
+++ b/work/workmanager-multiprocess/api/current.txt
@@ -1 +1,18 @@
 // Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/workmanager-multiprocess/api/public_plus_experimental_current.txt b/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
index e6f50d0..bcdaaae 100644
--- a/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
+++ b/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
@@ -1 +1,18 @@
 // Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/workmanager-multiprocess/api/restricted_current.txt b/work/workmanager-multiprocess/api/restricted_current.txt
index e6f50d0..bcdaaae 100644
--- a/work/workmanager-multiprocess/api/restricted_current.txt
+++ b/work/workmanager-multiprocess/api/restricted_current.txt
@@ -1 +1,18 @@
 // Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/workmanager-multiprocess/build.gradle b/work/workmanager-multiprocess/build.gradle
index 94d9a0c..a81a41f 100644
--- a/work/workmanager-multiprocess/build.gradle
+++ b/work/workmanager-multiprocess/build.gradle
@@ -41,6 +41,7 @@
 
 dependencies {
     api project(":work:work-runtime")
+    implementation("androidx.room:room-runtime:2.2.5")
     api(GUAVA_LISTENABLE_FUTURE)
     androidTestImplementation(KOTLIN_STDLIB)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/compose/ui/ui-viewbinding/src/androidTest/AndroidManifest.xml b/work/workmanager-multiprocess/src/androidTest/AndroidManifest.xml
similarity index 64%
rename from compose/ui/ui-viewbinding/src/androidTest/AndroidManifest.xml
rename to work/workmanager-multiprocess/src/androidTest/AndroidManifest.xml
index 6e4f4af..9721117 100644
--- a/compose/ui/ui-viewbinding/src/androidTest/AndroidManifest.xml
+++ b/work/workmanager-multiprocess/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2020 The Android Open Source Project
+  Copyright 2021 The Android Open Source Project
 
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
@@ -14,4 +13,11 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<manifest package="androidx.compose.ui.viewinterop" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.work.multiprocess.test">
+
+    <application>
+        <service android:name="androidx.work.multiprocess.RemoteWorkerService" />
+    </application>
+
+</manifest>
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ListenableWorkerImplClientTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ListenableWorkerImplClientTest.kt
new file mode 100644
index 0000000..ab84364
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ListenableWorkerImplClientTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.IBinder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.utils.SerialExecutor
+import androidx.work.impl.utils.futures.SettableFuture
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+public class ListenableWorkerImplClientTest {
+    private lateinit var mContext: Context
+    private lateinit var mWorkManager: WorkManagerImpl
+    private lateinit var mExecutor: Executor
+    private lateinit var mClient: ListenableWorkerImplClient
+
+    @Before
+    public fun setUp() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        mContext = mock(Context::class.java)
+        mWorkManager = mock(WorkManagerImpl::class.java)
+        `when`(mContext.applicationContext).thenReturn(mContext)
+        mExecutor = Executor {
+            it.run()
+        }
+
+        val taskExecutor = mock(TaskExecutor::class.java)
+        `when`(taskExecutor.backgroundExecutor).thenReturn(SerialExecutor(mExecutor))
+        `when`(mWorkManager.workTaskExecutor).thenReturn(taskExecutor)
+        mClient = ListenableWorkerImplClient(mContext, mExecutor)
+    }
+
+    @Test
+    @MediumTest
+    public fun failGracefullyWhenBindFails() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+        `when`(
+            mContext.bindService(
+                any(Intent::class.java),
+                any(ServiceConnection::class.java),
+                anyInt()
+            )
+        ).thenReturn(false)
+        val componentName = ComponentName("packageName", "className")
+        var exception: Throwable? = null
+        try {
+            mClient.getListenableWorkerImpl(componentName).get()
+        } catch (throwable: Throwable) {
+            exception = throwable
+        }
+        assertNotNull(exception)
+        val message = exception?.cause?.message ?: ""
+        assertTrue(message.contains("Unable to bind to service"))
+    }
+
+    @Test
+    @MediumTest
+    @Suppress("UNCHECKED_CAST")
+    public fun cleanUpWhenDispatcherFails() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val binder = mock(IBinder::class.java)
+        val remoteDispatcher =
+            mock(RemoteDispatcher::class.java) as RemoteDispatcher<IListenableWorkerImpl>
+        val remoteStub = mock(IListenableWorkerImpl::class.java)
+        val callback = spy(RemoteCallback())
+        val message = "Something bad happened"
+        `when`(remoteDispatcher.execute(remoteStub, callback)).thenThrow(RuntimeException(message))
+        `when`(remoteStub.asBinder()).thenReturn(binder)
+        val session = SettableFuture.create<IListenableWorkerImpl>()
+        session.set(remoteStub)
+        var exception: Throwable? = null
+        try {
+            mClient.execute(session, remoteDispatcher, callback).get()
+        } catch (throwable: Throwable) {
+            exception = throwable
+        }
+        assertNotNull(exception)
+        verify(callback).onFailure(message)
+    }
+
+    @Test
+    @MediumTest
+    @Suppress("UNCHECKED_CAST")
+    public fun cleanUpWhenSessionIsInvalid() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val remoteDispatcher =
+            mock(RemoteDispatcher::class.java) as RemoteDispatcher<IListenableWorkerImpl>
+        val callback = spy(RemoteCallback())
+        val session = SettableFuture.create<IListenableWorkerImpl>()
+        session.setException(RuntimeException("Something bad happened"))
+        var exception: Throwable? = null
+        try {
+            mClient.execute(session, remoteDispatcher, callback).get()
+        } catch (throwable: Throwable) {
+            exception = throwable
+        }
+        assertNotNull(exception)
+        verify(callback).onFailure(anyString())
+    }
+
+    @Test
+    @MediumTest
+    public fun cleanUpOnSuccessfulDispatch() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val binder = mock(IBinder::class.java)
+        val remoteDispatcher = RemoteDispatcher<IListenableWorkerImpl> { _, callback ->
+            callback.onSuccess(ByteArray(0))
+        }
+        val remoteStub = mock(IListenableWorkerImpl::class.java)
+        val callback = spy(RemoteCallback())
+        `when`(remoteStub.asBinder()).thenReturn(binder)
+        val session = SettableFuture.create<IListenableWorkerImpl>()
+        session.set(remoteStub)
+        var exception: Throwable? = null
+        try {
+            mClient.execute(session, remoteDispatcher, callback).get()
+        } catch (throwable: Throwable) {
+            exception = throwable
+        }
+        assertNull(exception)
+        verify(callback).onSuccess(any())
+    }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableDataTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableDataTest.kt
new file mode 100644
index 0000000..4f281f3
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableDataTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess
+
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.work.Data
+import androidx.work.multiprocess.parcelable.ParcelConverters
+import androidx.work.multiprocess.parcelable.ParcelableData
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+public class ParcelableDataTest {
+
+    @Test
+    @SmallTest
+    public fun testParcelableData() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val data = createData()
+        val parcelableData = ParcelableData(data)
+        val parcelled: ParcelableData =
+            ParcelConverters.unmarshall(
+                ParcelConverters.marshall(parcelableData),
+                ParcelableData.CREATOR
+            )
+        assertEquals(data, parcelled.data)
+    }
+
+    private fun createData(): Data {
+        val map = mutableMapOf<String, Any?>()
+        map["byte"] = 1.toByte()
+        map["boolean"] = false
+        map["int"] = 1
+        map["long"] = 10L
+        map["float"] = 99f
+        map["double"] = 99.0
+        map["string"] = "two"
+        map["byte array"] = byteArrayOf(1, 2, 3)
+        map["boolean array"] = booleanArrayOf(true, false, true)
+        map["int array"] = intArrayOf(1, 2, 3)
+        map["long array"] = longArrayOf(1L, 2L, 3L)
+        map["float array"] = floatArrayOf(1f, 2f, 3f)
+        map["double array"] = doubleArrayOf(1.0, 2.0, 3.0)
+        map["string array"] = listOf("a", "b", "c").toTypedArray()
+        map["null"] = null
+        val dataBuilder = Data.Builder()
+        dataBuilder.putAll(map)
+        return dataBuilder.build()
+    }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkContinuationImplTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkContinuationImplTest.kt
index ae64f39..29d7f79 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkContinuationImplTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkContinuationImplTest.kt
@@ -17,6 +17,7 @@
 package androidx.work.multiprocess
 
 import android.content.Context
+import android.os.Build
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.arch.core.executor.TaskExecutor
 import androidx.test.core.app.ApplicationProvider
@@ -50,6 +51,11 @@
 
     @Before
     public fun setUp() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         context = ApplicationProvider.getApplicationContext<Context>()
         val taskExecutor = object : TaskExecutor() {
             override fun executeOnDiskIO(runnable: Runnable) {
@@ -105,6 +111,11 @@
     @Test
     @MediumTest
     public fun basicContinuationTest() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val continuation = workManager.beginWith(listOf(first)).then(second)
@@ -115,6 +126,11 @@
     @Test
     @MediumTest
     public fun continuationTests2() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val third = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
@@ -126,6 +142,11 @@
     @Test
     @MediumTest
     public fun continuationTest3() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val continuation = workManager.beginUniqueWork(
@@ -138,6 +159,11 @@
     @Test
     @MediumTest
     public fun continuationTest4() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val continuation = workManager.beginUniqueWork(
@@ -154,6 +180,11 @@
     @Test
     @MediumTest
     public fun combineContinuationTests() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
         val third = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkInfoTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkInfoTest.kt
index 5b42c27..b77d969 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkInfoTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkInfoTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.work.multiprocess
 
+import android.os.Build
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.work.Data
@@ -31,9 +32,16 @@
 @RunWith(AndroidJUnit4::class)
 public class ParcelableWorkInfoTest {
 
+    // Setting the minSdkVersion to 27 otherwise we end up with SIGSEGVs.
+
     @Test
     @SmallTest
     public fun converterTest1() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val workInfo = WorkInfo(
             UUID.randomUUID(),
             WorkInfo.State.ENQUEUED,
@@ -48,6 +56,11 @@
     @Test
     @SmallTest
     public fun converterTest2() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val data = Data.Builder()
             .put("test", "testString")
             .put("int", 10)
@@ -67,6 +80,11 @@
     @Test
     @SmallTest
     public fun arrayConverterTest1() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val data = Data.Builder()
             .put("test", "testString")
             .put("int", 10)
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkQueryTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkQueryTest.kt
index c3dfbd5..64a7a50 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkQueryTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkQueryTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.work.multiprocess
 
+import android.os.Build
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.work.WorkInfo
@@ -33,6 +34,11 @@
     @Test
     @SmallTest
     public fun converterTest1() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val workQuery = WorkQuery.Builder.fromUniqueWorkNames(listOf("name1"))
             .addTags(listOf("tag1", "tag2"))
             .addIds(listOf(UUID.randomUUID()))
@@ -45,6 +51,11 @@
     @Test
     @SmallTest
     public fun converterTest2() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val workQuery = WorkQuery.Builder.fromUniqueWorkNames(listOf("name1"))
             .build()
 
@@ -54,6 +65,11 @@
     @Test
     @SmallTest
     public fun converterTest3() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val workQuery = WorkQuery.Builder.fromTags(listOf("tag1", "tag2"))
             .build()
 
@@ -63,6 +79,11 @@
     @Test
     @SmallTest
     public fun converterTest4() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val workQuery = WorkQuery.Builder.fromStates(listOf(WorkInfo.State.ENQUEUED))
             .build()
 
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
index d95f76b..d98d45f 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
@@ -17,8 +17,8 @@
 package androidx.work.multiprocess
 
 import android.net.Uri
+import android.os.Build
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import androidx.work.BackoffPolicy
 import androidx.work.Constraints
@@ -39,6 +39,11 @@
     @Test
     @SmallTest
     public fun converterTest1() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
             .addTag("Test Worker")
             .keepResultsForAtLeast(1, TimeUnit.DAYS)
@@ -49,6 +54,11 @@
     @Test
     @SmallTest
     public fun converterTest2() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
             .setInitialDelay(1, TimeUnit.HOURS)
             .setInputData(
@@ -69,8 +79,12 @@
 
     @Test
     @SmallTest
-    @SdkSuppress(minSdkVersion = 24)
     public fun converterTest3() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val uri = Uri.parse("test://foo")
         val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
             .setInitialDelay(1, TimeUnit.HOURS)
@@ -96,6 +110,11 @@
     @Test
     @SmallTest
     public fun converterTest4() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val requests = mutableListOf<WorkRequest>()
         repeat(10) {
             requests += OneTimeWorkRequest.Builder(TestWorker::class.java)
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt
new file mode 100644
index 0000000..fcc9575
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess
+
+import android.content.Context
+import androidx.work.Data
+import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * A Remote Listenable Worker which always fails.
+ */
+public class RemoteFailureWorker(context: Context, workerParameters: WorkerParameters) :
+    RemoteListenableWorker(context, workerParameters) {
+    override fun startRemoteWork(): ListenableFuture<Result> {
+        val future = SettableFuture.create<Result>()
+        val result = Result.failure(outputData())
+        future.set(result)
+        return future
+    }
+
+    public companion object {
+        public fun outputData(): Data {
+            return Data.Builder()
+                .put("output_1", 1)
+                .put("output_2,", "test")
+                .build()
+        }
+    }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
new file mode 100644
index 0000000..d490a33
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess
+
+import android.content.Context
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.work.Configuration
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkRequest
+import androidx.work.impl.Processor
+import androidx.work.impl.Scheduler
+import androidx.work.impl.WorkDatabase
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.WorkerWrapper
+import androidx.work.impl.foreground.ForegroundProcessor
+import androidx.work.impl.utils.SerialExecutor
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_CLASS_NAME
+import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+public class RemoteListenableWorkerTest {
+    private lateinit var mConfiguration: Configuration
+    private lateinit var mTaskExecutor: TaskExecutor
+    private lateinit var mScheduler: Scheduler
+    private lateinit var mProcessor: Processor
+    private lateinit var mForegroundProcessor: ForegroundProcessor
+    private lateinit var mWorkManager: WorkManagerImpl
+    private lateinit var mExecutor: Executor
+
+    // Necessary for the reified function
+    public lateinit var mContext: Context
+    public lateinit var mDatabase: WorkDatabase
+
+    @Before
+    public fun setUp() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        mContext = InstrumentationRegistry.getInstrumentation().context
+        mExecutor = Executor {
+            it.run()
+        }
+        mConfiguration = Configuration.Builder()
+            .setExecutor(mExecutor)
+            .setTaskExecutor(mExecutor)
+            .build()
+        mTaskExecutor = mock(TaskExecutor::class.java)
+        `when`(mTaskExecutor.backgroundExecutor).thenReturn(SerialExecutor(mExecutor))
+        `when`(mTaskExecutor.mainThreadExecutor).thenReturn(mExecutor)
+        mScheduler = mock(Scheduler::class.java)
+        mForegroundProcessor = mock(ForegroundProcessor::class.java)
+        mWorkManager = mock(WorkManagerImpl::class.java)
+        mDatabase = WorkDatabase.create(mContext, mExecutor, true)
+        val schedulers = listOf(mScheduler)
+        // Processor
+        mProcessor = Processor(mContext, mConfiguration, mTaskExecutor, mDatabase, schedulers)
+        // WorkManagerImpl
+        `when`(mWorkManager.configuration).thenReturn(mConfiguration)
+        `when`(mWorkManager.workTaskExecutor).thenReturn(mTaskExecutor)
+        `when`(mWorkManager.workDatabase).thenReturn(mDatabase)
+        `when`(mWorkManager.schedulers).thenReturn(schedulers)
+        `when`(mWorkManager.processor).thenReturn(mProcessor)
+        WorkManagerImpl.setDelegate(mWorkManager)
+    }
+
+    @Test
+    @MediumTest
+    public fun testRemoteSuccessWorker() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val request = buildRequest<RemoteSuccessWorker>()
+        val wrapper = buildWrapper(request)
+        wrapper.run()
+        wrapper.future.get()
+        val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+        assertEquals(workSpec.state, WorkInfo.State.SUCCEEDED)
+        assertEquals(workSpec.output, RemoteSuccessWorker.outputData())
+    }
+
+    @Test
+    @MediumTest
+    public fun testRemoteFailureWorker() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val request = buildRequest<RemoteFailureWorker>()
+        val wrapper = buildWrapper(request)
+        wrapper.run()
+        wrapper.future.get()
+        val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+        assertEquals(workSpec.state, WorkInfo.State.FAILED)
+        assertEquals(workSpec.output, RemoteFailureWorker.outputData())
+    }
+
+    @Test
+    @MediumTest
+    public fun testRemoteRetryWorker() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val request = buildRequest<RemoteRetryWorker>()
+        val wrapper = buildWrapper(request)
+        wrapper.run()
+        wrapper.future.get()
+        val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+        assertEquals(workSpec.state, WorkInfo.State.ENQUEUED)
+    }
+
+    public inline fun <reified T : RemoteListenableWorker> buildRequest(): OneTimeWorkRequest {
+        val inputData = Data.Builder()
+            .putString(ARGUMENT_PACKAGE_NAME, mContext.packageName)
+            .putString(ARGUMENT_CLASS_NAME, RemoteWorkerService::class.java.name)
+            .build()
+
+        val request = OneTimeWorkRequest.Builder(T::class.java)
+            .setInputData(inputData)
+            .build()
+
+        mDatabase.workSpecDao().insertWorkSpec(request.workSpec)
+        return request
+    }
+
+    public fun buildWrapper(request: WorkRequest): WorkerWrapper {
+        return WorkerWrapper.Builder(
+            mContext,
+            mConfiguration,
+            mTaskExecutor,
+            mForegroundProcessor,
+            mDatabase,
+            request.stringId
+        ).build()
+    }
+}
\ No newline at end of file
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt
new file mode 100644
index 0000000..68afb87b
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * A Remote Listenable Worker which always retries.
+ */
+public class RemoteRetryWorker(context: Context, workerParameters: WorkerParameters) :
+    RemoteListenableWorker(context, workerParameters) {
+    override fun startRemoteWork(): ListenableFuture<Result> {
+        val future = SettableFuture.create<Result>()
+        val result = Result.retry()
+        future.set(result)
+        return future
+    }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt
new file mode 100644
index 0000000..810f3ef
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess
+
+import android.content.Context
+import androidx.work.Data
+import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * A Remote Listenable Worker which always succeeds.
+ */
+public class RemoteSuccessWorker(context: Context, workerParameters: WorkerParameters) :
+    RemoteListenableWorker(context, workerParameters) {
+    override fun startRemoteWork(): ListenableFuture<Result> {
+        val future = SettableFuture.create<Result>()
+        val result = Result.success(outputData())
+        future.set(result)
+        return future
+    }
+
+    public companion object {
+        public fun outputData(): Data {
+            return Data.Builder()
+                .put("output_1", 1)
+                .put("output_2,", "test")
+                .build()
+        }
+    }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
index fba4ec3..f74b7cf 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
@@ -53,6 +53,11 @@
 
     @Before
     public fun setUp() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         mContext = mock(Context::class.java)
         mWorkManager = mock(WorkManagerImpl::class.java)
         `when`(mContext.applicationContext).thenReturn(mContext)
@@ -94,9 +99,16 @@
 
     @Test
     @MediumTest
+    @Suppress("UNCHECKED_CAST")
     public fun cleanUpWhenDispatcherFails() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val binder = mock(IBinder::class.java)
-        val remoteDispatcher = mock(RemoteWorkManagerClient.RemoteDispatcher::class.java)
+        val remoteDispatcher =
+            mock(RemoteDispatcher::class.java) as RemoteDispatcher<IWorkManagerImpl>
         val remoteStub = mock(IWorkManagerImpl::class.java)
         val callback = spy(RemoteCallback())
         val message = "Something bad happened"
@@ -117,8 +129,15 @@
 
     @Test
     @MediumTest
+    @Suppress("UNCHECKED_CAST")
     public fun cleanUpWhenSessionIsInvalid() {
-        val remoteDispatcher = mock(RemoteWorkManagerClient.RemoteDispatcher::class.java)
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val remoteDispatcher =
+            mock(RemoteDispatcher::class.java) as RemoteDispatcher<IWorkManagerImpl>
         val callback = spy(RemoteCallback())
         val session = SettableFuture.create<IWorkManagerImpl>()
         session.setException(RuntimeException("Something bad happened"))
@@ -136,8 +155,13 @@
     @Test
     @MediumTest
     public fun cleanUpOnSuccessfulDispatch() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
         val binder = mock(IBinder::class.java)
-        val remoteDispatcher = RemoteWorkManagerClient.RemoteDispatcher { _, callback ->
+        val remoteDispatcher = RemoteDispatcher<IWorkManagerImpl> { _, callback ->
             callback.onSuccess(ByteArray(0))
         }
         val remoteStub = mock(IWorkManagerImpl::class.java)
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
index f97bee9..f3e5022 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
@@ -81,16 +81,16 @@
         public void run() {
             try {
                 I result = mCallback.mFuture.get();
-                successCallback(mCallback.mCallback, mCallback.toByteArray(result));
+                reportSuccess(mCallback.mCallback, mCallback.toByteArray(result));
             } catch (Throwable throwable) {
-                failureCallback(mCallback.mCallback, throwable);
+                reportFailure(mCallback.mCallback, throwable);
             }
         }
 
         /**
          * Dispatches successful callbacks safely.
          */
-        public static void successCallback(
+        public static void reportSuccess(
                 @NonNull IWorkManagerImplCallback callback,
                 @NonNull byte[] response) {
             try {
@@ -103,7 +103,7 @@
         /**
          * Dispatches failures callbacks safely.
          **/
-        public static void failureCallback(
+        public static void reportFailure(
                 @NonNull IWorkManagerImplCallback callback,
                 @NonNull Throwable throwable) {
             try {
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java
new file mode 100644
index 0000000..c4c1cca
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess;
+
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportFailure;
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportSuccess;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.Configuration;
+import androidx.work.ListenableWorker;
+import androidx.work.Logger;
+import androidx.work.WorkerParameters;
+import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.utils.futures.SettableFuture;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import androidx.work.multiprocess.parcelable.ParcelConverters;
+import androidx.work.multiprocess.parcelable.ParcelableRemoteWorkRequest;
+import androidx.work.multiprocess.parcelable.ParcelableResult;
+import androidx.work.multiprocess.parcelable.ParcelableWorkerParameters;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * An implementation of ListenableWorker that can be executed in a remote process.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ListenableWorkerImpl extends IListenableWorkerImpl.Stub {
+    // Synthetic access
+    static final String TAG = Logger.tagWithPrefix("ListenableWorkerImpl");
+    // Synthetic access
+    static byte[] sEMPTY = new byte[0];
+    // Synthetic access
+    static final Object sLock = new Object();
+
+    // Synthetic access
+    final Context mContext;
+    // Synthetic access
+    final WorkManagerImpl mWorkManager;
+    // Synthetic access
+    final Configuration mConfiguration;
+    // Synthetic access
+    final TaskExecutor mTaskExecutor;
+    // Synthetic access
+    final Map<String, ListenableFuture<ListenableWorker.Result>> mFutureMap;
+
+    ListenableWorkerImpl(@NonNull Context context) {
+        mContext = context.getApplicationContext();
+        mWorkManager = WorkManagerImpl.getInstance(mContext);
+        mConfiguration = mWorkManager.getConfiguration();
+        mTaskExecutor = mWorkManager.getWorkTaskExecutor();
+        mFutureMap = new HashMap<>();
+    }
+
+    @Override
+    public void startWork(
+            @NonNull final byte[] request,
+            @NonNull final IWorkManagerImplCallback callback) {
+        try {
+            ParcelableRemoteWorkRequest parcelableRemoteWorkRequest =
+                    ParcelConverters.unmarshall(request, ParcelableRemoteWorkRequest.CREATOR);
+
+            ParcelableWorkerParameters parcelableWorkerParameters =
+                    parcelableRemoteWorkRequest.getParcelableWorkerParameters();
+
+            WorkerParameters workerParameters =
+                    parcelableWorkerParameters.toWorkerParameters(mWorkManager);
+
+            final String id = workerParameters.getId().toString();
+            final String workerClassName = parcelableRemoteWorkRequest.getWorkerClassName();
+
+            Logger.get().debug(TAG,
+                    String.format("Executing work request (%s, %s)", id, workerClassName));
+
+            final ListenableFuture<ListenableWorker.Result> futureResult =
+                    executeWorkRequest(id, workerClassName, workerParameters);
+
+            futureResult.addListener(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        ListenableWorker.Result result = futureResult.get();
+                        ParcelableResult parcelableResult = new ParcelableResult(result);
+                        byte[] response = ParcelConverters.marshall(parcelableResult);
+                        reportSuccess(callback, response);
+                    } catch (ExecutionException | InterruptedException exception) {
+                        reportFailure(callback, exception);
+                    } catch (CancellationException cancellationException) {
+                        Logger.get().debug(TAG, String.format("Worker (%s) was cancelled", id));
+                        reportFailure(callback, cancellationException);
+                    } finally {
+                        synchronized (sLock) {
+                            mFutureMap.remove(id);
+                        }
+                    }
+                }
+            }, mTaskExecutor.getBackgroundExecutor());
+        } catch (Throwable throwable) {
+            reportFailure(callback, throwable);
+        }
+    }
+
+    @Override
+    public void interrupt(
+            @NonNull final byte[] request,
+            @NonNull final IWorkManagerImplCallback callback) {
+        try {
+            ParcelableWorkerParameters parcelableWorkerParameters =
+                    ParcelConverters.unmarshall(request, ParcelableWorkerParameters.CREATOR);
+            final String id = parcelableWorkerParameters.getId().toString();
+            Logger.get().debug(TAG, String.format("Interrupting work with id (%s)", id));
+
+            final ListenableFuture<ListenableWorker.Result> future;
+            synchronized (sLock) {
+                future = mFutureMap.remove(id);
+            }
+            if (future != null) {
+                mWorkManager.getWorkTaskExecutor().getBackgroundExecutor()
+                        .execute(new Runnable() {
+                            @Override
+                            public void run() {
+                                future.cancel(true);
+                                reportSuccess(callback, sEMPTY);
+                            }
+                        });
+            } else {
+                // Nothing to do.
+                reportSuccess(callback, sEMPTY);
+            }
+        } catch (Throwable throwable) {
+            reportFailure(callback, throwable);
+        }
+    }
+
+    @NonNull
+    private ListenableFuture<ListenableWorker.Result> executeWorkRequest(
+            @NonNull String id,
+            @NonNull String workerClassName,
+            @NonNull WorkerParameters workerParameters) {
+
+        final SettableFuture<ListenableWorker.Result> future = SettableFuture.create();
+
+        Logger.get().debug(TAG,
+                String.format("Tracking execution of %s (%s)", id, workerClassName));
+
+        synchronized (sLock) {
+            mFutureMap.put(id, future);
+        }
+
+        ListenableWorker worker = mConfiguration.getWorkerFactory()
+                .createWorkerWithDefaultFallback(mContext, workerClassName, workerParameters);
+
+        if (worker == null) {
+            String message = String.format(
+                    "Unable to create an instance of %s", workerClassName);
+            Logger.get().error(TAG, message);
+            future.setException(new IllegalStateException(message));
+            return future;
+        }
+
+        if (!(worker instanceof RemoteListenableWorker)) {
+            String message = String.format(
+                    "%s does not extend %s",
+                    workerClassName,
+                    RemoteListenableWorker.class.getName()
+            );
+            Logger.get().error(TAG, message);
+            future.setException(new IllegalStateException(message));
+            return future;
+        }
+
+        try {
+            RemoteListenableWorker remoteListenableWorker = (RemoteListenableWorker) worker;
+            future.setFuture(remoteListenableWorker.startRemoteWork());
+        } catch (Throwable throwable) {
+            future.setException(throwable);
+        }
+
+        return future;
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
new file mode 100644
index 0000000..5977450
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess;
+
+import static android.content.Context.BIND_AUTO_CREATE;
+
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportFailure;
+
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.Logger;
+import androidx.work.impl.utils.futures.SettableFuture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/***
+ * A client for {@link IListenableWorkerImpl}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ListenableWorkerImplClient {
+    // Synthetic access
+    static final String TAG = Logger.tagWithPrefix("ListenableWorkerImplClient");
+
+    // Synthetic access
+    final Context mContext;
+
+    // Synthetic access
+    final Executor mExecutor;
+
+    public ListenableWorkerImplClient(
+            @NonNull Context context,
+            @NonNull Executor executor) {
+        mContext = context;
+        mExecutor = executor;
+    }
+
+    /**
+     * @return a {@link ListenableFuture} of {@link IListenableWorkerImpl} after a
+     * {@link ServiceConnection} is established.
+     */
+    @NonNull
+    public ListenableFuture<IListenableWorkerImpl> getListenableWorkerImpl(
+            @NonNull ComponentName component) {
+
+        Logger.get().debug(TAG,
+                String.format("Binding to %s, %s", component.getPackageName(),
+                        component.getClassName()));
+
+        Connection session = new Connection();
+        try {
+            Intent intent = new Intent();
+            intent.setComponent(component);
+            boolean bound = mContext.bindService(intent, session, BIND_AUTO_CREATE);
+            if (!bound) {
+                unableToBind(session, new RuntimeException("Unable to bind to service"));
+            }
+        } catch (Throwable throwable) {
+            unableToBind(session, throwable);
+        }
+
+        return session.mFuture;
+    }
+
+    /**
+     * Executes a method on an instance of {@link IListenableWorkerImpl} using the instance of
+     * {@link RemoteDispatcher}.
+     */
+    @NonNull
+    public ListenableFuture<byte[]> execute(
+            @NonNull ComponentName componentName,
+            @NonNull RemoteDispatcher<IListenableWorkerImpl> dispatcher) {
+
+        ListenableFuture<IListenableWorkerImpl> session = getListenableWorkerImpl(componentName);
+        return execute(session, dispatcher, new RemoteCallback());
+    }
+
+    /**
+     * Executes a method on an instance of {@link IListenableWorkerImpl} using the instance of
+     * {@link RemoteDispatcher} and the {@link RemoteCallback}.
+     */
+    @NonNull
+    @SuppressLint("LambdaLast")
+    public ListenableFuture<byte[]> execute(
+            @NonNull ListenableFuture<IListenableWorkerImpl> session,
+            @NonNull final RemoteDispatcher<IListenableWorkerImpl> dispatcher,
+            @NonNull final RemoteCallback callback) {
+
+        session.addListener(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    final IListenableWorkerImpl iListenableWorker = session.get();
+                    callback.setBinder(iListenableWorker.asBinder());
+                    mExecutor.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            try {
+                                dispatcher.execute(iListenableWorker, callback);
+                            } catch (Throwable innerThrowable) {
+                                Logger.get().error(TAG, "Unable to execute", innerThrowable);
+                                reportFailure(callback, innerThrowable);
+                            }
+                        }
+                    });
+                } catch (ExecutionException | InterruptedException exception) {
+                    String message = "Unable to bind to service";
+                    Logger.get().error(TAG, message, exception);
+                    reportFailure(callback, exception);
+                }
+            }
+        }, mExecutor);
+        return callback.getFuture();
+    }
+
+    /**
+     * The implementation of {@link ServiceConnection} that handles changes in the connection.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static class Connection implements ServiceConnection {
+        private static final String TAG = Logger.tagWithPrefix("ListenableWorkerImplSession");
+
+        final SettableFuture<IListenableWorkerImpl> mFuture;
+
+        public Connection() {
+            mFuture = SettableFuture.create();
+        }
+
+        @Override
+        public void onServiceConnected(
+                @NonNull ComponentName componentName,
+                @NonNull IBinder iBinder) {
+            Logger.get().debug(TAG, "Service connected");
+            IListenableWorkerImpl iListenableWorkerImpl =
+                    IListenableWorkerImpl.Stub.asInterface(iBinder);
+            mFuture.set(iListenableWorkerImpl);
+        }
+
+        @Override
+        public void onServiceDisconnected(@NonNull ComponentName componentName) {
+            Logger.get().warning(TAG, "Service disconnected");
+            mFuture.setException(new RuntimeException("Service disconnected"));
+        }
+
+        @Override
+        public void onBindingDied(@NonNull ComponentName name) {
+            Logger.get().warning(TAG, "Binding died");
+            mFuture.setException(new RuntimeException("Binding died"));
+        }
+
+        @Override
+        public void onNullBinding(@NonNull ComponentName name) {
+            Logger.get().error(TAG, "Unable to bind to service");
+            mFuture.setException(
+                    new RuntimeException(String.format("Cannot bind to service %s", name)));
+        }
+    }
+
+    private static void unableToBind(@NonNull Connection session, @NonNull Throwable throwable) {
+        Logger.get().error(TAG, "Unable to bind to service", throwable);
+        session.mFuture.setException(throwable);
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteClientUtils.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteClientUtils.java
new file mode 100644
index 0000000..e2da73e
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteClientUtils.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.arch.core.util.Function;
+import androidx.work.impl.utils.futures.SettableFuture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A collection of utilities which make using
+ * {@link com.google.common.util.concurrent.ListenableFuture} easier.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class RemoteClientUtils {
+    private RemoteClientUtils() {
+        // Utilities
+    }
+
+    /**
+     * A mapper that essentially drops the byte[].
+     */
+    public static final Function<byte[], Void> sVoidMapper = new Function<byte[], Void>() {
+        @Override
+        public Void apply(byte[] input) {
+            return null;
+        }
+    };
+
+    /**
+     * Defines a mapper for a {@link ListenableFuture}.
+     */
+    @NonNull
+    public static <I, O> ListenableFuture<O> map(
+            @NonNull final ListenableFuture<I> input,
+            @NonNull final Function<I, O> transformation,
+            @NonNull Executor executor) {
+
+        final SettableFuture<O> output = SettableFuture.create();
+        input.addListener(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    I in = input.get();
+                    O out = transformation.apply(in);
+                    output.set(out);
+                } catch (Throwable throwable) {
+                    Throwable cause = throwable.getCause();
+                    cause = cause == null ? throwable : cause;
+                    output.setException(cause);
+                }
+            }
+        }, executor);
+        return output;
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java
new file mode 100644
index 0000000..5a7a385
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * @param <T> The remote interface subtype that usually implements {@link android.os.IBinder}.
+ * @hide
+ */
+@SuppressLint("LambdaLast")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface RemoteDispatcher<T> {
+    /**
+     * Perform the actual work given an instance of {@link IWorkManagerImpl} and the
+     * {@link IWorkManagerImplCallback} callback.
+     *
+     * @param binder   the remote interface implementation
+     * @param callback the {@link IWorkManagerImplCallback} instance
+     */
+    void execute(@NonNull T binder,
+            @NonNull IWorkManagerImplCallback callback) throws Throwable;
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java
new file mode 100644
index 0000000..cb124f1
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.RemoteException;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.util.Function;
+import androidx.work.Data;
+import androidx.work.ListenableWorker;
+import androidx.work.Logger;
+import androidx.work.WorkerParameters;
+import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.futures.SettableFuture;
+import androidx.work.multiprocess.parcelable.ParcelConverters;
+import androidx.work.multiprocess.parcelable.ParcelableRemoteWorkRequest;
+import androidx.work.multiprocess.parcelable.ParcelableResult;
+import androidx.work.multiprocess.parcelable.ParcelableWorkerParameters;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+
+/**
+ * Is an implementation of a {@link ListenableWorker} that can bind to a remote process.
+ * <p>
+ * To be able to bind to a remote process, A {@link RemoteListenableWorker} needs additional
+ * arguments as part of its input {@link Data}.
+ * <p>
+ * The arguments ({@link #ARGUMENT_PACKAGE_NAME}, {@link #ARGUMENT_CLASS_NAME}) are used to
+ * determine the {@link android.app.Service} that the {@link RemoteListenableWorker} can bind to.
+ * {@link #startRemoteWork()} is then subsequently called in the process that the
+ * {@link android.app.Service} is running in.
+ */
+public abstract class RemoteListenableWorker extends ListenableWorker {
+    // Synthetic access
+    static final String TAG = Logger.tagWithPrefix("RemoteListenableWorker");
+
+    /**
+     * The {@code #ARGUMENT_PACKAGE_NAME}, {@link #ARGUMENT_CLASS_NAME} together determine the
+     * {@link ComponentName} that the {@link RemoteListenableWorker} binds to before calling
+     * {@link #startRemoteWork()}.
+     */
+    public static final String ARGUMENT_PACKAGE_NAME =
+            "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+
+    /**
+     * The {@link #ARGUMENT_PACKAGE_NAME}, {@code className} together determine the
+     * {@link ComponentName} that the {@link RemoteListenableWorker} binds to before calling
+     * {@link #startRemoteWork()}.
+     */
+    public static final String ARGUMENT_CLASS_NAME =
+            "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+
+    // Synthetic access
+    final WorkerParameters mWorkerParameters;
+
+    // Synthetic access
+    final WorkManagerImpl mWorkManager;
+
+    // Synthetic access
+    final Executor mExecutor;
+
+    // Synthetic access
+    final ListenableWorkerImplClient mClient;
+
+    // Synthetic access
+    @Nullable
+    String mWorkerClassName;
+
+    @Nullable
+    private ComponentName mComponentName;
+
+    /**
+     * @param appContext   The application {@link Context}
+     * @param workerParams {@link WorkerParameters} to setup the internal state of this worker
+     */
+    public RemoteListenableWorker(
+            @NonNull Context appContext,
+            @NonNull WorkerParameters workerParams) {
+        super(appContext, workerParams);
+        mWorkerParameters = workerParams;
+        mWorkManager = WorkManagerImpl.getInstance(appContext);
+        mExecutor = mWorkManager.getWorkTaskExecutor().getBackgroundExecutor();
+        mClient = new ListenableWorkerImplClient(getApplicationContext(), mExecutor);
+    }
+
+    @Override
+    @NonNull
+    public final ListenableFuture<Result> startWork() {
+        SettableFuture<Result> future = SettableFuture.create();
+        Data data = getInputData();
+        final String id = mWorkerParameters.getId().toString();
+        String packageName = data.getString(ARGUMENT_PACKAGE_NAME);
+        String serviceClassName = data.getString(ARGUMENT_CLASS_NAME);
+
+        if (TextUtils.isEmpty(packageName)) {
+            String message = "Need to specify a package name for the Remote Service.";
+            Logger.get().error(TAG, message);
+            future.setException(new IllegalArgumentException(message));
+            return future;
+        }
+
+        if (TextUtils.isEmpty(serviceClassName)) {
+            String message = "Need to specify a class name for the Remote Service.";
+            Logger.get().error(TAG, message);
+            future.setException(new IllegalArgumentException(message));
+            return future;
+        }
+
+        mComponentName = new ComponentName(packageName, serviceClassName);
+
+        ListenableFuture<byte[]> result = mClient.execute(
+                mComponentName,
+                new RemoteDispatcher<IListenableWorkerImpl>() {
+                    @Override
+                    public void execute(
+                            @NonNull IListenableWorkerImpl listenableWorkerImpl,
+                            @NonNull IWorkManagerImplCallback callback) throws RemoteException {
+
+                        WorkSpec workSpec = mWorkManager.getWorkDatabase()
+                                .workSpecDao()
+                                .getWorkSpec(id);
+
+                        mWorkerClassName = workSpec.workerClassName;
+                        ParcelableRemoteWorkRequest remoteWorkRequest =
+                                new ParcelableRemoteWorkRequest(
+                                        workSpec.workerClassName, mWorkerParameters
+                                );
+                        byte[] request = ParcelConverters.marshall(remoteWorkRequest);
+                        listenableWorkerImpl.startWork(request, callback);
+                    }
+                });
+
+        return RemoteClientUtils.map(result, new Function<byte[], Result>() {
+            @Override
+            public Result apply(byte[] input) {
+                ParcelableResult parcelableResult = ParcelConverters.unmarshall(input,
+                        ParcelableResult.CREATOR);
+                return parcelableResult.getResult();
+            }
+        }, mExecutor);
+    }
+
+    /**
+     * Override this method to define the work that needs to run in the remote process. This method
+     * is called on the main thread.
+     * <p>
+     * A ListenableWorker is given a maximum of ten minutes to finish its execution and return a
+     * {@code Result}.  After this time has expired, the worker will be signalled to stop and its
+     * {@link ListenableFuture} will be cancelled. Note that the 10 minute execution window also
+     * includes the cost of binding to the remote process.
+     *
+     * @return A {@link ListenableFuture} with the {@code Result} of the computation.  If you
+     * cancel this Future, WorkManager will treat this unit of work as a {@code Result#failure()}.
+     */
+    @NonNull
+    public abstract ListenableFuture<Result> startRemoteWork();
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @SuppressWarnings("FutureReturnValueIgnored")
+    public void onStopped() {
+        super.onStopped();
+        // Delegate interruptions to the remote process.
+        if (mComponentName != null) {
+            mClient.execute(mComponentName,
+                    new RemoteDispatcher<IListenableWorkerImpl>() {
+                        @Override
+                        public void execute(
+                                @NonNull IListenableWorkerImpl listenableWorkerImpl,
+                                @NonNull IWorkManagerImplCallback callback)
+                                throws RemoteException {
+                            ParcelableWorkerParameters parcelableWorkerParameters =
+                                    new ParcelableWorkerParameters(mWorkerParameters);
+                            byte[] request = ParcelConverters.marshall(parcelableWorkerParameters);
+                            listenableWorkerImpl.interrupt(request, callback);
+                        }
+                    });
+        }
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
index 59bb5b0f..039948a 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
@@ -18,7 +18,9 @@
 
 import static android.content.Context.BIND_AUTO_CREATE;
 
-import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.failureCallback;
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportFailure;
+import static androidx.work.multiprocess.RemoteClientUtils.map;
+import static androidx.work.multiprocess.RemoteClientUtils.sVoidMapper;
 
 import android.annotation.SuppressLint;
 import android.content.ComponentName;
@@ -77,6 +79,7 @@
 
     private Session mSession;
 
+    @SuppressLint("BanKeepAnnotation")
     @Keep
     public RemoteWorkManagerClient(@NonNull Context context, @NonNull WorkManagerImpl workManager) {
         mContext = context.getApplicationContext();
@@ -95,7 +98,7 @@
     @NonNull
     @Override
     public ListenableFuture<Void> enqueue(@NonNull final List<WorkRequest> requests) {
-        ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+        ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
             @Override
             public void execute(
                     @NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -150,7 +153,7 @@
     @NonNull
     @Override
     public ListenableFuture<Void> enqueue(@NonNull final WorkContinuation continuation) {
-        ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+        ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
             @Override
             public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
                     @NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -166,7 +169,7 @@
     @NonNull
     @Override
     public ListenableFuture<Void> cancelWorkById(@NonNull final UUID id) {
-        ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+        ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
             @Override
             public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
                     @NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -179,7 +182,7 @@
     @NonNull
     @Override
     public ListenableFuture<Void> cancelAllWorkByTag(@NonNull final String tag) {
-        ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+        ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
             @Override
             public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
                     @NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -192,7 +195,7 @@
     @NonNull
     @Override
     public ListenableFuture<Void> cancelUniqueWork(@NonNull final String uniqueWorkName) {
-        ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+        ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
             @Override
             public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
                     @NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -205,7 +208,7 @@
     @NonNull
     @Override
     public ListenableFuture<Void> cancelAllWork() {
-        ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+        ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
             @Override
             public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
                     @NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -218,7 +221,7 @@
     @NonNull
     @Override
     public ListenableFuture<List<WorkInfo>> getWorkInfos(@NonNull final WorkQuery workQuery) {
-        ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+        ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
             @Override
             public void execute(
                     @NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -244,7 +247,8 @@
      * @return The {@link ListenableFuture} instance.
      */
     @NonNull
-    public ListenableFuture<byte[]> execute(@NonNull final RemoteDispatcher dispatcher) {
+    public ListenableFuture<byte[]> execute(
+            @NonNull final RemoteDispatcher<IWorkManagerImpl> dispatcher) {
         return execute(getSession(), dispatcher, new RemoteCallback());
     }
 
@@ -261,7 +265,7 @@
     @VisibleForTesting
     ListenableFuture<byte[]> execute(
             @NonNull final ListenableFuture<IWorkManagerImpl> session,
-            @NonNull final RemoteDispatcher dispatcher,
+            @NonNull final RemoteDispatcher<IWorkManagerImpl> dispatcher,
             @NonNull final RemoteCallback callback) {
         session.addListener(new Runnable() {
             @Override
@@ -277,13 +281,13 @@
                                 dispatcher.execute(iWorkManager, callback);
                             } catch (Throwable innerThrowable) {
                                 Logger.get().error(TAG, "Unable to execute", innerThrowable);
-                                failureCallback(callback, innerThrowable);
+                                reportFailure(callback, innerThrowable);
                             }
                         }
                     });
                 } catch (ExecutionException | InterruptedException exception) {
                     Logger.get().error(TAG, "Unable to bind to service");
-                    failureCallback(callback, new RuntimeException("Unable to bind to service"));
+                    reportFailure(callback, new RuntimeException("Unable to bind to service"));
                     cleanUp();
                 }
             }
@@ -335,53 +339,6 @@
     }
 
     /**
-     * A mapper that essentially drops the byte[].
-     */
-    private static final Function<byte[], Void> sVoidMapper = new Function<byte[], Void>() {
-        @Override
-        public Void apply(byte[] input) {
-            return null;
-        }
-    };
-
-    private static <I, O> ListenableFuture<O> map(
-            @NonNull final ListenableFuture<I> input,
-            @NonNull final Function<I, O> transformation,
-            @NonNull Executor executor) {
-
-        final SettableFuture<O> output = SettableFuture.create();
-        input.addListener(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    I in = input.get();
-                    O out = transformation.apply(in);
-                    output.set(out);
-                } catch (Throwable throwable) {
-                    Throwable cause = throwable.getCause();
-                    cause = cause == null ? throwable : cause;
-                    output.setException(cause);
-                }
-            }
-        }, executor);
-        return output;
-    }
-
-    /**
-     * @hide
-     */
-    @SuppressLint("LambdaLast")
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public interface RemoteDispatcher {
-        /**
-         * Perform the actual work given an instance of {@link IWorkManagerImpl} and the
-         * {@link IWorkManagerImplCallback} callback.
-         */
-        void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
-                @NonNull IWorkManagerImplCallback callback) throws Throwable;
-    }
-
-    /**
      * The implementation of {@link ServiceConnection} that handles changes in the connection.
      *
      * @hide
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
index 134d478..68cb11d 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
@@ -17,7 +17,7 @@
 package androidx.work.multiprocess;
 
 
-import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.failureCallback;
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportFailure;
 
 import android.content.Context;
 
@@ -81,7 +81,7 @@
                     };
             listenableCallback.dispatchCallbackSafely();
         } catch (Throwable throwable) {
-            failureCallback(callback, throwable);
+            reportFailure(callback, throwable);
         }
     }
 
@@ -107,7 +107,7 @@
                     };
             listenableCallback.dispatchCallbackSafely();
         } catch (Throwable throwable) {
-            failureCallback(callback, throwable);
+            reportFailure(callback, throwable);
         }
     }
 
@@ -127,7 +127,7 @@
                     };
             listenableCallback.dispatchCallbackSafely();
         } catch (Throwable throwable) {
-            failureCallback(callback, throwable);
+            reportFailure(callback, throwable);
         }
     }
 
@@ -149,7 +149,7 @@
                     };
             listenableCallback.dispatchCallbackSafely();
         } catch (Throwable throwable) {
-            failureCallback(callback, throwable);
+            reportFailure(callback, throwable);
         }
     }
 
@@ -171,7 +171,7 @@
                     };
             listenableCallback.dispatchCallbackSafely();
         } catch (Throwable throwable) {
-            failureCallback(callback, throwable);
+            reportFailure(callback, throwable);
         }
     }
 
@@ -191,7 +191,7 @@
                     };
             listenableCallback.dispatchCallbackSafely();
         } catch (Throwable throwable) {
-            failureCallback(callback, throwable);
+            reportFailure(callback, throwable);
         }
     }
 
@@ -214,7 +214,7 @@
                     };
             listenableCallback.dispatchCallbackSafely();
         } catch (Throwable throwable) {
-            failureCallback(callback, throwable);
+            reportFailure(callback, throwable);
         }
     }
 }
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java
new file mode 100644
index 0000000..4fbf6a6
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.work.Logger;
+
+/**
+ * The {@link Service} which hosts an implementation of a {@link androidx.work.ListenableWorker}.
+ */
+public class RemoteWorkerService extends Service {
+    static final String TAG = Logger.tagWithPrefix("RemoteWorkerService");
+    private IBinder mBinder;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mBinder = new ListenableWorkerImpl(this);
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(@NonNull Intent intent) {
+        Logger.get().info(TAG, "Binding to RemoteWorkerService");
+        return mBinder;
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableData.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableData.java
new file mode 100644
index 0000000..46566ba
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableData.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess.parcelable;
+
+import static androidx.work.Data.convertPrimitiveBooleanArray;
+import static androidx.work.Data.convertPrimitiveByteArray;
+import static androidx.work.Data.convertPrimitiveDoubleArray;
+import static androidx.work.Data.convertPrimitiveFloatArray;
+import static androidx.work.Data.convertPrimitiveIntArray;
+import static androidx.work.Data.convertPrimitiveLongArray;
+import static androidx.work.Data.convertToPrimitiveArray;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.readBooleanValue;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.writeBooleanValue;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.work.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link androidx.work.Data} but {@link android.os.Parcelable}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableData implements Parcelable {
+
+    // The list of supported types.
+    private static final byte TYPE_NULL = 0;
+    private static final byte TYPE_BOOLEAN = 1;
+    private static final byte TYPE_BYTE = 2;
+    private static final byte TYPE_INTEGER = 3;
+    private static final byte TYPE_LONG = 4;
+    private static final byte TYPE_FLOAT = 5;
+    private static final byte TYPE_DOUBLE = 6;
+    private static final byte TYPE_STRING = 7;
+    private static final byte TYPE_BOOLEAN_ARRAY = 8;
+    private static final byte TYPE_BYTE_ARRAY = 9;
+    private static final byte TYPE_INTEGER_ARRAY = 10;
+    private static final byte TYPE_LONG_ARRAY = 11;
+    private static final byte TYPE_FLOAT_ARRAY = 12;
+    private static final byte TYPE_DOUBLE_ARRAY = 13;
+    private static final byte TYPE_STRING_ARRAY = 14;
+
+    private final Data mData;
+
+    public ParcelableData(@NonNull Data data) {
+        mData = data;
+    }
+
+    protected ParcelableData(@NonNull Parcel in) {
+        Map<String, Object> values = new HashMap<>();
+        // size
+        int size = in.readInt();
+        for (int i = 0; i < size; i++) {
+            // entries
+            addEntry(in, values);
+        }
+        mData = new Data(values);
+    }
+
+    public static final Creator<ParcelableData> CREATOR =
+            new Creator<ParcelableData>() {
+                @Override
+                public ParcelableData createFromParcel(@NonNull Parcel in) {
+                    return new ParcelableData(in);
+                }
+
+                @Override
+                public ParcelableData[] newArray(int size) {
+                    return new ParcelableData[size];
+                }
+            };
+
+    @NonNull
+    public Data getData() {
+        return mData;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        Map<String, Object> keyValueMap = mData.getKeyValueMap();
+        // size
+        parcel.writeInt(keyValueMap.size());
+        for (Map.Entry<String, Object> entry : keyValueMap.entrySet()) {
+            writeToParcel(parcel, entry.getKey(), entry.getValue());
+        }
+    }
+
+    private void writeToParcel(
+            @NonNull Parcel parcel,
+            @NonNull String key,
+            @Nullable Object value) {
+
+        // type + value
+        if (value == null) {
+            parcel.writeByte(TYPE_NULL);
+        } else {
+            Class<?> valueType = value.getClass();
+            if (valueType == Boolean.class) {
+                parcel.writeByte(TYPE_BOOLEAN);
+                boolean booleanValue = (Boolean) value;
+                writeBooleanValue(parcel, booleanValue);
+            } else if (valueType == Byte.class) {
+                parcel.writeByte(TYPE_BYTE);
+                byte byteValue = (Byte) value;
+                parcel.writeByte(byteValue);
+            } else if (valueType == Integer.class) {
+                parcel.writeByte(TYPE_INTEGER);
+                int intValue = (Integer) value;
+                parcel.writeInt(intValue);
+            } else if (valueType == Long.class) {
+                parcel.writeByte(TYPE_LONG);
+                Long longValue = (Long) value;
+                parcel.writeLong(longValue);
+            } else if (valueType == Float.class) {
+                parcel.writeByte(TYPE_FLOAT);
+                float floatValue = (Float) value;
+                parcel.writeFloat(floatValue);
+            } else if (valueType == Double.class) {
+                parcel.writeByte(TYPE_DOUBLE);
+                double doubleValue = (Double) value;
+                parcel.writeDouble(doubleValue);
+            } else if (valueType == String.class) {
+                parcel.writeByte(TYPE_STRING);
+                String stringValue = (String) value;
+                parcel.writeString(stringValue);
+            } else if (valueType == Boolean[].class) {
+                parcel.writeByte(TYPE_BOOLEAN_ARRAY);
+                Boolean[] booleanArray = (Boolean[]) value;
+                parcel.writeBooleanArray(convertToPrimitiveArray(booleanArray));
+            } else if (valueType == Byte[].class) {
+                parcel.writeByte(TYPE_BYTE_ARRAY);
+                Byte[] byteArray = (Byte[]) value;
+                parcel.writeByteArray(convertToPrimitiveArray(byteArray));
+            } else if (valueType == Integer[].class) {
+                parcel.writeByte(TYPE_INTEGER_ARRAY);
+                Integer[] integerArray = (Integer[]) value;
+                parcel.writeIntArray(convertToPrimitiveArray(integerArray));
+            } else if (valueType == Long[].class) {
+                parcel.writeByte(TYPE_LONG_ARRAY);
+                Long[] longArray = (Long[]) value;
+                parcel.writeLongArray(convertToPrimitiveArray(longArray));
+            } else if (valueType == Float[].class) {
+                parcel.writeByte(TYPE_FLOAT_ARRAY);
+                Float[] floatArray = (Float[]) value;
+                parcel.writeFloatArray(convertToPrimitiveArray(floatArray));
+            } else if (valueType == Double[].class) {
+                parcel.writeByte(TYPE_DOUBLE_ARRAY);
+                Double[] doubleArray = (Double[]) value;
+                parcel.writeDoubleArray(convertToPrimitiveArray(doubleArray));
+            } else if (valueType == String[].class) {
+                parcel.writeByte(TYPE_STRING_ARRAY);
+                String[] stringArray = (String[]) value;
+                parcel.writeStringArray(stringArray);
+            } else {
+                // Exhaustive check
+                String message = String.format("Unsupported value type %s", valueType.getName());
+                throw new IllegalArgumentException(message);
+            }
+        }
+        // key
+        parcel.writeString(key);
+    }
+
+    private void addEntry(@NonNull Parcel parcel, @NonNull Map<String, Object> values) {
+        // type
+        int type = parcel.readByte();
+        Object value = null;
+        switch (type) {
+            case TYPE_NULL:
+                break;
+            case TYPE_BYTE:
+                value = parcel.readByte();
+                break;
+            case TYPE_BOOLEAN:
+                value = readBooleanValue(parcel);
+                break;
+            case TYPE_INTEGER:
+                value = parcel.readInt();
+                break;
+            case TYPE_LONG:
+                value = parcel.readLong();
+                break;
+            case TYPE_FLOAT:
+                value = parcel.readFloat();
+                break;
+            case TYPE_DOUBLE:
+                value = parcel.readDouble();
+                break;
+            case TYPE_STRING:
+                value = parcel.readString();
+                break;
+            case TYPE_BOOLEAN_ARRAY:
+                value = convertPrimitiveBooleanArray(parcel.createBooleanArray());
+                break;
+            case TYPE_BYTE_ARRAY:
+                value = convertPrimitiveByteArray(parcel.createByteArray());
+                break;
+            case TYPE_INTEGER_ARRAY:
+                value = convertPrimitiveIntArray(parcel.createIntArray());
+                break;
+            case TYPE_LONG_ARRAY:
+                value = convertPrimitiveLongArray(parcel.createLongArray());
+                break;
+            case TYPE_FLOAT_ARRAY:
+                value = convertPrimitiveFloatArray(parcel.createFloatArray());
+                break;
+            case TYPE_DOUBLE_ARRAY:
+                value = convertPrimitiveDoubleArray(parcel.createDoubleArray());
+                break;
+            case TYPE_STRING_ARRAY:
+                value = parcel.createStringArray();
+                break;
+            default:
+                String message = String.format("Unsupported type %s", type);
+                throw new IllegalStateException(message);
+        }
+        String key = parcel.readString();
+        values.put(key, value);
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java
new file mode 100644
index 0000000..d210690
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess.parcelable;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.WorkerParameters;
+
+/**
+ * Everything you need to run a {@link androidx.work.multiprocess.RemoteListenableWorker}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableRemoteWorkRequest implements Parcelable {
+    // We are holding on to parcelables here instead of the actual deserialized representation.
+    // This is because, to create an instance of WorkerParameters we need the application context
+    // using which we can determine the configuration, taskExecutor to use etc.
+
+    private final String mWorkerClassName;
+    private final ParcelableWorkerParameters mParcelableWorkerParameters;
+
+    public ParcelableRemoteWorkRequest(
+            @NonNull String workerClassName,
+            @NonNull WorkerParameters workerParameters) {
+
+        mWorkerClassName = workerClassName;
+        mParcelableWorkerParameters = new ParcelableWorkerParameters(workerParameters);
+    }
+
+    protected ParcelableRemoteWorkRequest(@NonNull Parcel in) {
+        // workerClassName
+        mWorkerClassName = in.readString();
+        // parcelableWorkerParameters
+        mParcelableWorkerParameters = new ParcelableWorkerParameters(in);
+    }
+
+    public static final Creator<ParcelableRemoteWorkRequest> CREATOR =
+            new Creator<ParcelableRemoteWorkRequest>() {
+                @Override
+                public ParcelableRemoteWorkRequest createFromParcel(Parcel in) {
+                    return new ParcelableRemoteWorkRequest(in);
+                }
+
+                @Override
+                public ParcelableRemoteWorkRequest[] newArray(int size) {
+                    return new ParcelableRemoteWorkRequest[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        // workerClassName
+        parcel.writeString(mWorkerClassName);
+        // parcelableWorkerParameters
+        mParcelableWorkerParameters.writeToParcel(parcel, flags);
+    }
+
+    @NonNull
+    public String getWorkerClassName() {
+        return mWorkerClassName;
+    }
+
+    @NonNull
+    public ParcelableWorkerParameters getParcelableWorkerParameters() {
+        return mParcelableWorkerParameters;
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java
new file mode 100644
index 0000000..d24b96e
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess.parcelable;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.Data;
+import androidx.work.ListenableWorker;
+
+/**
+ * {@link androidx.work.ListenableWorker.Result}, but parcelable.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableResult implements Parcelable {
+    private final ListenableWorker.Result mResult;
+
+    public ParcelableResult(@NonNull ListenableWorker.Result result) {
+        mResult = result;
+    }
+
+    public ParcelableResult(@NonNull Parcel in) {
+        // resultType
+        int resultType = in.readInt();
+        // outputData
+        ParcelableData parcelableOutputData = new ParcelableData(in);
+        mResult = intToResultType(resultType, parcelableOutputData.getData());
+    }
+
+    public static final Creator<ParcelableResult> CREATOR =
+            new Creator<ParcelableResult>() {
+                @Override
+                @NonNull
+                public ParcelableResult createFromParcel(Parcel in) {
+                    return new ParcelableResult(in);
+                }
+
+                @Override
+                public ParcelableResult[] newArray(int size) {
+                    return new ParcelableResult[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        int resultType = resultTypeOf(mResult);
+        // resultType
+        parcel.writeInt(resultType);
+        // outputData
+        Data outputData = mResult.getOutputData();
+        ParcelableData parcelableOutputData = new ParcelableData(outputData);
+        parcelableOutputData.writeToParcel(parcel, flags);
+    }
+
+    @NonNull
+    public ListenableWorker.Result getResult() {
+        return mResult;
+    }
+
+    private static int resultTypeOf(ListenableWorker.Result result) {
+        if (result instanceof ListenableWorker.Result.Retry) {
+            return 1;
+        } else if (result instanceof ListenableWorker.Result.Success) {
+            return 2;
+        } else if (result instanceof ListenableWorker.Result.Failure) {
+            return 3;
+        } else {
+            // Exhaustive check
+            throw new IllegalStateException(String.format("Unknown Result %s", result));
+        }
+    }
+
+    @NonNull
+    private static ListenableWorker.Result intToResultType(int resultType, @NonNull Data data) {
+        ListenableWorker.Result result = null;
+        if (resultType == 1) {
+            result = ListenableWorker.Result.retry();
+        } else if (resultType == 2) {
+            result = ListenableWorker.Result.success(data);
+        } else if (resultType == 3) {
+            result = ListenableWorker.Result.failure(data);
+        } else {
+            // Exhaustive check
+            throw new IllegalStateException(String.format("Unknown result type %s", resultType));
+        }
+        return result;
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java
new file mode 100644
index 0000000..f750cd2
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess.parcelable;
+
+import static androidx.work.multiprocess.parcelable.ParcelUtils.readBooleanValue;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.writeBooleanValue;
+
+import android.annotation.SuppressLint;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.WorkerParameters;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link androidx.work.WorkerParameters.RuntimeExtras}, but parcelable.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableRuntimeExtras implements Parcelable {
+    private WorkerParameters.RuntimeExtras mRuntimeExtras;
+
+    public ParcelableRuntimeExtras(@NonNull WorkerParameters.RuntimeExtras runtimeExtras) {
+        mRuntimeExtras = runtimeExtras;
+    }
+
+    public ParcelableRuntimeExtras(@NonNull Parcel in) {
+        ClassLoader loader = getClass().getClassLoader();
+        // network
+        Network network = null;
+        boolean hasNetwork = readBooleanValue(in);
+        if (hasNetwork) {
+            network = in.readParcelable(loader);
+        }
+        // triggeredContentUris
+        List<Uri> triggeredContentUris = null;
+        boolean hasContentUris = readBooleanValue(in);
+        if (hasContentUris) {
+            Parcelable[] parceledUris = in.readParcelableArray(loader);
+            triggeredContentUris = new ArrayList<>(parceledUris.length);
+            for (Parcelable parcelable : parceledUris) {
+                triggeredContentUris.add((Uri) parcelable);
+            }
+        }
+        // triggeredContentAuthorities
+        List<String> triggeredContentAuthorities = null;
+        boolean hasContentAuthorities = readBooleanValue(in);
+        if (hasContentAuthorities) {
+            triggeredContentAuthorities = in.createStringArrayList();
+        }
+        mRuntimeExtras = new WorkerParameters.RuntimeExtras();
+        if (Build.VERSION.SDK_INT >= 28) {
+            mRuntimeExtras.network = network;
+        }
+        if (Build.VERSION.SDK_INT >= 24) {
+            if (triggeredContentUris != null) {
+                mRuntimeExtras.triggeredContentUris = triggeredContentUris;
+            }
+            if (triggeredContentAuthorities != null) {
+                mRuntimeExtras.triggeredContentAuthorities = triggeredContentAuthorities;
+            }
+        }
+    }
+
+    public static final Creator<ParcelableRuntimeExtras> CREATOR =
+            new Creator<ParcelableRuntimeExtras>() {
+                @Override
+                @NonNull
+                public ParcelableRuntimeExtras createFromParcel(Parcel in) {
+                    return new ParcelableRuntimeExtras(in);
+                }
+
+                @Override
+                public ParcelableRuntimeExtras[] newArray(int size) {
+                    return new ParcelableRuntimeExtras[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    @SuppressLint("NewApi")
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        Network network = null;
+        if (Build.VERSION.SDK_INT >= 28) {
+            network = mRuntimeExtras.network;
+        }
+        // network
+        boolean hasNetwork = network != null;
+        writeBooleanValue(parcel, hasNetwork);
+        if (hasNetwork) {
+            parcel.writeParcelable(network, flags);
+        }
+
+        List<Uri> triggeredContentUris = null;
+        List<String> triggeredAuthorities = null;
+        if (Build.VERSION.SDK_INT >= 24) {
+            triggeredContentUris = mRuntimeExtras.triggeredContentUris;
+            triggeredAuthorities = mRuntimeExtras.triggeredContentAuthorities;
+        }
+        // triggeredContentUris
+        boolean hasContentUris = triggeredContentUris != null && !triggeredContentUris.isEmpty();
+        writeBooleanValue(parcel, hasContentUris);
+        if (hasContentUris) {
+            Uri[] contentUriArray = new Uri[triggeredContentUris.size()];
+            for (int i = 0; i < contentUriArray.length; i++) {
+                contentUriArray[i] = triggeredContentUris.get(i);
+            }
+            parcel.writeParcelableArray(contentUriArray, flags);
+        }
+        // triggeredContentAuthorities
+        boolean hasContentAuthorities =
+                triggeredAuthorities != null && !triggeredAuthorities.isEmpty();
+        writeBooleanValue(parcel, hasContentAuthorities);
+        if (hasContentAuthorities) {
+            parcel.writeStringList(triggeredAuthorities);
+        }
+    }
+
+    @NonNull
+    public WorkerParameters.RuntimeExtras getRuntimeExtras() {
+        return mRuntimeExtras;
+    }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
index ee15125..a37f194 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
@@ -56,12 +56,14 @@
         // state
         WorkInfo.State state = intToState(parcel.readInt());
         // outputData
-        Data output = Data.fromByteArray(parcel.createByteArray());
+        ParcelableData parcelableOutputData = new ParcelableData(parcel);
+        Data output = parcelableOutputData.getData();
         // tags
         String[] tagsArray = parcel.createStringArray();
         List<String> tags = Arrays.asList(tagsArray);
         // progress
-        Data progress = Data.fromByteArray(parcel.createByteArray());
+        ParcelableData parcelableProgressData = new ParcelableData(parcel);
+        Data progress = parcelableProgressData.getData();
         // runAttemptCount
         int runAttemptCount = parcel.readInt();
         mWorkInfo = new WorkInfo(id, state, output, tags, progress, runAttemptCount);
@@ -97,15 +99,15 @@
         // state
         parcel.writeInt(stateToInt(mWorkInfo.getState()));
         // outputData
-        byte[] outputData = mWorkInfo.getOutputData().toByteArray();
-        parcel.writeByteArray(outputData);
+        ParcelableData parcelableOutputData = new ParcelableData(mWorkInfo.getOutputData());
+        parcelableOutputData.writeToParcel(parcel, flags);
         // tags
         // Note: converting to a String[] because that is faster.
         List<String> tags = new ArrayList<>(mWorkInfo.getTags());
         parcel.writeStringArray(tags.toArray(sEMPTY));
         // progress
-        byte[] progress = mWorkInfo.getProgress().toByteArray();
-        parcel.writeByteArray(progress);
+        ParcelableData parcelableProgress = new ParcelableData(mWorkInfo.getProgress());
+        parcelableProgress.writeToParcel(parcel, flags);
         // runAttemptCount
         parcel.writeInt(mWorkInfo.getRunAttemptCount());
     }
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
index dd0da14..e784b12 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
@@ -27,7 +27,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.work.Data;
 import androidx.work.WorkRequest;
 import androidx.work.impl.WorkRequestHolder;
 import androidx.work.impl.model.WorkSpec;
@@ -65,9 +64,11 @@
         // state
         workSpec.state = intToState(in.readInt());
         // inputData
-        workSpec.input = Data.fromByteArray(in.createByteArray());
+        ParcelableData parcelableInputData = new ParcelableData(in);
+        workSpec.input = parcelableInputData.getData();
         // outputData
-        workSpec.output = Data.fromByteArray(in.createByteArray());
+        ParcelableData parcelableOutputData = new ParcelableData(in);
+        workSpec.output = parcelableOutputData.getData();
         // initialDelay
         workSpec.initialDelay = in.readLong();
         // intervalDuration
@@ -125,11 +126,11 @@
         // state
         parcel.writeInt(stateToInt(workSpec.state));
         // inputData
-        byte[] inputData = workSpec.input.toByteArray();
-        parcel.writeByteArray(inputData);
+        ParcelableData parcelableInputData = new ParcelableData(workSpec.input);
+        parcelableInputData.writeToParcel(parcel, flags);
         // outputData
-        byte[] outputData = workSpec.output.toByteArray();
-        parcel.writeByteArray(outputData);
+        ParcelableData parcelableOutputData = new ParcelableData(workSpec.output);
+        parcelableOutputData.writeToParcel(parcel, flags);
         // initialDelay
         parcel.writeLong(workSpec.initialDelay);
         // intervalDuration
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
new file mode 100644
index 0000000..9b8f0f5
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess.parcelable;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.Configuration;
+import androidx.work.Data;
+import androidx.work.WorkerParameters;
+import androidx.work.impl.WorkDatabase;
+import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.foreground.ForegroundProcessor;
+import androidx.work.impl.utils.WorkForegroundUpdater;
+import androidx.work.impl.utils.WorkProgressUpdater;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * {@link androidx.work.WorkerParameters}, but parcelable.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableWorkerParameters implements Parcelable {
+    @NonNull
+    private final UUID mId;
+    @NonNull
+    private final Data mData;
+    @NonNull
+    private final Set<String> mTags;
+    @NonNull
+    private final WorkerParameters.RuntimeExtras mRuntimeExtras;
+    private final int mRunAttemptCount;
+
+    public ParcelableWorkerParameters(@NonNull WorkerParameters parameters) {
+        mId = parameters.getId();
+        mData = parameters.getInputData();
+        mTags = parameters.getTags();
+        mRuntimeExtras = parameters.getRuntimeExtras();
+        mRunAttemptCount = parameters.getRunAttemptCount();
+    }
+
+    public static final Creator<ParcelableWorkerParameters> CREATOR =
+            new Creator<ParcelableWorkerParameters>() {
+                @Override
+                @NonNull
+                public ParcelableWorkerParameters createFromParcel(Parcel in) {
+                    return new ParcelableWorkerParameters(in);
+                }
+
+                @Override
+                public ParcelableWorkerParameters[] newArray(int size) {
+                    return new ParcelableWorkerParameters[size];
+                }
+            };
+
+    public ParcelableWorkerParameters(@NonNull Parcel in) {
+        // id
+        String id = in.readString();
+        mId = UUID.fromString(id);
+        // inputData
+        ParcelableData parcelableInputData = new ParcelableData(in);
+        mData = parcelableInputData.getData();
+        // tags
+        mTags = new HashSet<>(in.createStringArrayList());
+        // runtimeExtras
+        ParcelableRuntimeExtras parcelableRuntimeExtras = new ParcelableRuntimeExtras(in);
+        mRuntimeExtras = parcelableRuntimeExtras.getRuntimeExtras();
+        // runAttemptCount
+        mRunAttemptCount = in.readInt();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        // id
+        parcel.writeString(mId.toString());
+        // inputData
+        ParcelableData parcelableInputData = new ParcelableData(mData);
+        parcelableInputData.writeToParcel(parcel, flags);
+        // tags
+        List<String> tags = new ArrayList<>(mTags);
+        parcel.writeStringList(tags);
+        // runtimeExtras
+        ParcelableRuntimeExtras parcelableRuntimeExtras =
+                new ParcelableRuntimeExtras(mRuntimeExtras);
+        parcelableRuntimeExtras.writeToParcel(parcel, flags);
+        // runAttemptCount
+        parcel.writeInt(mRunAttemptCount);
+    }
+
+    @NonNull
+    public UUID getId() {
+        return mId;
+    }
+
+    @NonNull
+    public Data getData() {
+        return mData;
+    }
+
+    public int getRunAttemptCount() {
+        return mRunAttemptCount;
+    }
+
+    @NonNull
+    public Set<String> getTags() {
+        return mTags;
+    }
+
+    /**
+     * Converts {@link ParcelableWorkerParameters} to an instance of {@link WorkerParameters}
+     * lazily.
+     */
+    @NonNull
+    public WorkerParameters toWorkerParameters(@NonNull WorkManagerImpl workManager) {
+        Configuration configuration = workManager.getConfiguration();
+        WorkDatabase workDatabase = workManager.getWorkDatabase();
+        TaskExecutor taskExecutor = workManager.getWorkTaskExecutor();
+        ForegroundProcessor foregroundProcessor = workManager.getProcessor();
+        return new WorkerParameters(
+                mId,
+                mData,
+                mTags,
+                mRuntimeExtras,
+                mRunAttemptCount,
+                configuration.getExecutor(),
+                taskExecutor,
+                configuration.getWorkerFactory(),
+                new WorkProgressUpdater(workDatabase, taskExecutor),
+                new WorkForegroundUpdater(workDatabase, foregroundProcessor, taskExecutor)
+        );
+    }
+}
diff --git a/work/workmanager/api/current.txt b/work/workmanager/api/current.txt
index 2a4986c..1151de1 100644
--- a/work/workmanager/api/current.txt
+++ b/work/workmanager/api/current.txt
@@ -182,6 +182,7 @@
     enum_constant public static final androidx.work.NetworkType METERED;
     enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
     enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
     enum_constant public static final androidx.work.NetworkType UNMETERED;
   }
 
@@ -300,6 +301,12 @@
     method public abstract androidx.work.Operation pruneWork();
   }
 
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
   public final class WorkQuery {
     method public java.util.List<java.util.UUID!> getIds();
     method public java.util.List<androidx.work.WorkInfo.State!> getStates();
diff --git a/work/workmanager/api/public_plus_experimental_current.txt b/work/workmanager/api/public_plus_experimental_current.txt
index 2a4986c..1151de1 100644
--- a/work/workmanager/api/public_plus_experimental_current.txt
+++ b/work/workmanager/api/public_plus_experimental_current.txt
@@ -182,6 +182,7 @@
     enum_constant public static final androidx.work.NetworkType METERED;
     enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
     enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
     enum_constant public static final androidx.work.NetworkType UNMETERED;
   }
 
@@ -300,6 +301,12 @@
     method public abstract androidx.work.Operation pruneWork();
   }
 
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
   public final class WorkQuery {
     method public java.util.List<java.util.UUID!> getIds();
     method public java.util.List<androidx.work.WorkInfo.State!> getStates();
diff --git a/work/workmanager/api/restricted_current.txt b/work/workmanager/api/restricted_current.txt
index 2a4986c..1151de1 100644
--- a/work/workmanager/api/restricted_current.txt
+++ b/work/workmanager/api/restricted_current.txt
@@ -182,6 +182,7 @@
     enum_constant public static final androidx.work.NetworkType METERED;
     enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
     enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
     enum_constant public static final androidx.work.NetworkType UNMETERED;
   }
 
@@ -300,6 +301,12 @@
     method public abstract androidx.work.Operation pruneWork();
   }
 
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
   public final class WorkQuery {
     method public java.util.List<java.util.UUID!> getIds();
     method public java.util.List<androidx.work.WorkInfo.State!> getStates();
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index a36e628..32786ae 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -16,6 +16,8 @@
 
 package androidx.work.impl.background.systemjob;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+
 import static androidx.work.NetworkType.CONNECTED;
 import static androidx.work.NetworkType.METERED;
 import static androidx.work.NetworkType.NOT_REQUIRED;
@@ -26,8 +28,10 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.arrayContaining;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.junit.Assert.assertTrue;
 
 import android.app.job.JobInfo;
+import android.net.NetworkRequest;
 import android.net.Uri;
 import android.os.Build;
 
@@ -303,6 +307,18 @@
                 is(JobInfo.NETWORK_TYPE_METERED));
     }
 
+    @Test
+    @SmallTest
+    @SdkSuppress(minSdkVersion = 30)
+    public void testConvertNetworkType_temporarilyMetered() {
+        WorkSpec workSpec = getTestWorkSpecWithConstraints(new Constraints.Builder()
+                .setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
+                .build());
+        JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
+        NetworkRequest networkRequest = jobInfo.getRequiredNetwork();
+        assertTrue(networkRequest.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+    }
+
     private WorkSpec getTestWorkSpecWithConstraints(Constraints constraints) {
         return getWorkSpec(new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setConstraints(constraints)
diff --git a/work/workmanager/src/main/AndroidManifest.xml b/work/workmanager/src/main/AndroidManifest.xml
index 08d71dc..c772c5e 100644
--- a/work/workmanager/src/main/AndroidManifest.xml
+++ b/work/workmanager/src/main/AndroidManifest.xml
@@ -29,7 +29,7 @@
             android:authorities="${applicationId}.androidx-startup"
             android:exported="false"
             tools:node="merge">
-            <meta-data  android:name="androidx.work.impl.WorkManagerInitializer"
+            <meta-data  android:name="androidx.work.WorkManagerInitializer"
                 android:value="androidx.startup" />
         </provider>
         <service
diff --git a/work/workmanager/src/main/aidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl b/work/workmanager/src/main/aidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl
new file mode 100644
index 0000000..9ff1a3a
--- /dev/null
+++ b/work/workmanager/src/main/aidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess;
+
+import androidx.work.multiprocess.IWorkManagerImplCallback;
+
+/**
+ * Implementation for a multi-process {@link ListenableWorker}.
+ *
+ * @hide
+ */
+oneway interface IListenableWorkerImpl {
+   // request is a ParcelablelRemoteRequest instance.
+   // callback gets a parcelized representation of Result
+   oneway void startWork(in byte[] request, IWorkManagerImplCallback callback);
+
+   // interrupt request.
+   // request is a ParcelableWorkerParameters instance.
+   // callback gets an empty result
+   oneway void interrupt(in byte[] request, IWorkManagerImplCallback callback);
+}
diff --git a/work/workmanager/src/main/java/androidx/work/Configuration.java b/work/workmanager/src/main/java/androidx/work/Configuration.java
index 6ddf8d2..c69547c 100644
--- a/work/workmanager/src/main/java/androidx/work/Configuration.java
+++ b/work/workmanager/src/main/java/androidx/work/Configuration.java
@@ -497,7 +497,7 @@
      * A class that can provide the {@link Configuration} for WorkManager and allow for on-demand
      * initialization of WorkManager.  To do this:
      * <p><ul>
-     *   <li>Disable {@code androidx.work.impl.WorkManagerInitializer} in your manifest</li>
+     *   <li>Disable {@code androidx.work.WorkManagerInitializer} in your manifest</li>
      *   <li>Implement the {@link Configuration.Provider} interface on your
      *   {@link android.app.Application} class</li>
      *   <li>Use {@link WorkManager#getInstance(Context)} when accessing WorkManger (NOT
diff --git a/work/workmanager/src/main/java/androidx/work/Data.java b/work/workmanager/src/main/java/androidx/work/Data.java
index 156240e..578e83e 100644
--- a/work/workmanager/src/main/java/androidx/work/Data.java
+++ b/work/workmanager/src/main/java/androidx/work/Data.java
@@ -64,7 +64,7 @@
     public static final int MAX_DATA_BYTES = 10 * 1024;    // 10KB
 
     @SuppressWarnings("WeakerAccess") /* synthetic access */
-    Map<String, Object> mValues;
+            Map<String, Object> mValues;
 
     Data() {    // stub required for room
     }
@@ -73,14 +73,18 @@
         mValues = new HashMap<>(other.mValues);
     }
 
-    Data(@NonNull Map<String, ?> values) {
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public Data(@NonNull Map<String, ?> values) {
         mValues = new HashMap<>(values);
     }
 
     /**
      * Gets the boolean value for the given key.
      *
-     * @param key The key for the argument
+     * @param key          The key for the argument
      * @param defaultValue The default value to return if the key is not found
      * @return The value specified by the key if it exists; the default value otherwise
      */
@@ -99,15 +103,12 @@
      * @param key The key for the argument
      * @return The value specified by the key if it exists; {@code null} otherwise
      */
-    public @Nullable boolean[] getBooleanArray(@NonNull String key) {
+    @Nullable
+    public boolean[] getBooleanArray(@NonNull String key) {
         Object value = mValues.get(key);
         if (value instanceof Boolean[]) {
             Boolean[] array = (Boolean[]) value;
-            boolean[] returnArray = new boolean[array.length];
-            for (int i = 0; i < array.length; ++i) {
-                returnArray[i] = array[i];
-            }
-            return returnArray;
+            return convertToPrimitiveArray(array);
         } else {
             return null;
         }
@@ -116,7 +117,7 @@
     /**
      * Gets the byte value for the given key.
      *
-     * @param key The key for the argument
+     * @param key          The key for the argument
      * @param defaultValue The default value to return if the key is not found
      * @return The value specified by the key if it exists; the default value otherwise
      */
@@ -135,15 +136,12 @@
      * @param key The key for the argument
      * @return The value specified by the key if it exists; {@code null} otherwise
      */
-    public @Nullable byte[] getByteArray(@NonNull String key) {
+    @Nullable
+    public byte[] getByteArray(@NonNull String key) {
         Object value = mValues.get(key);
         if (value instanceof Byte[]) {
             Byte[] array = (Byte[]) value;
-            byte[] returnArray = new byte[array.length];
-            for (int i = 0; i < array.length; ++i) {
-                returnArray[i] = array[i];
-            }
-            return returnArray;
+            return convertToPrimitiveArray(array);
         } else {
             return null;
         }
@@ -152,7 +150,7 @@
     /**
      * Gets the integer value for the given key.
      *
-     * @param key The key for the argument
+     * @param key          The key for the argument
      * @param defaultValue The default value to return if the key is not found
      * @return The value specified by the key if it exists; the default value otherwise
      */
@@ -171,15 +169,12 @@
      * @param key The key for the argument
      * @return The value specified by the key if it exists; {@code null} otherwise
      */
-    public @Nullable int[] getIntArray(@NonNull String key) {
+    @Nullable
+    public int[] getIntArray(@NonNull String key) {
         Object value = mValues.get(key);
         if (value instanceof Integer[]) {
             Integer[] array = (Integer[]) value;
-            int[] returnArray = new int[array.length];
-            for (int i = 0; i < array.length; ++i) {
-                returnArray[i] = array[i];
-            }
-            return returnArray;
+            return convertToPrimitiveArray(array);
         } else {
             return null;
         }
@@ -188,7 +183,7 @@
     /**
      * Gets the long value for the given key.
      *
-     * @param key The key for the argument
+     * @param key          The key for the argument
      * @param defaultValue The default value to return if the key is not found
      * @return The value specified by the key if it exists; the default value otherwise
      */
@@ -207,15 +202,12 @@
      * @param key The key for the argument
      * @return The value specified by the key if it exists; {@code null} otherwise
      */
-    public @Nullable long[] getLongArray(@NonNull String key) {
+    @Nullable
+    public long[] getLongArray(@NonNull String key) {
         Object value = mValues.get(key);
         if (value instanceof Long[]) {
             Long[] array = (Long[]) value;
-            long[] returnArray = new long[array.length];
-            for (int i = 0; i < array.length; ++i) {
-                returnArray[i] = array[i];
-            }
-            return returnArray;
+            return convertToPrimitiveArray(array);
         } else {
             return null;
         }
@@ -224,7 +216,7 @@
     /**
      * Gets the float value for the given key.
      *
-     * @param key The key for the argument
+     * @param key          The key for the argument
      * @param defaultValue The default value to return if the key is not found
      * @return The value specified by the key if it exists; the default value otherwise
      */
@@ -243,15 +235,12 @@
      * @param key The key for the argument
      * @return The value specified by the key if it exists; {@code null} otherwise
      */
-    public @Nullable float[] getFloatArray(@NonNull String key) {
+    @Nullable
+    public float[] getFloatArray(@NonNull String key) {
         Object value = mValues.get(key);
         if (value instanceof Float[]) {
             Float[] array = (Float[]) value;
-            float[] returnArray = new float[array.length];
-            for (int i = 0; i < array.length; ++i) {
-                returnArray[i] = array[i];
-            }
-            return returnArray;
+            return convertToPrimitiveArray(array);
         } else {
             return null;
         }
@@ -260,7 +249,7 @@
     /**
      * Gets the double value for the given key.
      *
-     * @param key The key for the argument
+     * @param key          The key for the argument
      * @param defaultValue The default value to return if the key is not found
      * @return The value specified by the key if it exists; the default value otherwise
      */
@@ -279,15 +268,12 @@
      * @param key The key for the argument
      * @return The value specified by the key if it exists; {@code null} otherwise
      */
-    public @Nullable double[] getDoubleArray(@NonNull String key) {
+    @Nullable
+    public double[] getDoubleArray(@NonNull String key) {
         Object value = mValues.get(key);
         if (value instanceof Double[]) {
             Double[] array = (Double[]) value;
-            double[] returnArray = new double[array.length];
-            for (int i = 0; i < array.length; ++i) {
-                returnArray[i] = array[i];
-            }
-            return returnArray;
+            return convertToPrimitiveArray(array);
         } else {
             return null;
         }
@@ -299,7 +285,8 @@
      * @param key The key for the argument
      * @return The value specified by the key if it exists; the default value otherwise
      */
-    public @Nullable String getString(@NonNull String key) {
+    @Nullable
+    public String getString(@NonNull String key) {
         Object value = mValues.get(key);
         if (value instanceof String) {
             return (String) value;
@@ -314,7 +301,8 @@
      * @param key The key for the argument
      * @return The value specified by the key if it exists; {@code null} otherwise
      */
-    public @Nullable String[] getStringArray(@NonNull String key) {
+    @Nullable
+    public String[] getStringArray(@NonNull String key) {
         Object value = mValues.get(key);
         if (value instanceof String[]) {
             return (String[]) value;
@@ -329,7 +317,8 @@
      * @return A {@link Map} of key-value pairs for this object; this Map is unmodifiable and should
      * be used for reads only.
      */
-    public @NonNull Map<String, Object> getKeyValueMap() {
+    @NonNull
+    public Map<String, Object> getKeyValueMap() {
         return Collections.unmodifiableMap(mValues);
     }
 
@@ -347,7 +336,7 @@
         return Data.toByteArrayInternal(this);
     }
 
-     /**
+    /**
      * Returns {@code true} if the instance of {@link Data} has a non-null value corresponding to
      * the given {@link String} key with the expected type of {@code T}.
      *
@@ -383,7 +372,8 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @TypeConverter
-    public static @NonNull byte[] toByteArrayInternal(@NonNull Data data) {
+    @NonNull
+    public static byte[] toByteArrayInternal(@NonNull Data data) {
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         ObjectOutputStream objectOutputStream = null;
         try {
@@ -429,7 +419,8 @@
      * @throws IllegalStateException if bytes is bigger than {@link #MAX_DATA_BYTES}
      */
     @TypeConverter
-    public static @NonNull Data fromByteArray(@NonNull byte[] bytes) {
+    @NonNull
+    public static Data fromByteArray(@NonNull byte[] bytes) {
         if (bytes.length > MAX_DATA_BYTES) {
             throw new IllegalStateException(
                     "Data cannot occupy more than " + MAX_DATA_BYTES + " bytes when serialized");
@@ -521,8 +512,12 @@
         return sb.toString();
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static @NonNull Boolean[] convertPrimitiveBooleanArray(@NonNull boolean[] value) {
+    /**
+     * * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Boolean[] convertPrimitiveBooleanArray(@NonNull boolean[] value) {
         Boolean[] returnValue = new Boolean[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -530,8 +525,12 @@
         return returnValue;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static @NonNull Byte[] convertPrimitiveByteArray(@NonNull byte[] value) {
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Byte[] convertPrimitiveByteArray(@NonNull byte[] value) {
         Byte[] returnValue = new Byte[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -539,8 +538,12 @@
         return returnValue;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static @NonNull Integer[] convertPrimitiveIntArray(@NonNull int[] value) {
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Integer[] convertPrimitiveIntArray(@NonNull int[] value) {
         Integer[] returnValue = new Integer[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -548,8 +551,12 @@
         return returnValue;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static @NonNull Long[] convertPrimitiveLongArray(@NonNull long[] value) {
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Long[] convertPrimitiveLongArray(@NonNull long[] value) {
         Long[] returnValue = new Long[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -557,8 +564,12 @@
         return returnValue;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static @NonNull Float[] convertPrimitiveFloatArray(@NonNull float[] value) {
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Float[] convertPrimitiveFloatArray(@NonNull float[] value) {
         Float[] returnValue = new Float[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -566,8 +577,12 @@
         return returnValue;
     }
 
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    static @NonNull Double[] convertPrimitiveDoubleArray(@NonNull double[] value) {
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static Double[] convertPrimitiveDoubleArray(@NonNull double[] value) {
         Double[] returnValue = new Double[value.length];
         for (int i = 0; i < value.length; ++i) {
             returnValue[i] = value[i];
@@ -576,6 +591,84 @@
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static boolean[] convertToPrimitiveArray(@NonNull Boolean[] array) {
+        boolean[] returnArray = new boolean[array.length];
+        for (int i = 0; i < array.length; ++i) {
+            returnArray[i] = array[i];
+        }
+        return returnArray;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static byte[] convertToPrimitiveArray(@NonNull Byte[] array) {
+        byte[] returnArray = new byte[array.length];
+        for (int i = 0; i < array.length; ++i) {
+            returnArray[i] = array[i];
+        }
+        return returnArray;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static int[] convertToPrimitiveArray(@NonNull Integer[] array) {
+        int[] returnArray = new int[array.length];
+        for (int i = 0; i < array.length; ++i) {
+            returnArray[i] = array[i];
+        }
+        return returnArray;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static long[] convertToPrimitiveArray(@NonNull Long[] array) {
+        long[] returnArray = new long[array.length];
+        for (int i = 0; i < array.length; ++i) {
+            returnArray[i] = array[i];
+        }
+        return returnArray;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static float[] convertToPrimitiveArray(@NonNull Float[] array) {
+        float[] returnArray = new float[array.length];
+        for (int i = 0; i < array.length; ++i) {
+            returnArray[i] = array[i];
+        }
+        return returnArray;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public static double[] convertToPrimitiveArray(@NonNull Double[] array) {
+        double[] returnArray = new double[array.length];
+        for (int i = 0; i < array.length; ++i) {
+            returnArray[i] = array[i];
+        }
+        return returnArray;
+    }
+
+    /**
      * A builder for {@link Data} objects.
      */
     public static final class Builder {
@@ -585,11 +678,12 @@
         /**
          * Puts a boolean into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putBoolean(@NonNull String key, boolean value) {
+        @NonNull
+        public Builder putBoolean(@NonNull String key, boolean value) {
             mValues.put(key, value);
             return this;
         }
@@ -597,11 +691,12 @@
         /**
          * Puts a boolean array into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putBooleanArray(@NonNull String key, @NonNull boolean[] value) {
+        @NonNull
+        public Builder putBooleanArray(@NonNull String key, @NonNull boolean[] value) {
             mValues.put(key, convertPrimitiveBooleanArray(value));
             return this;
         }
@@ -609,11 +704,12 @@
         /**
          * Puts an byte into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putByte(@NonNull String key, byte value) {
+        @NonNull
+        public Builder putByte(@NonNull String key, byte value) {
             mValues.put(key, value);
             return this;
         }
@@ -621,11 +717,12 @@
         /**
          * Puts an integer array into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putByteArray(@NonNull String key, @NonNull byte[] value) {
+        @NonNull
+        public Builder putByteArray(@NonNull String key, @NonNull byte[] value) {
             mValues.put(key, convertPrimitiveByteArray(value));
             return this;
         }
@@ -633,11 +730,12 @@
         /**
          * Puts an integer into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putInt(@NonNull String key, int value) {
+        @NonNull
+        public Builder putInt(@NonNull String key, int value) {
             mValues.put(key, value);
             return this;
         }
@@ -645,11 +743,12 @@
         /**
          * Puts an integer array into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putIntArray(@NonNull String key, @NonNull int[] value) {
+        @NonNull
+        public Builder putIntArray(@NonNull String key, @NonNull int[] value) {
             mValues.put(key, convertPrimitiveIntArray(value));
             return this;
         }
@@ -657,11 +756,12 @@
         /**
          * Puts a long into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putLong(@NonNull String key, long value) {
+        @NonNull
+        public Builder putLong(@NonNull String key, long value) {
             mValues.put(key, value);
             return this;
         }
@@ -669,11 +769,12 @@
         /**
          * Puts a long array into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putLongArray(@NonNull String key, @NonNull long[] value) {
+        @NonNull
+        public Builder putLongArray(@NonNull String key, @NonNull long[] value) {
             mValues.put(key, convertPrimitiveLongArray(value));
             return this;
         }
@@ -681,11 +782,12 @@
         /**
          * Puts a float into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putFloat(@NonNull String key, float value) {
+        @NonNull
+        public Builder putFloat(@NonNull String key, float value) {
             mValues.put(key, value);
             return this;
         }
@@ -693,11 +795,12 @@
         /**
          * Puts a float array into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putFloatArray(@NonNull String key, @NonNull float[] value) {
+        @NonNull
+        public Builder putFloatArray(@NonNull String key, @NonNull float[] value) {
             mValues.put(key, convertPrimitiveFloatArray(value));
             return this;
         }
@@ -705,11 +808,12 @@
         /**
          * Puts a double into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putDouble(@NonNull String key, double value) {
+        @NonNull
+        public Builder putDouble(@NonNull String key, double value) {
             mValues.put(key, value);
             return this;
         }
@@ -717,11 +821,12 @@
         /**
          * Puts a double array into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putDoubleArray(@NonNull String key, @NonNull double[] value) {
+        @NonNull
+        public Builder putDoubleArray(@NonNull String key, @NonNull double[] value) {
             mValues.put(key, convertPrimitiveDoubleArray(value));
             return this;
         }
@@ -729,11 +834,12 @@
         /**
          * Puts a String into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putString(@NonNull String key, @Nullable String value) {
+        @NonNull
+        public Builder putString(@NonNull String key, @Nullable String value) {
             mValues.put(key, value);
             return this;
         }
@@ -741,11 +847,12 @@
         /**
          * Puts a String array into the arguments.
          *
-         * @param key The key for this argument
+         * @param key   The key for this argument
          * @param value The value for this argument
          * @return The {@link Builder}
          */
-        public @NonNull Builder putStringArray(@NonNull String key, @NonNull String[] value) {
+        @NonNull
+        public Builder putStringArray(@NonNull String key, @NonNull String[] value) {
             mValues.put(key, value);
             return this;
         }
@@ -759,7 +866,8 @@
          * @param data {@link Data} containing key-value pairs to add
          * @return The {@link Builder}
          */
-        public @NonNull Builder putAll(@NonNull Data data) {
+        @NonNull
+        public Builder putAll(@NonNull Data data) {
             putAll(data.mValues);
             return this;
         }
@@ -773,7 +881,8 @@
          * @param values A {@link Map} of key-value pairs to add
          * @return The {@link Builder}
          */
-        public @NonNull Builder putAll(@NonNull Map<String, Object> values) {
+        @NonNull
+        public Builder putAll(@NonNull Map<String, Object> values) {
             for (Map.Entry<String, Object> entry : values.entrySet()) {
                 String key = entry.getKey();
                 Object value = entry.getValue();
@@ -787,13 +896,14 @@
          * Long, Float, Double, String, and array versions of each of those types.
          * Invalid types throw an {@link IllegalArgumentException}.
          *
-         * @param key A {@link String} key to add
+         * @param key   A {@link String} key to add
          * @param value A nullable {@link Object} value to add of the valid types
          * @return The {@link Builder}
          * @hide
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        public @NonNull Builder put(@NonNull String key, @Nullable Object value) {
+        @NonNull
+        public Builder put(@NonNull String key, @Nullable Object value) {
             if (value == null) {
                 mValues.put(key, null);
             } else {
@@ -837,9 +947,10 @@
          * Builds a {@link Data} object.
          *
          * @return The {@link Data} object containing all key-value pairs specified by this
-         *         {@link Builder}.
+         * {@link Builder}.
          */
-        public @NonNull Data build() {
+        @NonNull
+        public Data build() {
             Data data = new Data(mValues);
             // Make sure we catch Data objects that are too large at build() instead of later.  This
             // method will throw an exception if data is too big.
diff --git a/work/workmanager/src/main/java/androidx/work/NetworkType.java b/work/workmanager/src/main/java/androidx/work/NetworkType.java
index 20f6d69..06d7e3a 100644
--- a/work/workmanager/src/main/java/androidx/work/NetworkType.java
+++ b/work/workmanager/src/main/java/androidx/work/NetworkType.java
@@ -16,6 +16,8 @@
 
 package androidx.work;
 
+import androidx.annotation.RequiresApi;
+
 /**
  * An enumeration of various network types that can be used as {@link Constraints} for work.
  */
@@ -45,5 +47,16 @@
     /**
      * A metered network connection is required for this work.
      */
-    METERED
+    METERED,
+
+    /**
+     * A temporarily unmetered Network. This capability will be set for networks that are
+     * generally metered, but are currently unmetered.
+     *
+     * Note: This capability can be changed at any time. When it is removed,
+     * {@link ListenableWorker}s are responsible for stopping any data transfer that should not
+     * occur on a metered network.
+     */
+    @RequiresApi(30)
+    TEMPORARILY_UNMETERED
 }
diff --git a/work/workmanager/src/main/java/androidx/work/WorkManager.java b/work/workmanager/src/main/java/androidx/work/WorkManager.java
index 29a7982..4ae19f2 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkManager.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkManager.java
@@ -189,7 +189,7 @@
      * {@link Configuration}.  By default, this method should not be called because WorkManager is
      * automatically initialized.  To initialize WorkManager yourself, please follow these steps:
      * <p><ul>
-     * <li>Disable {@code androidx.work.impl.WorkManagerInitializer} in your manifest.
+     * <li>Disable {@code androidx.work.WorkManagerInitializer} in your manifest.
      * <li>Invoke this method in {@code Application#onCreate} or a {@code ContentProvider}. Note
      * that this method <b>must</b> be invoked in one of these two places or you risk getting a
      * {@code NullPointerException} in {@link #getInstance(Context)}.
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerInitializer.java b/work/workmanager/src/main/java/androidx/work/WorkManagerInitializer.java
similarity index 83%
rename from work/workmanager/src/main/java/androidx/work/impl/WorkManagerInitializer.java
rename to work/workmanager/src/main/java/androidx/work/WorkManagerInitializer.java
index 39f1b1c3..93acbdf 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerInitializer.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkManagerInitializer.java
@@ -14,27 +14,20 @@
  * limitations under the License.
  */
 
-package androidx.work.impl;
+package androidx.work;
 
 import android.content.Context;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
 import androidx.startup.Initializer;
-import androidx.work.Configuration;
-import androidx.work.Logger;
-import androidx.work.WorkManager;
 
 import java.util.Collections;
 import java.util.List;
 
 /**
  * Initializes {@link androidx.work.WorkManager} using {@code androidx.startup}.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class WorkManagerInitializer implements Initializer<WorkManager> {
+public final class WorkManagerInitializer implements Initializer<WorkManager> {
 
     private static final String TAG = Logger.tagWithPrefix("WrkMgrInitializer");
 
diff --git a/work/workmanager/src/main/java/androidx/work/WorkerParameters.java b/work/workmanager/src/main/java/androidx/work/WorkerParameters.java
index 6580b99..a4c07e9 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkerParameters.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkerParameters.java
@@ -195,6 +195,14 @@
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public @NonNull RuntimeExtras getRuntimeExtras() {
+        return mRuntimeExtras;
+    }
+
+    /**
      * Extra runtime information for Workers.
      *
      * @hide
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
index 7074358..26654a4 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
@@ -434,9 +434,8 @@
             // Check to see if there is more work to be done. If there is no more work, then
             // disable RescheduleReceiver. Using a transaction here, as there could be more than
             // one thread looking at the list of eligible WorkSpecs.
-            List<String> unfinishedWork = mWorkDatabase.workSpecDao().getAllUnfinishedWork();
-            boolean noMoreWork = unfinishedWork == null || unfinishedWork.isEmpty();
-            if (noMoreWork) {
+            boolean hasUnfinishedWork = mWorkDatabase.workSpecDao().hasUnfinishedWork();
+            if (!hasUnfinishedWork) {
                 PackageManagerHelper.setComponentEnabled(
                         mAppContext, RescheduleReceiver.class, false);
             }
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index 224d23c..ac259b5 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -18,9 +18,12 @@
 
 import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
 
+import android.annotation.SuppressLint;
 import android.app.job.JobInfo;
 import android.content.ComponentName;
 import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
 import android.os.Build;
 import android.os.PersistableBundle;
 
@@ -68,16 +71,16 @@
      */
     JobInfo convert(WorkSpec workSpec, int jobId) {
         Constraints constraints = workSpec.constraints;
-        int jobInfoNetworkType = convertNetworkType(constraints.getRequiredNetworkType());
         PersistableBundle extras = new PersistableBundle();
         extras.putString(EXTRA_WORK_SPEC_ID, workSpec.id);
         extras.putBoolean(EXTRA_IS_PERIODIC, workSpec.isPeriodic());
         JobInfo.Builder builder = new JobInfo.Builder(jobId, mWorkServiceComponent)
-                .setRequiredNetworkType(jobInfoNetworkType)
                 .setRequiresCharging(constraints.requiresCharging())
                 .setRequiresDeviceIdle(constraints.requiresDeviceIdle())
                 .setExtras(extras);
 
+        setRequiredNetwork(builder, constraints.getRequiredNetworkType());
+
         if (!constraints.requiresDeviceIdle()) {
             // Device Idle and Backoff Criteria cannot be set together
             int backoffPolicy = workSpec.backoffPolicy == BackoffPolicy.LINEAR
@@ -131,11 +134,34 @@
     }
 
     /**
+     * Adds the required network capabilities on the {@link JobInfo.Builder} instance.
+     *
+     * @param builder     The instance of {@link JobInfo.Builder}.
+     * @param networkType The {@link NetworkType} instance.
+     */
+    @SuppressLint("UnsafeNewApiCall")
+    static void setRequiredNetwork(
+            @NonNull JobInfo.Builder builder,
+            @NonNull NetworkType networkType) {
+
+        if (Build.VERSION.SDK_INT >= 30 && networkType == NetworkType.TEMPORARILY_UNMETERED) {
+            NetworkRequest networkRequest = new NetworkRequest.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+                    .build();
+
+            builder.setRequiredNetwork(networkRequest);
+        } else {
+            builder.setRequiredNetworkType(convertNetworkType(networkType));
+        }
+    }
+
+    /**
      * Converts {@link NetworkType} into {@link JobInfo}'s network values.
      *
      * @param networkType The {@link NetworkType} network type
      * @return The {@link JobInfo} network type
      */
+    @SuppressWarnings("MissingCasesInEnumSwitch")
     static int convertNetworkType(NetworkType networkType) {
         switch(networkType) {
             case NOT_REQUIRED:
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
index 799a112..1672f94 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
@@ -16,9 +16,11 @@
 
 package androidx.work.impl.constraints.controllers;
 
+import static androidx.work.NetworkType.TEMPORARILY_UNMETERED;
 import static androidx.work.NetworkType.UNMETERED;
 
 import android.content.Context;
+import android.os.Build;
 
 import androidx.annotation.NonNull;
 import androidx.work.impl.constraints.NetworkState;
@@ -39,7 +41,9 @@
 
     @Override
     boolean hasConstraint(@NonNull WorkSpec workSpec) {
-        return workSpec.constraints.getRequiredNetworkType() == UNMETERED;
+        return workSpec.constraints.getRequiredNetworkType() == UNMETERED
+                || (Build.VERSION.SDK_INT >= 30
+                && workSpec.constraints.getRequiredNetworkType() == TEMPORARILY_UNMETERED);
     }
 
     @Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java b/work/workmanager/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
index 261e8e2..8bafccf 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.work.Logger;
 import androidx.work.OneTimeWorkRequest;
 import androidx.work.WorkManager;
@@ -29,7 +30,10 @@
 
 /**
  * The {@link android.content.BroadcastReceiver} which dumps out useful diagnostics information.
+ *
+ * @hide
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class DiagnosticsReceiver extends BroadcastReceiver {
     private static final String TAG = Logger.tagWithPrefix("DiagnosticsRcvr");
 
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
index 68b0e28..aa2f15f 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
@@ -265,6 +265,12 @@
     List<String> getAllUnfinishedWork();
 
     /**
+     * @return {@code true} if there is pending work.
+     */
+    @Query("SELECT COUNT(*) > 0 FROM workspec WHERE state NOT IN " + COMPLETED_STATES + " LIMIT 1")
+    boolean hasUnfinishedWork();
+
+    /**
      * Marks a {@link WorkSpec} as scheduled.
      *
      * @param id        The identifier for the {@link WorkSpec}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
index 2cdd930..4e665ea 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
@@ -26,6 +26,7 @@
 import static androidx.work.WorkInfo.State.SUCCEEDED;
 
 import android.net.Uri;
+import android.os.Build;
 
 import androidx.room.TypeConverter;
 import androidx.work.BackoffPolicy;
@@ -76,6 +77,7 @@
         int UNMETERED = 2;
         int NOT_ROAMING = 3;
         int METERED = 4;
+        int TEMPORARILY_UNMETERED = 5;
     }
 
     /**
@@ -193,6 +195,7 @@
      * @return The associated int constant
      */
     @TypeConverter
+    @SuppressWarnings("NewApi")
     public static int networkTypeToInt(NetworkType networkType) {
         switch (networkType) {
             case NOT_REQUIRED:
@@ -211,8 +214,13 @@
                 return NetworkTypeIds.METERED;
 
             default:
+                if (Build.VERSION.SDK_INT >= 30
+                        && networkType == NetworkType.TEMPORARILY_UNMETERED) {
+                    return NetworkTypeIds.TEMPORARILY_UNMETERED;
+                }
                 throw new IllegalArgumentException(
                         "Could not convert " + networkType + " to int");
+
         }
     }
 
@@ -241,6 +249,9 @@
                 return NetworkType.METERED;
 
             default:
+                if (Build.VERSION.SDK_INT >= 30 && value == NetworkTypeIds.TEMPORARILY_UNMETERED) {
+                    return NetworkType.TEMPORARILY_UNMETERED;
+                }
                 throw new IllegalArgumentException(
                         "Could not convert " + value + " to NetworkType");
         }