[go: nahoru, domu]

Merge "Change most of StringDef enum like values to intdef." into androidx-main
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index d086cea..27fd54e 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -9,7 +9,6 @@
 android {
     defaultConfig {
         multiDexEnabled true
-        testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"
     }
     namespace "androidx.activity"
 }
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
index f773be2..ab89766 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
@@ -30,12 +30,18 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class ComponentActivityCallbacksTest {
+
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun onConfigurationChanged() {
        withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityLifecycleTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityLifecycleTest.kt
index 56ed97d..2a23d40 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityLifecycleTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityLifecycleTest.kt
@@ -24,6 +24,8 @@
 import androidx.test.filters.LargeTest
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,6 +39,9 @@
 @RunWith(AndroidJUnit4::class)
 class ComponentActivityLifecycleTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     @Throws(Throwable::class)
     fun testLifecycleObserver() {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityMenuTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityMenuTest.kt
index 4062812..f72721a 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityMenuTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityMenuTest.kt
@@ -44,6 +44,8 @@
 import com.google.common.truth.Truth.assertWithMessage
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -53,6 +55,9 @@
 @RunWith(AndroidJUnit4::class)
 class ComponentActivityMenuTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun inflatesMenu() {
        withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
index 8e67e4f..23758b0 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
@@ -25,7 +25,9 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -33,6 +35,9 @@
 @RunWith(AndroidJUnit4::class)
 class ComponentActivityOverrideLifecycleTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @UiThreadTest
     @Test
     fun testEagerOverride() {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
index c0a91ed..a091c94 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
@@ -20,6 +20,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.testutils.withActivity
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import androidx.testutils.withUse
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -28,6 +30,9 @@
 @RunWith(AndroidJUnit4::class)
 class ComponentActivityReportFullyDrawnTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testReportFullyDrawn() {
        withUse(ActivityScenario.launch(ReportFullyDrawnActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
index ecbd905..e2025c9 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
@@ -31,6 +31,8 @@
 import androidx.test.filters.LargeTest
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import org.junit.Test
@@ -39,6 +41,10 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class ComponentActivityResultTest {
+
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun launchInOnCreate() {
         ActivityScenario.launch(ResultComponentActivity::class.java).use { scenario ->
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityRunOnNextRecreateTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityRunOnNextRecreateTest.kt
index e89edfa..38fe672 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityRunOnNextRecreateTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityRunOnNextRecreateTest.kt
@@ -26,6 +26,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -33,6 +35,9 @@
 @RunWith(AndroidJUnit4::class)
 class ComponentActivityRunOnNextRecreateTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     private class Restarted2 : SavedStateRegistry.AutoRecreated {
         override fun onRecreated(owner: SavedStateRegistryOwner) {
             (owner as? AutoRestarterActivity)?.restartedValue = "restarted"
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
index 238b742..0222cd4 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
@@ -27,7 +27,9 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.After
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,6 +37,9 @@
 @RunWith(AndroidJUnit4::class)
 class ComponentActivitySavedStateTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @After
     fun clear() {
         SavedStateActivity.checkEnabledInOnCreate = false
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityViewModelTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityViewModelTest.kt
index 4b9b9e6..cd451a7 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityViewModelTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityViewModelTest.kt
@@ -31,6 +31,8 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -38,6 +40,9 @@
 @RunWith(AndroidJUnit4::class)
 class ComponentActivityViewModelTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test(expected = IllegalStateException::class)
     @UiThreadTest
     fun testNotAttachedActivity() {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
index 5eaa89cb..b3a2567 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
@@ -26,6 +26,8 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -33,6 +35,10 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class ComponentDialogTest {
+
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testLifecycle() {
        withUse(ActivityScenario.launch(EmptyContentActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
index 1908896..925adcd 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
@@ -31,6 +31,8 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -38,6 +40,9 @@
 @RunWith(AndroidJUnit4::class)
 class ContentViewTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testLifecycleObserver() {
        withUse(ActivityScenario.launch(ContentViewActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/FullyDrawnReporterTest.kt b/activity/activity/src/androidTest/java/androidx/activity/FullyDrawnReporterTest.kt
index 2d7ff0a..1562394 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/FullyDrawnReporterTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/FullyDrawnReporterTest.kt
@@ -32,6 +32,8 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.sync.Mutex
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -39,6 +41,10 @@
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
 class FullyDrawnReporterTest {
+
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun findFullyDrawnReporterOwner() {
         withUse(ActivityScenario.launch(FullyDrawnActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/LeakInputMethodManagerTest.kt b/activity/activity/src/androidTest/java/androidx/activity/LeakInputMethodManagerTest.kt
index f324886..19d07fd 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/LeakInputMethodManagerTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/LeakInputMethodManagerTest.kt
@@ -25,20 +25,26 @@
 import android.widget.TextView
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class LeakInputMethodManagerTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     val activityRule = androidx.test.rule.ActivityTestRule(LeakingActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun leakThroughRemovedEditText() {
         activityRule.runOnUiThread {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
index 5e1790d..1ff31b7 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
@@ -24,6 +24,8 @@
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -33,6 +35,9 @@
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S_V2)
 class OnBackPressedDispatcherInvokerTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testSimpleInvoker() {
         var registerCount = 0
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index 3a75468..7380772 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -26,7 +26,9 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,6 +39,9 @@
     private var fallbackCount = 0
     lateinit var dispatcher: OnBackPressedDispatcher
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Before
     fun setup() {
         fallbackCount = 0
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
index 98a312c..f78e6ea 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
@@ -23,12 +23,18 @@
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ViewTreeOnBackPressedDispatcherTest {
+
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     /**
      * Tests that a direct set/get on a single view survives a round trip
      */
diff --git a/activity/activity/src/androidTest/java/androidx/activity/contextaware/ContextAwareHelperTest.kt b/activity/activity/src/androidTest/java/androidx/activity/contextaware/ContextAwareHelperTest.kt
index 424d646..e3bbb87 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/contextaware/ContextAwareHelperTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/contextaware/ContextAwareHelperTest.kt
@@ -21,6 +21,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -29,6 +31,9 @@
 class ContextAwareHelperTest {
     private val contextAware = TestContextAware()
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun addOnContextAvailableListener() {
         var callbackCount = 0
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
index f6240cc..3ef8476 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
@@ -26,6 +26,8 @@
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -47,6 +49,9 @@
         }
     }
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun launchTest() {
         val launcher = registry.register("key", StartActivityForResult()) {}
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
index 03474b9..dad575f 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
@@ -29,7 +29,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -47,6 +49,9 @@
         }
     }
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testRegisterLifecycleOwnerCallback() {
         val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.INITIALIZED)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
index 42992fb..fde1fa0 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
@@ -20,6 +20,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -27,6 +29,9 @@
 @RunWith(AndroidJUnit4::class)
 class PickVisualMediaRequestTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun buildPickVisualMedia() {
         val request = PickVisualMediaRequest.Builder().setMediaType(
diff --git a/activity/integration-tests/testapp/build.gradle b/activity/integration-tests/testapp/build.gradle
index 733b7e4..b8d1e1f 100644
--- a/activity/integration-tests/testapp/build.gradle
+++ b/activity/integration-tests/testapp/build.gradle
@@ -22,8 +22,6 @@
 
 android {
     defaultConfig {
-        testInstrumentationRunnerArgument "listener", "com.squareup.leakcanary.FailTestOnLeakRunListener"
-
         applicationId "androidx.activity.integration.testapp"
     }
     namespace "androidx.activity.integration.testapp"
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index 6b454fa..c188aea 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -54,7 +54,6 @@
         exclude group: "androidx.appcompat", module: "appcompat"
         exclude group: "androidx.core", module: "core"
     })
-    androidTestImplementation(projectOrArtifact(":recyclerview:recyclerview"))
     androidTestImplementation(libs.multidex)
 
     testImplementation(libs.kotlinStdlib)
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
index 863853d..e126d37 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
@@ -56,6 +56,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ActivityTestRule;
 
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -80,6 +81,7 @@
                 mActivityTestRule.getActivity().getSupportActionBar());
     }
 
+    @Ignore // b/256195085
     @Test
     public void testActionBarOverflowVisibilityListener() {
         if ("ranchu".equals(Build.HARDWARE)) {
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
index 5bd08dc..3e48121 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
@@ -26,6 +26,7 @@
 import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWait
 import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
 import androidx.core.os.LocaleListCompat
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.testutils.waitForExecution
@@ -118,6 +119,7 @@
 
     @SdkSuppress(minSdkVersion = 17)
     @Test
+    @FlakyTest(bugId = 255765202)
     fun testLayoutDirectionAfterRecreating() {
         setLocalesAndWaitForRecreate(rule, getRTLLocaleList())
 
diff --git a/benchmark/benchmark-darwin-gradle-plugin/build.gradle b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
index 4282513..ee36734 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/build.gradle
+++ b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
@@ -25,6 +25,7 @@
 
 dependencies {
     api(gradleApi())
+    implementation(libs.kotlinGradlePluginz)
     implementation(libs.gson)
     implementation(libs.apacheCommonsMath)
     testImplementation(gradleTestKit())
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index b712be2..13f1350 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -16,8 +16,12 @@
 
 package androidx.benchmark.darwin.gradle
 
+import java.util.concurrent.atomic.AtomicBoolean
 import org.gradle.api.Plugin
 import org.gradle.api.Project
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
 
 /**
  * The Darwin benchmark plugin that helps run KMP benchmarks on iOS devices, and extracts benchmark
@@ -28,6 +32,28 @@
         val extension =
             project.extensions.create("darwinBenchmark", DarwinBenchmarkPluginExtension::class.java)
 
+        val appliedDarwinPlugin = AtomicBoolean()
+
+        project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {
+            val multiplatformExtension: KotlinMultiplatformExtension =
+                project.extensions.getByType(it.projectExtensionClass.java)
+            multiplatformExtension.targets.all { kotlinTarget ->
+                if (kotlinTarget is KotlinNativeTarget) {
+                    if (kotlinTarget.konanTarget.family.isAppleFamily) {
+                        // We want to apply the plugin only once.
+                        if (appliedDarwinPlugin.compareAndSet(false, true)) {
+                            applyDarwinPlugin(extension, project)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun applyDarwinPlugin(
+        extension: DarwinBenchmarkPluginExtension,
+        project: Project
+    ) {
         val xcodeProjectPath = extension.xcodeProjectName.flatMap { name ->
             project.layout.buildDirectory.dir("$name.xcodeproj")
         }
@@ -51,13 +77,10 @@
             it.xcodeProjectPath.set(generateXCodeProjectTask.flatMap { task ->
                 task.xcProjectPath
             })
-            val taskName = extension.xcFrameworkConfig.map { name ->
-                "assemble${name}ReleaseXCFramework"
-            }
             it.destination.set(extension.destination)
             it.scheme.set(extension.scheme)
             it.xcResultPath.set(xcResultPath)
-            it.dependsOn(taskName)
+            it.dependsOn(XCFRAMEWORK_TASK_NAME)
         }
 
         project.tasks.register(
@@ -78,5 +101,9 @@
         const val GENERATE_XCODE_PROJECT_TASK = "generateXCodeProject"
         const val RUN_DARWIN_BENCHMARKS_TASK = "runDarwinBenchmarks"
         const val DARWIN_BENCHMARK_RESULTS_TASK = "darwinBenchmarkResults"
+
+        // There is always a XCFrameworkConfig with the name `AndroidXDarwinBenchmarks`
+        // The corresponding task name is `assemble{name}ReleaseXCFramework`
+        const val XCFRAMEWORK_TASK_NAME = "assembleAndroidXDarwinBenchmarksReleaseXCFramework"
     }
 }
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
index 83781e8..35fec87 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
@@ -43,10 +43,4 @@
      * This is typically discovered by using `xcrun xctrace list devices`.
      */
     abstract val destination: Property<String>
-
-    /**
-     * The name of the `XCFrameworkConfig` defined in the benchmark's module build.gradle.
-     * This is used to derive the name of the `release` task to generate the `XCFramework`.
-     */
-    abstract val xcFrameworkConfig: Property<String>
 }
diff --git a/benchmark/benchmark-darwin-samples/build.gradle b/benchmark/benchmark-darwin-samples/build.gradle
index da95b27..dc8c184 100644
--- a/benchmark/benchmark-darwin-samples/build.gradle
+++ b/benchmark/benchmark-darwin-samples/build.gradle
@@ -7,8 +7,7 @@
 }
 
 androidXMultiplatform {
-
-    // XCFrameworkConfig must match module name
+    // XCFrameworkConfig must always be called AndroidXDarwinBenchmarks
     def xcf = new XCFrameworkConfig(project, "AndroidXDarwinBenchmarks")
 
     ios {
@@ -57,7 +56,6 @@
     scheme = "testapp-ios"
     // ios 13, 15.2
     destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
-    xcFrameworkConfig = "AndroidXDarwinBenchmarks"
 }
 
 androidx {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/BaselineProfilesTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/BaselineProfilesTest.kt
index 602fcbc..05451ec 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/BaselineProfilesTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/BaselineProfilesTest.kt
@@ -16,8 +16,11 @@
 
 package androidx.benchmark.macro
 
+import androidx.benchmark.DeviceInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -37,4 +40,15 @@
         assertEquals(filtered.lines().size, 1)
         assertEquals("Landroidx/Foo/Bar;", filtered)
     }
+
+    @Test
+    fun deviceSpecifier() {
+        if (DeviceInfo.isEmulator) {
+            assertEquals(deviceSpecifier, "-e ")
+        } else {
+            assertTrue(deviceSpecifier.startsWith("-s "), "observed $deviceSpecifier")
+            assertTrue(deviceSpecifier.endsWith(" "), "observed $deviceSpecifier")
+            assertNotEquals(deviceSpecifier, "-s  ")
+        }
+    }
 }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ConfigurableActivity.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ConfigurableActivity.kt
index edfd61f..5d953d2 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ConfigurableActivity.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ConfigurableActivity.kt
@@ -28,9 +28,10 @@
 class ConfigurableActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+        val textContent = intent.getStringExtra(EXTRA_TEXT)
 
         val view = TextView(this).apply {
-            text = intent.getStringExtra(EXTRA_TEXT)
+            text = textContent
         }
         setContentView(view)
 
@@ -50,7 +51,11 @@
             else -> {
                 // report delayed, modify text
                 val runnable = {
-                    view.text = FULLY_DRAWN_TEXT
+                    view.text = if (textContent == INNER_ACTIVITY_TEXT) {
+                        INNER_ACTIVITY_FULLY_DRAWN_TEXT
+                    } else {
+                        FULLY_DRAWN_TEXT
+                    }
                     reportFullyDrawn()
                 }
                 view.postDelayed(runnable, reportFullyDrawnDelayMs)
@@ -76,6 +81,7 @@
         private const val EXTRA_REPORT_FULLY_DRAWN_DELAY_MS = "REPORT_FULLY_DRAWN_DELAY_MS"
         const val FULLY_DRAWN_TEXT = "FULLY DRAWN"
         const val INNER_ACTIVITY_TEXT = "INNER ACTIVITY"
+        const val INNER_ACTIVITY_FULLY_DRAWN_TEXT = "INNER ACTIVITY FULLY DRAWN"
 
         fun createIntent(
             text: String,
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index 0815a2a..3fc5027 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -27,11 +27,13 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
+import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
 import kotlin.test.assertNotNull
+import kotlin.test.assertNull
 import kotlin.test.assertTrue
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
 import org.junit.Assert.fail
 import org.junit.Assume.assumeTrue
 import org.junit.Before
@@ -212,4 +214,58 @@
     /** Tests getFrameStats after launch which does nothing, as Activity already visible */
     @Test
     fun getFrameStats_noop() = validateLaunchAndFrameStats(pressHome = false)
+
+    private fun validateShaderCache(empty: Boolean, packageName: String) {
+        val path = MacrobenchmarkScope.getShaderCachePath(packageName)
+        println("validating shader path $path")
+        val fileCount = Shell.executeScript("find $path -type f | wc -l").trim().toInt()
+        if (empty) {
+            assertEquals(0, fileCount)
+        } else {
+            assertNotEquals(0, fileCount)
+        }
+    }
+
+    private fun validateDropShaderCacheWithRoot(
+        dropShaderCacheBlock: MacrobenchmarkScope.() -> Unit
+    ) {
+        // need root to inspect target app's code cache dir, and emulators
+        // don't seem to store shaders
+        assumeTrue(Shell.isSessionRooted() && !DeviceInfo.isEmulator)
+
+        val scope = MacrobenchmarkScope(
+            Packages.TARGET,
+            launchWithClearTask = false
+        )
+        // reset to empty to begin with
+        scope.killProcess()
+        scope.dropShaderCacheBlock()
+        validateShaderCache(empty = true, scope.packageName)
+
+        // start an activity, expecting shader compilation
+        scope.pressHome()
+        // NOTE: if platform fixes default activity to not compile shaders,
+        //   may need to update this test UI to trigger shader creation
+        scope.startActivityAndWait()
+        Thread.sleep(5000) // sleep to await flushing cache to disk
+        scope.killProcess()
+        validateShaderCache(empty = false, scope.packageName)
+
+        // verify deletion
+        scope.killProcess()
+        scope.dropShaderCacheBlock()
+        validateShaderCache(empty = true, scope.packageName)
+    }
+
+    @Test
+    fun dropShaderCacheBroadcast() = validateDropShaderCacheWithRoot {
+        // since this test runs on root and the public api falls back to
+        // a root impl, test the broadcast directly
+        assertNull(ProfileInstallBroadcast.dropShaderCache(packageName))
+    }
+
+    @Test
+    fun dropShaderCachePublicApi() = validateDropShaderCacheWithRoot {
+        dropShaderCache()
+    }
 }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index c3fbdc2..892820b 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -129,10 +129,18 @@
                 scope.startActivityAndWait(launchIntent)
             }
 
-            if (delayMs > 0) {
+            if (useInAppNav) {
+                // in app nav destinations always have different strings to differentiate
+                // vs the first activity's strings to prevent races
+                awaitActivityText(
+                    if (delayMs > 0) {
+                        ConfigurableActivity.INNER_ACTIVITY_FULLY_DRAWN_TEXT
+                    } else {
+                        ConfigurableActivity.INNER_ACTIVITY_TEXT
+                    }
+                )
+            } else if (delayMs > 0) {
                 awaitActivityText(ConfigurableActivity.FULLY_DRAWN_TEXT)
-            } else if (useInAppNav) {
-                awaitActivityText(ConfigurableActivity.INNER_ACTIVITY_TEXT)
             }
         }
 
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
index e9e3b13..749f23e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
@@ -22,6 +22,7 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import androidx.benchmark.Arguments
+import androidx.benchmark.DeviceInfo
 import androidx.benchmark.InstrumentationResults
 import androidx.benchmark.Outputs
 import androidx.benchmark.Shell
@@ -285,7 +286,7 @@
         .append(
             """
                 To copy the profile use:
-                adb pull "${record.profilePath}" .
+                adb pull $deviceSpecifier"${record.profilePath}" .
             """.trimIndent()
         )
 
@@ -296,13 +297,31 @@
             .append(
                 """
                     To copy the startup profile use:
-                    adb pull "${record.startupProfilePath}" .
+                    adb pull $deviceSpecifier"${record.startupProfilePath}" .
                 """.trimIndent()
             )
     }
     return summary.toString()
 }
 
+/**
+ * adb device specifier, blank if can't be defined. Includes right side space.
+ */
+internal val deviceSpecifier by lazy {
+    if (DeviceInfo.isEmulator) {
+        // emulators have serials that aren't usable via ADB -s,
+        // so we just specify emulator and hope there's only one
+        "-e "
+    } else {
+        val getpropOutput = Shell.executeScriptWithStderr("getprop ro.serialno")
+        if (getpropOutput.stdout.isBlank() || getpropOutput.stderr.isNotBlank()) {
+            "" // failed to get serial
+        } else {
+            "-s ${getpropOutput.stdout.trim()} "
+        }
+    }
+}
+
 private data class Summary(
     val totalRunTime: Long,
     val profilePath: String,
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 39cf318..285827b 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -28,16 +28,14 @@
 import androidx.benchmark.macro.CompilationMode.Ignore
 import androidx.benchmark.macro.CompilationMode.None
 import androidx.benchmark.macro.CompilationMode.Partial
-import androidx.benchmark.userspaceTrace
 import androidx.profileinstaller.ProfileInstallReceiver
 import org.junit.AssumptionViolatedException
 
 /**
  * Type of compilation to use for a Macrobenchmark.
  *
- * Every Macrobenchmark has compilation reset before running, so that previous runs do not interfere
- * with the next. This compilation mode dictates any pre-compilation that occurs before repeatedly
- * running the setup / measure blocks of the benchmark.
+ * This compilation mode controls pre-compilation that occurs before running the setup / measure
+ * blocks of the benchmark.
  *
  * On Android N+ (API 24+), there are different levels of compilation supported:
  *
@@ -48,9 +46,12 @@
  * guide pre-compilation to mimic an application's performance after some, and JIT-ing has occurred.
  *
  * * [Full] - the app is fully pre-compiled. This is generally not representative of real user
- * experience, as apps are not fully pre-compiled on user devices, but this can be used to either
- * illustrate ideal performance, or to reduce noise/inconsistency from just-in-time compilation
- * while the benchmark runs.
+ * experience, as apps are not fully pre-compiled on user devices more recent than Android N
+ * (API 24). `Full` can be used to show unrealistic but potentially more stable performance by
+ * removing the noise/inconsistency from just-in-time compilation within benchmark runs. Note that
+ * `Full` compilation will often be slower than [Partial] compilation, as the increased code size
+ * creates more cost for disk loading during startup, and increases pressure in the instruction
+ * cache.
  *
  * * [None] - the app isn't pre-compiled at all, bypassing the default compilation that should
  * generally be done at install time, e.g. by the Play Store. This will illustrate worst case
@@ -58,7 +59,8 @@
  * useful for judging the performance impact of the baseline profiles included in your application.
  *
  * * [Ignore] - the state of compilation will be ignored. The intended use-case is for a developer
- * to customize the compilation state for an app; and then tell Macrobenchmark to leave it unchanged.
+ * to customize the compilation state for an app; and then tell Macrobenchmark to leave it
+ * unchanged.
  *
  * On Android M (API 23), only [Full] is supported, as all apps are always fully compiled.
  *
@@ -71,40 +73,12 @@
 sealed class CompilationMode {
     internal fun resetAndCompile(
         packageName: String,
-        usePackageReset: () -> Boolean = Shell::isSessionRooted,
         killProcessBlock: () -> Unit,
         warmupBlock: () -> Unit
     ) {
         if (Build.VERSION.SDK_INT >= 24) {
             if (Arguments.enableCompilation) {
                 Log.d(TAG, "Resetting $packageName")
-                // The compilation mode chooses whether a reset is required or not.
-                // Currently the only compilation mode that does not perform a reset is
-                // CompilationMode.Ignore.
-                if (shouldReset()) {
-                    // It's not possible to reset the compilation profile on `user` builds.
-                    // The flag `enablePackageReset` can be set to `true` on `userdebug` builds in
-                    // order to speed-up the profile reset. When set to false, reset is performed
-                    // uninstalling and reinstalling the app.
-                    if (usePackageReset()) {
-                        // Package reset enabled
-                        Log.d(TAG, "Re-compiling $packageName")
-                        // cmd package compile --reset returns a "Success" or a "Failure" to stdout.
-                        // Rather than rely on exit codes which are not always correct, we specifically
-                        // look for the work "Success" in stdout to make sure reset actually
-                        // happened.
-                        val output = Shell.executeScriptWithStderr(
-                            "cmd package compile --reset $packageName"
-                        )
-                        check(output.stdout.trim() == "Success") {
-                            "Unable to recompile $packageName ($output)"
-                        }
-                    } else {
-                        // User builds. Kick off a full uninstall-reinstall
-                        Log.d(TAG, "Reinstalling $packageName")
-                        reinstallPackage(packageName)
-                    }
-                }
                 // Write skip file to stop profile installer from interfering with the benchmark
                 writeProfileInstallerSkipFile(packageName, killProcessBlock = killProcessBlock)
                 compileImpl(packageName, killProcessBlock, warmupBlock)
@@ -114,46 +88,6 @@
         }
     }
 
-    // This is a more expensive when compared to `compile --reset`.
-    private fun reinstallPackage(packageName: String) {
-        userspaceTrace("reinstallPackage") {
-            val packagePath = Shell.executeScript("pm path $packageName")
-            // The result looks like: `package: <result>`
-            val apkPath = packagePath.substringAfter("package:").trim()
-            // Copy the APK to /data/local/temp
-            val tempApkPath = "/data/local/tmp/$packageName-${System.currentTimeMillis()}.apk"
-            Log.d(TAG, "Copying APK to $tempApkPath")
-            val result = Shell.executeScriptWithStderr(
-                "cp $apkPath $tempApkPath"
-            )
-            if (result.stderr.isNotBlank()) {
-                Log.w(TAG, "Unable to copy apk ($result)")
-            } else {
-                try {
-                    // Uninstall package
-                    // This is what effectively clears the ART profiles
-                    Log.d(TAG, "Uninstalling $packageName")
-                    var output = Shell.executeScriptWithStderr("pm uninstall $packageName")
-                    check(output.stdout.trim() == "Success") {
-                        "Unable to uninstall $packageName ($result)"
-                    }
-                    // Install the APK from /data/local/tmp
-                    Log.d(TAG, "Installing $packageName")
-                    // Provide a `-t` argument to `pm install` to ensure test packages are
-                    // correctly installed. (b/231294733)
-                    output = Shell.executeScriptWithStderr("pm install -t $tempApkPath")
-                    check(output.stdout.trim() == "Success") {
-                        "Unable to install $packageName ($result)"
-                    }
-                } finally {
-                    // Cleanup the temporary APK
-                    Log.d(TAG, "Deleting $tempApkPath")
-                    Shell.executeCommand("rm $tempApkPath")
-                }
-            }
-        }
-    }
-
     /**
      * Writes a skip file via a [ProfileInstallReceiver] broadcast, so profile installation
      * does not interfere with benchmarks.
@@ -180,9 +114,6 @@
         warmupBlock: () -> Unit
     )
 
-    @RequiresApi(24)
-    internal abstract fun shouldReset(): Boolean
-
     /**
      * No pre-compilation - a compilation profile reset is performed and the entire app will be
      * allowed to Just-In-Time compile as it runs.
@@ -200,10 +131,8 @@
             killProcessBlock: () -> Unit,
             warmupBlock: () -> Unit
         ) {
-            // nothing to do!
+            cmdPackageCompile(packageName, "verify")
         }
-
-        override fun shouldReset(): Boolean = true
     }
 
     /**
@@ -223,8 +152,6 @@
         ) {
             // Do nothing.
         }
-
-        override fun shouldReset(): Boolean = false
     }
 
     /**
@@ -335,8 +262,6 @@
                 cmdPackageCompile(packageName, "speed-profile")
             }
         }
-
-        override fun shouldReset(): Boolean = true
     }
 
     /**
@@ -361,8 +286,6 @@
             }
             // Noop on older versions: apps are fully compiled at install time on API 23 and below
         }
-
-        override fun shouldReset(): Boolean = true
     }
 
     /**
@@ -386,8 +309,6 @@
         ) {
             // Nothing to do - handled externally
         }
-
-        override fun shouldReset(): Boolean = true
     }
 
     companion object {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 5db44a4..ed2982f 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -377,13 +377,14 @@
                 // Run setup before killing process
                 setupBlock(this)
 
+                // Shader caches are stored in the code cache directory. Make sure that
+                // they are cleared every iteration. Must be done before kill, since on user builds
+                // this broadcasts to the target app
+                dropShaderCache()
+
                 // Kill - code below must not wake process!
                 killProcess()
 
-                // Shader caches are stored in the code cache directory. Make sure that
-                // they are cleared every iteration.
-                dropShaderCache()
-
                 // Ensure app's pages are not cached in memory for a true _cold_ start.
                 dropKernelPageCache()
 
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 45a220b..7c21891 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -16,16 +16,19 @@
 
 package androidx.benchmark.macro
 
+import android.content.Context
 import android.content.Intent
 import android.os.Build
 import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.benchmark.DeviceInfo
 import androidx.benchmark.Shell
+import androidx.benchmark.macro.MacrobenchmarkScope.Companion.Api24Helper.shaderDir
 import androidx.benchmark.macro.perfetto.forceTrace
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
 import androidx.tracing.trace
+import java.io.File
 
 /**
  * Provides access to common operations in app automation, such as killing the app,
@@ -225,19 +228,30 @@
     }
 
     /**
-     * Deletes the Shader cache for an application.
+     * Deletes the shader cache for an application.
      *
-     * Enables measurement of shader-cache startup cost during
-     * [cold starts](androidx.benchmark.macro.StartupMode.COLD).
+     * Used by `measureRepeated(startupMode = StartupMode.COLD)` to remove compiled shaders for each
+     * measurement, to ensure their cost is captured each time.
+     *
+     * Requires `profileinstaller` 1.3.0-alpha02 to be used by the target, or a rooted device.
+     *
+     * @throws IllegalStateException if the device is not rooted, and the target app cannot be
+     * signalled to drop its shader cache.
      */
     public fun dropShaderCache() {
         Log.d(TAG, "Dropping shader cache for $packageName")
-        // Shader cache is stored in the codeCacheDirectory
-        // https://source.corp.google.com/android-internal/frameworks/base/core/java/android/app/ActivityThread.java;l=6410
-        val shaderCachePath = instrumentation.targetContext.codeCacheDir.absolutePath
-        val output = Shell.executeScript("rm -rf $shaderCachePath")
-        check(output.isBlank()) {
-            "Unable to drop shader cache for $packageName ($output)"
+        val dropError = ProfileInstallBroadcast.dropShaderCache(packageName)
+        if (dropError != null) {
+            if (Shell.isSessionRooted()) {
+                // fall back to root approach
+                val path = getShaderCachePath(packageName)
+                val output = Shell.executeScriptWithStderr("find $path -type f | xargs rm")
+                check(output.isBlank()) {
+                    "Failed to delete shader cache directory $path, output $output"
+                }
+            } else {
+                throw IllegalStateException(dropError)
+            }
         }
     }
 
@@ -294,4 +308,27 @@
             }
         }
     }
+
+    internal companion object {
+        fun getShaderCachePath(packageName: String): String {
+            val context = InstrumentationRegistry.getInstrumentation().context
+
+            // Shader paths sourced from ActivityThread.java
+            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                context.shaderDir
+            } else {
+                // getCodeCacheDir was added in L, but not used by platform for shaders until M
+                // as M is minApi of this library, that's all we support here
+                context.codeCacheDir
+            }.absolutePath.replace(context.packageName, packageName)
+        }
+
+        @RequiresApi(Build.VERSION_CODES.N)
+        internal object Api24Helper {
+            val Context.shaderDir: File
+                get() =
+                    // shaders started using device protected storage context once it was added in N
+                    createDeviceProtectedStorageContext().codeCacheDir
+        }
+    }
 }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
index 07215af..1e2eb91 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
@@ -171,4 +171,50 @@
             }
         }
     }
+
+    private fun benchmarkOperation(packageName: String, operation: String): String? {
+        Log.d(TAG, "Profile Installer - Benchmark Operation: $operation")
+        // Redefining constants here, because these are only defined in the latest alpha for
+        // ProfileInstaller.
+        // Use an explicit broadcast given the app was force-stopped.
+        val action = "androidx.profileinstaller.action.BENCHMARK_OPERATION"
+        val operationKey = "EXTRA_BENCHMARK_OPERATION"
+        val extras = "$operationKey $operation"
+        val result = Shell.executeCommand(
+            "am broadcast -a $action -e $extras $packageName/$receiverName"
+        )
+            .substringAfter("Broadcast completed: result=")
+            .trim()
+            .toIntOrNull()
+        return when (result) {
+            null, 0, 16 /* BENCHMARK_OPERATION_UNKNOWN */ -> {
+                // 0 is returned by the platform by default, and also if no broadcast receiver
+                // receives the broadcast.
+
+                // NOTE: may need to update this over time for different versions,
+                // based on operation string
+                "The $operation broadcast was not received. " +
+                    "This most likely means that the `androidx.profileinstaller` library " +
+                    "used by the target apk is old. Please use `1.3.0-alpha02` or newer. " +
+                    "For more information refer to the release notes at " +
+                    "https://developer.android.com/jetpack/androidx/releases/profileinstaller."
+            }
+            15 -> { // RESULT_BENCHMARK_OPERATION_FAILURE
+                "The $operation broadcast failed."
+            }
+            14 -> { // RESULT_BENCHMARK_OPERATION_SUCCESS
+                null // success!
+            }
+            else -> {
+                throw RuntimeException(
+                    "unrecognized ProfileInstaller result code: $result"
+                )
+            }
+        }
+    }
+
+    fun dropShaderCache(packageName: String): String? = benchmarkOperation(
+        packageName,
+        "DROP_SHADER_CACHE"
+    )
 }
\ No newline at end of file
diff --git a/benchmark/gradle-plugin/build.gradle b/benchmark/gradle-plugin/build.gradle
index 2ef57d1..9bbf2cc 100644
--- a/benchmark/gradle-plugin/build.gradle
+++ b/benchmark/gradle-plugin/build.gradle
@@ -32,7 +32,6 @@
 
     testImplementation(gradleTestKit())
     testImplementation(project(":internal-testutils-gradle-plugin"))
-    testImplementation(libs.testRunner)
     testImplementation(libs.junit)
     testImplementation(libs.kotlinTest)
 }
@@ -71,4 +70,4 @@
         failOnWarning.set(true)
         enableStricterValidation.set(true)
     }
-}
\ No newline at end of file
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 727552a..15d46bf 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -148,6 +148,10 @@
 
         project.configurations.create("samples")
         project.validateMultiplatformPluginHasNotBeenApplied()
+
+        if (!ProjectLayoutType.isPlayground(project)) {
+            project.whenChangingOutputTextValidationMustInvalidateAllTasks()
+        }
     }
 
     private fun Project.registerProjectOrArtifact() {
@@ -1083,6 +1087,17 @@
     }
 }
 
+// If our output message validation configuration changes, invalidate all tasks to make sure
+// all output messages get regenerated and re-validated
+private fun Project.whenChangingOutputTextValidationMustInvalidateAllTasks() {
+    val configFile = project.rootProject.file("development/build_log_simplifier/messages.ignore")
+    if (configFile.exists()) {
+        project.tasks.configureEach { task ->
+            task.inputs.file(configFile)
+        }
+    }
+}
+
 /**
  * Validates the Maven version against Jetpack guidelines.
  */
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 672f99b..963ed8d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -220,28 +220,14 @@
 
         registerStudioTask()
 
-        if (!ProjectLayoutType.isPlayground(project)) {
-            whenChangingOutputTextValidationMustInvalidateAllTasks()
-        }
-
         TaskUpToDateValidator.setup(project, registry)
 
         project.tasks.register("listTaskOutputs", ListTaskOutputsTask::class.java) { task ->
             task.setOutput(File(project.getDistributionDirectory(), "task_outputs.txt"))
             task.removePrefix(project.getCheckoutRoot().path)
         }
-    }
-
-    // If our output message validation configuration changes, invalidate all tasks to make sure
-    // all output messages get regenerated and re-validated
-    private fun Project.whenChangingOutputTextValidationMustInvalidateAllTasks() {
-        val configFile = project.file("development/build_log_simplifier/messages.ignore")
-        if (configFile.exists()) {
-            subprojects { subproject ->
-                subproject.tasks.configureEach { task ->
-                    task.inputs.file(configFile)
-                }
-            }
+        tasks.matching { it.name == "commonizeNativeDistribution" }.configureEach {
+            it.notCompatibleWithConfigurationCache("https://youtrack.jetbrains.com/issue/KT-54627")
         }
     }
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
index 08aedba..4e9b6b1e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
@@ -22,15 +22,15 @@
 import org.gradle.api.Project
 import org.gradle.api.Task
 import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.TaskAction
-import org.gradle.work.DisableCachingByDefault
 
 /**
  * Finds the outputs of every task and saves this mapping into a file
  */
-@DisableCachingByDefault(because = "Uses too many inputs to be feasible to cache, but runs quickly")
+@CacheableTask
 abstract class ListTaskOutputsTask : DefaultTask() {
     @OutputFile
     val outputFile: Property<File> = project.objects.property(File::class.java)
@@ -39,12 +39,14 @@
     @Input
     val tasks: MutableList<Task> = mutableListOf()
 
+    @get:Input
+    val outputText by lazy { computeOutputText() }
+
     init {
         group = "Help"
-        outputs.upToDateWhen { false }
-        notCompatibleWithConfigurationCache(
-            "This task uses project object to inspect outputs of all tasks"
-        )
+        // compute the output text when the taskgraph is ready so that the output text can be
+        // saved in the configuration cache and not generate a configuration cache violation
+        project.gradle.taskGraph.whenReady({ outputText.toString() })
     }
 
     fun setOutput(f: File) {
@@ -94,14 +96,15 @@
         return components.joinToString("")
     }
 
+    fun computeOutputText(): String {
+        val tasksByOutput = project.rootProject.findAllTasksByOutput()
+        return formatTasks(tasksByOutput)
+    }
+
     @TaskAction
     fun exec() {
-        val tasksByOutput = project.rootProject.findAllTasksByOutput()
-        val text = formatTasks(tasksByOutput)
-
         val outputFile = outputFile.get()
-        outputFile.writeText(text)
-        logger.lifecycle("Wrote ${outputFile.path}")
+        outputFile.writeText(outputText)
     }
 }
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
index 6ad30d2..844ee8b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
@@ -15,7 +15,6 @@
  */
 package androidx.build.license
 
-import androidx.build.enforceKtlintVersion
 import androidx.build.getPrebuiltsRoot
 import java.io.File
 import org.gradle.api.DefaultTask
@@ -115,45 +114,49 @@
 
         task.filesToCheck.from(
             project.provider {
-                val checkerConfig = project.configurations.detachedConfiguration()
-                checkerConfig.isCanBeConsumed = false
-                checkerConfig.attributes {
-                    it.attribute(
-                        Usage.USAGE_ATTRIBUTE,
-                        project.objects.named<Usage>(Usage.JAVA_RUNTIME)
-                    )
-                    it.attribute(
-                        Category.CATEGORY_ATTRIBUTE,
-                        project.objects.named<Category>(Category.LIBRARY)
-                    )
-                    it.attribute(
-                        GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
-                        project.objects.named<GradlePluginApiVersion>(
-                            GradleVersion.current().getVersion()
-                        )
-                    )
-                }
-                // workaround for b/234884534
-                project.enforceKtlintVersion(checkerConfig)
 
-                project
-                    .configurations
-                    .flatMap {
-                        it.allDependencies
-                            .filterIsInstance(ExternalDependency::class.java)
-                            .filterNot {
-                                it.group?.startsWith("com.android") == true
-                            }
-                            .filterNot {
-                                it.group?.startsWith("android.arch") == true
-                            }
-                            .filterNot {
-                                it.group?.startsWith("androidx") == true
-                            }
+                val configName = "CheckExternalLicences"
+                val container = project.configurations
+                val checkerConfig =
+                container.findByName(configName) ?: container.create(configName) { checkerConfig ->
+
+                    checkerConfig.isCanBeConsumed = false
+                    checkerConfig.attributes {
+                        it.attribute(
+                            Usage.USAGE_ATTRIBUTE,
+                            project.objects.named<Usage>(Usage.JAVA_RUNTIME)
+                        )
+                        it.attribute(
+                            Category.CATEGORY_ATTRIBUTE,
+                            project.objects.named<Category>(Category.LIBRARY)
+                        )
+                        it.attribute(
+                            GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
+                            project.objects.named<GradlePluginApiVersion>(
+                                GradleVersion.current().getVersion()
+                            )
+                        )
                     }
-                    .forEach {
-                        checkerConfig.dependencies.add(it)
-                    }
+
+                    project
+                        .configurations
+                        .flatMap {
+                            it.allDependencies
+                                .filterIsInstance(ExternalDependency::class.java)
+                                .filterNot {
+                                    it.group?.startsWith("com.android") == true
+                                }
+                                .filterNot {
+                                    it.group?.startsWith("android.arch") == true
+                                }
+                                .filterNot {
+                                    it.group?.startsWith("androidx") == true
+                                }
+                        }
+                        .forEach {
+                            checkerConfig.dependencies.add(it)
+                        }
+                }
 
                 val localArtifactRepositories = project.findLocalMavenRepositories()
                 val dependencyArtifacts = checkerConfig.incoming.artifacts.artifacts.mapNotNull {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
index ce578e5..dfb282f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
@@ -24,11 +24,13 @@
 import androidx.build.getSdkPath
 import androidx.build.getSupportRootFolder
 import com.android.build.gradle.BaseExtension
+import javax.inject.Inject
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
 import org.gradle.api.artifacts.type.ArtifactTypeDefinition.JAR_TYPE
 import org.gradle.api.file.FileCollection
+import org.gradle.api.file.FileSystemOperations
 import org.gradle.api.tasks.PathSensitivity
 import org.gradle.api.tasks.testing.Test
 import org.gradle.kotlin.dsl.the
@@ -38,7 +40,9 @@
 /**
  * Configures screenshot testing using Paparazzi for AndroidX projects.
  */
-class AndroidXPaparazziImplPlugin : Plugin<Project> {
+class AndroidXPaparazziImplPlugin @Inject constructor(
+    private val fileSystemOperations: FileSystemOperations
+) : Plugin<Project> {
     override fun apply(project: Project) {
         val paparazziNative = project.createUnzippedPaparazziNativeDependency()
         project.afterEvaluate {
@@ -53,6 +57,7 @@
     private fun Test.configureTestTask(paparazziNative: FileCollection) {
         val platformDirectory = project.getSdkPath().resolve("platforms/$COMPILE_SDK_VERSION")
         val goldenRootDirectory = project.getSupportRootFolder().resolve("../../golden")
+        val reportDirectory = project.buildDir.resolve("paparazzi").resolve(name)
         val modulePath = project.path.replace(':', '/').trim('/')
         val android = project.the<BaseExtension>()
         val packageName = requireNotNull(android.namespace) {
@@ -69,6 +74,13 @@
             .withPathSensitivity(PathSensitivity.RELATIVE)
             .withPropertyName("goldenDirectory")
 
+        // Mark report directory as an output directory
+        outputs.dir(reportDirectory)
+            .withPropertyName("paparazziReportDir")
+
+        // Clean the contents of the report directory before each test run
+        doFirst { fileSystemOperations.delete { it.delete(reportDirectory.listFiles()) } }
+
         // Set non-path system properties at configuration time, so that changes invalidate caching
         prefixedSystemProperties(
             "gradlePluginApplied" to "true",
@@ -82,10 +94,10 @@
         doFirst {
             systemProperty("paparazzi.platform.data.root", paparazziNative.singleFile.canonicalPath)
             prefixedSystemProperties(
-                "platformDir" to platformDirectory,
+                "platformDir" to platformDirectory.canonicalPath,
                 "assetsDir" to ".", // TODO: Merged assets dirs? (needed for compose?)
                 "resDir" to ".", // TODO: Merged resource dirs? (needed for compose?)
-                "reportDir" to reports.junitXml.outputLocation.get().asFile.canonicalPath,
+                "reportDir" to reportDirectory.canonicalPath,
                 "goldenRootDir" to goldenRootDirectory.canonicalPath,
             )
         }
diff --git a/busytown/androidx_host_tests.sh b/busytown/androidx_host_tests.sh
index bba9e74..00ba171 100755
--- a/busytown/androidx_host_tests.sh
+++ b/busytown/androidx_host_tests.sh
@@ -5,9 +5,10 @@
 
 cd "$(dirname $0)"
 
-impl/build.sh test \
+impl/build.sh test linuxX64Test \
     -Pandroidx.ignoreTestFailures \
     -Pandroidx.displayTestOutput=false \
+    -Pandroidx.enabled.kmp.target.platforms=+native \
     "$@"
 
 echo "Completing $0 at $(date)"
diff --git a/busytown/androidx_host_tests_max_dep_versions.sh b/busytown/androidx_host_tests_max_dep_versions.sh
index 75d276e..a56dbc4 100755
--- a/busytown/androidx_host_tests_max_dep_versions.sh
+++ b/busytown/androidx_host_tests_max_dep_versions.sh
@@ -5,8 +5,9 @@
 
 cd "$(dirname $0)"
 
-impl/build.sh test -Pandroidx.useMaxDepVersions \
+impl/build.sh test linuxX64Test -Pandroidx.useMaxDepVersions \
     -Pandroidx.displayTestOutput=false \
+    -Pandroidx.enabled.kmp.target.platforms=+native \
     -Pandroidx.ignoreTestFailures "$@"
 
 echo "Completing $0 at $(date)"
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
index 46a29ef9..65e72dc 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
@@ -208,8 +208,7 @@
 
         // Assert, verify the usage count of the DeferrableSurface
         assertThat(cameraOpenedUsageCount).isEqualTo(2)
-        // TODO(lnishan): After aosp/2241588, cameraDisconnectedUsageCount should remain at 2
-        assertThat(cameraDisconnectedUsageCount).isGreaterThan(0)
+        assertThat(cameraDisconnectedUsageCount).isEqualTo(2)
         assertThat(cameraClosedUsageCount).isEqualTo(1)
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
index 3043278..b4bcd62 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
 import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
 import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraConfigs
 import androidx.camera.core.impl.CameraControlInternal
 import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.impl.CameraInternal
@@ -50,6 +51,8 @@
     private val threads: UseCaseThreads,
 ) : CameraInternal {
     private val cameraId = config.cameraId
+    private var coreCameraConfig: androidx.camera.core.impl.CameraConfig =
+        CameraConfigs.emptyConfig()
     private val debugId = cameraAdapterIds.incrementAndGet()
     private val cameraState = LiveDataObservable<CameraInternal.State>()
 
@@ -104,5 +107,13 @@
         useCaseManager.deactivate(useCase)
     }
 
+    override fun getExtendedConfig(): androidx.camera.core.impl.CameraConfig {
+        return coreCameraConfig
+    }
+
+    override fun setExtendedConfig(cameraConfig: androidx.camera.core.impl.CameraConfig?) {
+        coreCameraConfig = cameraConfig ?: CameraConfigs.emptyConfig()
+    }
+
     override fun toString(): String = "CameraInternalAdapter<$cameraId>"
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
index 42550a1..1c6c3c2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
@@ -18,8 +18,10 @@
 
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.GraphState
 import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.StreamId
+import kotlinx.coroutines.flow.StateFlow
 
 class FakeCameraGraph(
     val fakeCameraGraphSession: FakeCameraGraphSession = FakeCameraGraphSession()
@@ -30,6 +32,9 @@
     override val streams: StreamGraph
         get() = throw NotImplementedError("Not used in testing")
 
+    override val graphState: StateFlow<GraphState>
+        get() = throw NotImplementedError("Not used in testing")
+
     override suspend fun acquireSession(): CameraGraph.Session {
         return fakeCameraGraphSession
     }
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 6c03ad4..740146a 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
@@ -28,6 +28,32 @@
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
 import java.io.Closeable
 import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.flow.StateFlow
+
+abstract class GraphState internal constructor() {
+    /**
+     * When the [CameraGraph] is starting. This means we're in the process of opening a (virtual)
+     * camera and creating a capture session.
+     */
+    object GraphStateStarting : GraphState()
+
+    /**
+     * When the [CameraGraph] is started. This means a capture session has been successfully created
+     * for the [CameraGraph].
+     */
+    object GraphStateStarted : GraphState()
+
+    /**
+     * When the [CameraGraph] is stopping. This means we're in the process of stopping the graph.
+     */
+    object GraphStateStopping : GraphState()
+
+    /**
+     * When the [CameraGraph] hasn't been started, or stopped. This does not guarantee the closure
+     * of the capture session or the camera device itself.
+     */
+    object GraphStateStopped : GraphState()
+}
 
 /**
  * A [CameraGraph] represents the combined configuration and state of a camera.
@@ -37,6 +63,12 @@
     public val streams: StreamGraph
 
     /**
+     * Returns the state flow of [GraphState], which emits the current state of the
+     * [CameraGraph], including when a [CameraGraph] is stopped, starting or started.
+     */
+    public val graphState: StateFlow<GraphState>
+
+    /**
      * This will cause the [CameraGraph] to start opening the [CameraDevice] and configuring a
      * [CameraCaptureSession]. While the CameraGraph is alive it will attempt to keep the camera
      * open, active, and in a configured running state.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
index 44e6b3b..69f3df0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
@@ -78,10 +78,20 @@
                 camera: CameraId? = null,
                 outputType: OutputStream.OutputType = OutputStream.OutputType.SURFACE,
                 mirrorMode: OutputStream.MirrorMode? = null,
-                timestampBase: OutputStream.TimestampBase? = null
+                timestampBase: OutputStream.TimestampBase? = null,
+                dynamicRangeProfile: OutputStream.DynamicRangeProfile? = null,
+                streamUseCase: OutputStream.StreamUseCase? = null,
             ): Config = create(
-                OutputStream.Config
-                    .create(size, format, camera, outputType, mirrorMode, timestampBase)
+                OutputStream.Config.create(
+                    size,
+                    format,
+                    camera,
+                    outputType,
+                    mirrorMode,
+                    timestampBase,
+                    dynamicRangeProfile,
+                    streamUseCase,
+                )
             )
 
             /**
@@ -124,6 +134,8 @@
     public val camera: CameraId
     public val mirrorMode: MirrorMode?
     public val timestampBase: TimestampBase?
+    public val dynamicRangeProfile: DynamicRangeProfile?
+    public val streamUseCase: StreamUseCase?
     // TODO: Consider adding sensor mode and/or other metadata
 
     /**
@@ -135,6 +147,8 @@
         public val camera: CameraId?,
         public val mirrorMode: MirrorMode?,
         public val timestampBase: TimestampBase?,
+        public val dynamicRangeProfile: DynamicRangeProfile?,
+        public val streamUseCase: StreamUseCase?,
     ) {
         companion object {
             fun create(
@@ -144,15 +158,34 @@
                 outputType: OutputType = OutputType.SURFACE,
                 mirrorMode: MirrorMode? = null,
                 timestampBase: TimestampBase? = null,
+                dynamicRangeProfile: DynamicRangeProfile? = null,
+                streamUseCase: StreamUseCase? = null,
             ): Config =
                 if (
                     outputType == OutputType.SURFACE_TEXTURE ||
                     outputType == OutputType.SURFACE_VIEW
                 ) {
-                    LazyOutputConfig(size, format, camera, outputType, mirrorMode, timestampBase)
+                    LazyOutputConfig(
+                        size,
+                        format,
+                        camera,
+                        outputType,
+                        mirrorMode,
+                        timestampBase,
+                        dynamicRangeProfile,
+                        streamUseCase
+                    )
                 } else {
                     check(outputType == OutputType.SURFACE)
-                    SimpleOutputConfig(size, format, camera, mirrorMode, timestampBase)
+                    SimpleOutputConfig(
+                        size,
+                        format,
+                        camera,
+                        mirrorMode,
+                        timestampBase,
+                        dynamicRangeProfile,
+                        streamUseCase,
+                    )
                 }
 
             /** Create a stream configuration from an externally created [OutputConfiguration] */
@@ -176,7 +209,17 @@
             camera: CameraId?,
             mirrorMode: MirrorMode?,
             timestampBase: TimestampBase?,
-        ) : Config(size, format, camera, mirrorMode, timestampBase)
+            dynamicRangeProfile: DynamicRangeProfile?,
+            streamUseCase: StreamUseCase?,
+        ) : Config(
+                size,
+                format,
+                camera,
+                mirrorMode,
+                timestampBase,
+                dynamicRangeProfile,
+                streamUseCase
+            )
 
         /**
          * Used to configure an output with a surface that may be provided after the camera is running.
@@ -193,7 +236,17 @@
             internal val outputType: OutputType,
             mirrorMode: MirrorMode?,
             timestampBase: TimestampBase?,
-        ) : Config(size, format, camera, mirrorMode, timestampBase)
+            dynamicRangeProfile: DynamicRangeProfile?,
+            streamUseCase: StreamUseCase?,
+        ) : Config(
+                size,
+                format,
+                camera,
+                mirrorMode,
+                timestampBase,
+                dynamicRangeProfile,
+                streamUseCase,
+            )
 
         /**
          * Used to define an output that comes from an externally managed OutputConfiguration object.
@@ -209,8 +262,15 @@
             format: StreamFormat,
             camera: CameraId?,
             val output: OutputConfiguration,
-        ) : Config(size, format, camera, MirrorMode(Api33Compat.getMirrorMode(output)),
-            TimestampBase(Api33Compat.getTimestampBase(output)))
+        ) : Config(
+                size,
+                format,
+                camera,
+                MirrorMode(Api33Compat.getMirrorMode(output)),
+                TimestampBase(Api33Compat.getTimestampBase(output)),
+                DynamicRangeProfile(Api33Compat.getDynamicRangeProfile(output)),
+                StreamUseCase(Api33Compat.getStreamUseCase(output)),
+            )
     }
 
     enum class OutputType {
@@ -254,6 +314,52 @@
             val TIMESTAMP_BASE_CHOREOGRAPHER_SYNCED = TimestampBase(4)
         }
     }
+
+    /**
+     * Adds the ability to define the dynamic range profile of the OutputStream.
+     * [STANDARD] is the default dynamic range profile for the camera device, with which
+     * the camera device uses an 8-bit standard profile.
+     *
+     * See the documentation on [OutputConfiguration.setDynamicRangeProfile] for more details.
+     */
+    @JvmInline
+    value class DynamicRangeProfile(val value: Long) {
+        companion object {
+            val STANDARD = DynamicRangeProfile(1)
+            val HLG10 = DynamicRangeProfile(2)
+            val HDR10 = DynamicRangeProfile(4)
+            val HDR10_PLUS = DynamicRangeProfile(8)
+            val DOLBY_VISION_10B_HDR_REF = DynamicRangeProfile(16)
+            val DOLBY_VISION_10B_HDR_REF_PO = DynamicRangeProfile(32)
+            val DOLBY_VISION_10B_HDR_OEM = DynamicRangeProfile(64)
+            val DOLBY_VISION_10B_HDR_OEM_PO = DynamicRangeProfile(128)
+            val DOLBY_VISION_8B_HDR_REF = DynamicRangeProfile(256)
+            val DOLBY_VISION_8B_HDR_REF_PO = DynamicRangeProfile(512)
+            val DOLBY_VISION_8B_HDR_OEM = DynamicRangeProfile(1024)
+            val DOLBY_VISION_8B_HDR_OEM_PO = DynamicRangeProfile(2048)
+            val PUBLIC_MAX = DynamicRangeProfile(4096)
+        }
+    }
+
+    /**
+     * Adds the ability to define the stream specific use case of the OutputStream.
+     * [DEFAULT] is the default stream use case, with which
+     * the camera device uses the properties of the output target, such as format, dataSpace,
+     * or surface class type, to optimize the image processing pipeline.
+     *
+     * See the documentation on [OutputConfiguration.setStreamUseCase] for more details.
+     */
+    @JvmInline
+    value class StreamUseCase(val value: Long) {
+        companion object {
+            val DEFAULT = StreamUseCase(0)
+            val PREVIEW = StreamUseCase(1)
+            val STILL_CAPTURE = StreamUseCase(2)
+            val VIDEO_RECORD = StreamUseCase(3)
+            val PREVIEW_VIDEO_STILL = StreamUseCase(4)
+            val VIDEO_CALL = StreamUseCase(5)
+        }
+    }
 }
 
 /**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index 7ae56e8..8d8bec7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -308,6 +308,18 @@
 internal object Api33Compat {
     @JvmStatic
     @DoNotInline
+    fun setDynamicRangeProfile(outputConfig: OutputConfiguration, dynamicRangeProfile: Long) {
+        outputConfig.dynamicRangeProfile = dynamicRangeProfile
+    }
+
+    @JvmStatic
+    @DoNotInline
+    fun getDynamicRangeProfile(outputConfig: OutputConfiguration): Long {
+        return outputConfig.dynamicRangeProfile
+    }
+
+    @JvmStatic
+    @DoNotInline
     fun setMirrorMode(outputConfig: OutputConfiguration, mirrorMode: Int) {
         outputConfig.mirrorMode = mirrorMode
     }
@@ -317,6 +329,19 @@
     fun getMirrorMode(outputConfig: OutputConfiguration): Int {
         return outputConfig.mirrorMode
     }
+
+    @JvmStatic
+    @DoNotInline
+    fun setStreamUseCase(outputConfig: OutputConfiguration, streamUseCase: Long) {
+        outputConfig.streamUseCase = streamUseCase
+    }
+
+    @JvmStatic
+    @DoNotInline
+    fun getStreamUseCase(outputConfig: OutputConfiguration): Long {
+        return outputConfig.streamUseCase
+    }
+
     @JvmStatic
     @DoNotInline
     fun setTimestampBase(outputConfig: OutputConfiguration, timestampBase: Int) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
index f781b609..6d4b953 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
@@ -325,6 +325,8 @@
                 outputType = outputConfig.deferredOutputType!!,
                 mirrorMode = outputConfig.mirrorMode,
                 timestampBase = outputConfig.timestampBase,
+                dynamicRangeProfile = outputConfig.dynamicRangeProfile,
+                streamUseCase = outputConfig.streamUseCase,
                 surfaceSharing = outputConfig.surfaceSharing,
                 surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
                 physicalCameraId = if (outputConfig.camera != graphConfig.camera) {
@@ -354,6 +356,8 @@
             outputSurfaces.first(),
             mirrorMode = outputConfig.mirrorMode,
             timestampBase = outputConfig.timestampBase,
+            dynamicRangeProfile = outputConfig.dynamicRangeProfile,
+            streamUseCase = outputConfig.streamUseCase,
             size = outputConfig.size,
             surfaceSharing = outputConfig.surfaceSharing,
             surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
index c7243bd..3079060 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
@@ -26,8 +26,10 @@
 import android.view.SurfaceHolder
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.OutputStream.OutputType
+import androidx.camera.camera2.pipe.OutputStream.DynamicRangeProfile
 import androidx.camera.camera2.pipe.OutputStream.MirrorMode
+import androidx.camera.camera2.pipe.OutputStream.OutputType
+import androidx.camera.camera2.pipe.OutputStream.StreamUseCase
 import androidx.camera.camera2.pipe.OutputStream.TimestampBase
 import androidx.camera.camera2.pipe.UnsafeWrapper
 import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
@@ -139,6 +141,8 @@
             outputType: OutputType = OutputType.SURFACE,
             mirrorMode: MirrorMode? = null,
             timestampBase: TimestampBase? = null,
+            dynamicRangeProfile: DynamicRangeProfile? = null,
+            streamUseCase: StreamUseCase? = null,
             size: Size? = null,
             surfaceSharing: Boolean = false,
             surfaceGroupId: Int = SURFACE_GROUP_ID_NONE,
@@ -224,6 +228,26 @@
                 }
             }
 
+            if (dynamicRangeProfile != null) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                    Api33Compat.setDynamicRangeProfile(configuration, dynamicRangeProfile.value)
+                } else {
+                    if (dynamicRangeProfile != DynamicRangeProfile.STANDARD) {
+                        Log.warn {
+                            "Cannot set dynamicRangeProfile to a non-default value on API " +
+                                "${Build.VERSION.SDK_INT}. This may result in unexpected " +
+                                "behavior. Requested $dynamicRangeProfile"
+                        }
+                    }
+                }
+            }
+
+            if (streamUseCase != null) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                    Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
+                }
+            }
+
             // Create and return the Android
             return AndroidOutputConfiguration(
                 configuration,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
index 3a6eee4..76f5753 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
@@ -21,6 +21,7 @@
 import androidx.camera.camera2.pipe.CameraController
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.GraphState
 import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.config.CameraGraphScope
@@ -31,6 +32,7 @@
 import androidx.camera.camera2.pipe.core.acquireOrNull
 import javax.inject.Inject
 import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.flow.StateFlow
 
 internal val cameraGraphIds = atomic(0)
 
@@ -40,6 +42,7 @@
     graphConfig: CameraGraph.Config,
     metadata: CameraMetadata,
     private val graphProcessor: GraphProcessor,
+    private val graphListener: GraphListener,
     private val streamGraph: StreamGraphImpl,
     private val surfaceGraph: SurfaceGraph,
     private val cameraController: CameraController,
@@ -63,9 +66,13 @@
     override val streams: StreamGraph
         get() = streamGraph
 
+    override val graphState: StateFlow<GraphState>
+        get() = graphProcessor.graphState
+
     override fun start() {
         Debug.traceStart { "$this#start" }
         Log.info { "Starting $this" }
+        graphListener.onGraphStarting()
         cameraController.start()
         Debug.traceStop()
     }
@@ -73,6 +80,7 @@
     override fun stop() {
         Debug.traceStart { "$this#stop" }
         Log.info { "Stopping $this" }
+        graphListener.onGraphStopping()
         cameraController.stop()
         Debug.traceStop()
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
index 598657f..89ea72b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
@@ -17,16 +17,29 @@
 package androidx.camera.camera2.pipe.graph
 
 import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraGraph
 
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 interface GraphListener {
     /**
+     * Used to indicate that the graph is starting. This is called immediately when a [CameraGraph]
+     * is being started.
+     */
+    fun onGraphStarting() {}
+
+    /**
      * Used to indicate that the graph has been initialized and is ready to actively process
      * requests using the provided [GraphRequestProcessor] interface.
      */
     fun onGraphStarted(requestProcessor: GraphRequestProcessor)
 
     /**
+     * Used to indicate that the graph is stopping. This is called immediately when a [CameraGraph]
+     * is being stopped.
+     */
+    fun onGraphStopping() {}
+
+    /**
      * Used to indicate that a previously initialized [GraphRequestProcessor] is no longer
      * available.
      */
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index 598e8bf..4b23fbc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -21,6 +21,10 @@
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.GraphState
+import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
+import androidx.camera.camera2.pipe.GraphState.GraphStateStarting
+import androidx.camera.camera2.pipe.GraphState.GraphStateStarted
 import androidx.camera.camera2.pipe.CaptureSequenceProcessor
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.config.CameraGraphScope
@@ -35,6 +39,8 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 
 /**
  * The [GraphProcessor] is responsible for queuing and then submitting them to a
@@ -42,6 +48,8 @@
  * and submitted before the camera is available.
  */
 internal interface GraphProcessor {
+    val graphState: StateFlow<GraphState>
+
     fun submit(request: Request)
     fun submit(requests: List<Request>)
     suspend fun submit(parameters: Map<*, Any?>): Boolean
@@ -102,7 +110,19 @@
     @GuardedBy("lock")
     private var closed = false
 
+    private val _graphState = MutableStateFlow<GraphState>(GraphStateStopped)
+
+    override val graphState: StateFlow<GraphState>
+        get() = _graphState
+
+    override fun onGraphStarting() {
+        debug { "$this onGraphStarting" }
+        _graphState.value = GraphStateStarting
+    }
+
     override fun onGraphStarted(requestProcessor: GraphRequestProcessor) {
+        debug { "$this onGraphStarted" }
+        _graphState.value = GraphStateStarted
         var old: GraphRequestProcessor? = null
         synchronized(lock) {
             if (closed) {
@@ -126,6 +146,8 @@
     }
 
     override fun onGraphStopped(requestProcessor: GraphRequestProcessor) {
+        debug { "$this onGraphStopped" }
+        _graphState.value = GraphStateStopped
         var old: GraphRequestProcessor? = null
         synchronized(lock) {
             if (closed) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
index dc857da..d03ed3b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
@@ -121,6 +121,8 @@
                     },
                     mirrorMode = output.mirrorMode,
                     timestampBase = output.timestampBase,
+                    dynamicRangeProfile = output.dynamicRangeProfile,
+                    streamUseCase = output.streamUseCase,
                     externalOutputConfig =
                     (output as? OutputStream.Config.ExternalOutputConfig)?.output
                 )
@@ -143,7 +145,9 @@
                     outputConfig.format,
                     outputConfig.camera,
                     outputConfig.mirrorMode,
-                    outputConfig.timestampBase
+                    outputConfig.timestampBase,
+                    outputConfig.dynamicRangeProfile,
+                    outputConfig.streamUseCase,
                 )
                 outputStream
             }
@@ -181,6 +185,8 @@
         val deferredOutputType: OutputStream.OutputType?,
         val mirrorMode: OutputStream.MirrorMode?,
         val timestampBase: OutputStream.TimestampBase?,
+        val dynamicRangeProfile: OutputStream.DynamicRangeProfile?,
+        val streamUseCase: OutputStream.StreamUseCase?,
     ) {
         internal val streamBuilder = mutableListOf<CameraStream>()
         val streams: List<CameraStream>
@@ -199,6 +205,8 @@
         override val camera: CameraId,
         override val mirrorMode: OutputStream.MirrorMode?,
         override val timestampBase: OutputStream.TimestampBase?,
+        override val dynamicRangeProfile: OutputStream.DynamicRangeProfile?,
+        override val streamUseCase: OutputStream.StreamUseCase?,
     ) : OutputStream {
         override lateinit var stream: CameraStream
         override fun toString(): String = id.toString()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
index 23e8bd6..5b1200b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
@@ -118,6 +118,7 @@
             graphConfig,
             metadata,
             fakeGraphProcessor,
+            fakeGraphProcessor,
             streamGraph,
             surfaceGraph,
             cameraController,
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
index 2ceaefc..83ecdba 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
@@ -41,9 +41,9 @@
     fun testPrecomputedTestData() {
         val streamGraph = StreamGraphImpl(config.fakeMetadata, config.graphConfig)
 
-        assertThat(streamGraph.streams).hasSize(7)
-        assertThat(streamGraph.streams).hasSize(7)
-        assertThat(streamGraph.outputConfigs).hasSize(6)
+        assertThat(streamGraph.streams).hasSize(9)
+        assertThat(streamGraph.streams).hasSize(9)
+        assertThat(streamGraph.outputConfigs).hasSize(8)
 
         val stream1 = streamGraph[config.streamConfig1]!!
         val outputStream1 = stream1.outputs.single()
@@ -52,6 +52,8 @@
         assertThat(outputStream1.size.height).isEqualTo(100)
         assertThat(outputStream1.mirrorMode).isNull()
         assertThat(outputStream1.timestampBase).isNull()
+        assertThat(outputStream1.dynamicRangeProfile).isNull()
+        assertThat(outputStream1.streamUseCase).isNull()
 
         val stream2 = streamGraph[config.streamConfig2]!!
         val outputStream2 = stream2.outputs.single()
@@ -61,6 +63,8 @@
         assertThat(outputStream2.size.height).isEqualTo(321)
         assertThat(outputStream2.mirrorMode).isNull()
         assertThat(outputStream2.timestampBase).isNull()
+        assertThat(outputStream2.dynamicRangeProfile).isNull()
+        assertThat(outputStream2.streamUseCase).isNull()
     }
 
     @Test
@@ -202,4 +206,26 @@
         assertThat(stream2.outputs.single().timestampBase)
             .isEqualTo(OutputStream.TimestampBase.TIMESTAMP_BASE_MONOTONIC)
     }
+
+    @Test
+    fun testDefaultAndPropagatedDynamicRangeProfiles() {
+        val streamGraph = StreamGraphImpl(config.fakeMetadata, config.graphConfig)
+        val stream1 = streamGraph[config.streamConfig1]!!
+        assertThat(stream1.outputs.single().dynamicRangeProfile).isNull()
+
+        val stream2 = streamGraph[config.streamConfig6]!!
+        assertThat(stream2.outputs.single().dynamicRangeProfile)
+            .isEqualTo(OutputStream.DynamicRangeProfile.PUBLIC_MAX)
+    }
+
+    @Test
+    fun testDefaultAndPropagatedStreamUseCases() {
+        val streamGraph = StreamGraphImpl(config.fakeMetadata, config.graphConfig)
+        val stream1 = streamGraph[config.streamConfig1]!!
+        assertThat(stream1.outputs.single().streamUseCase).isNull()
+
+        val stream2 = streamGraph[config.streamConfig7]!!
+        assertThat(stream2.outputs.single().streamUseCase)
+            .isEqualTo(OutputStream.StreamUseCase.VIDEO_RECORD)
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
index f5ed8ec..57809dd 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
@@ -55,8 +55,10 @@
     private val stream3 = streamMap[config.streamConfig3]!!
     private val stream4 = streamMap[config.streamConfig4]!!
     private val stream5 = streamMap[config.streamConfig5]!!
-    private val stream6 = streamMap[config.sharedStreamConfig1]!!
-    private val stream7 = streamMap[config.sharedStreamConfig2]!!
+    private val stream6 = streamMap[config.streamConfig6]!!
+    private val stream7 = streamMap[config.streamConfig7]!!
+    private val stream8 = streamMap[config.sharedStreamConfig1]!!
+    private val stream9 = streamMap[config.sharedStreamConfig2]!!
 
     private val fakeSurface1 = Surface(SurfaceTexture(1))
     private val fakeSurface2 = Surface(SurfaceTexture(2))
@@ -65,6 +67,8 @@
     private val fakeSurface5 = Surface(SurfaceTexture(5))
     private val fakeSurface6 = Surface(SurfaceTexture(6))
     private val fakeSurface7 = Surface(SurfaceTexture(7))
+    private val fakeSurface8 = Surface(SurfaceTexture(8))
+    private val fakeSurface9 = Surface(SurfaceTexture(9))
 
     @After
     fun teardown() {
@@ -75,6 +79,8 @@
         fakeSurface5.release()
         fakeSurface6.release()
         fakeSurface7.release()
+        fakeSurface8.release()
+        fakeSurface9.release()
     }
 
     @Test
@@ -86,6 +92,8 @@
         surfaceGraph[stream5.id] = fakeSurface5
         surfaceGraph[stream6.id] = fakeSurface6
         surfaceGraph[stream7.id] = fakeSurface7
+        surfaceGraph[stream8.id] = fakeSurface8
+        surfaceGraph[stream9.id] = fakeSurface9
 
         assertThat(controller.surfaceMap).isNotNull()
         assertThat(controller.surfaceMap?.get(stream1.id)).isEqualTo(fakeSurface1)
@@ -93,6 +101,8 @@
         assertThat(controller.surfaceMap?.get(stream3.id)).isEqualTo(fakeSurface3)
         assertThat(controller.surfaceMap?.get(stream4.id)).isEqualTo(fakeSurface4)
         assertThat(controller.surfaceMap?.get(stream5.id)).isEqualTo(fakeSurface5)
+        assertThat(controller.surfaceMap?.get(stream6.id)).isEqualTo(fakeSurface6)
+        assertThat(controller.surfaceMap?.get(stream7.id)).isEqualTo(fakeSurface7)
     }
 
     @Test
@@ -104,10 +114,12 @@
         surfaceGraph[stream3.id] = fakeSurface3
         surfaceGraph[stream4.id] = fakeSurface4
         surfaceGraph[stream5.id] = fakeSurface5
-        assertThat(controller.surfaceMap).isNull()
-
         surfaceGraph[stream6.id] = fakeSurface6
         surfaceGraph[stream7.id] = fakeSurface7
+        assertThat(controller.surfaceMap).isNull()
+
+        surfaceGraph[stream8.id] = fakeSurface8
+        surfaceGraph[stream9.id] = fakeSurface9
 
         assertThat(controller.surfaceMap).isNotNull()
         assertThat(controller.surfaceMap?.get(stream1.id)).isEqualTo(fakeSurface1)
@@ -117,6 +129,8 @@
         assertThat(controller.surfaceMap?.get(stream5.id)).isEqualTo(fakeSurface5)
         assertThat(controller.surfaceMap?.get(stream6.id)).isEqualTo(fakeSurface6)
         assertThat(controller.surfaceMap?.get(stream7.id)).isEqualTo(fakeSurface7)
+        assertThat(controller.surfaceMap?.get(stream8.id)).isEqualTo(fakeSurface8)
+        assertThat(controller.surfaceMap?.get(stream9.id)).isEqualTo(fakeSurface9)
     }
 
     @Test
@@ -134,6 +148,8 @@
         surfaceGraph[stream5.id] = fakeSurface5
         surfaceGraph[stream6.id] = fakeSurface6
         surfaceGraph[stream7.id] = fakeSurface7
+        surfaceGraph[stream8.id] = fakeSurface8
+        surfaceGraph[stream9.id] = fakeSurface9
 
         assertThat(controller.surfaceMap).isNotNull()
         assertThat(controller.surfaceMap?.get(stream1.id)).isEqualTo(fakeSurface1B)
@@ -143,6 +159,8 @@
         assertThat(controller.surfaceMap?.get(stream5.id)).isEqualTo(fakeSurface5)
         assertThat(controller.surfaceMap?.get(stream6.id)).isEqualTo(fakeSurface6)
         assertThat(controller.surfaceMap?.get(stream7.id)).isEqualTo(fakeSurface7)
+        assertThat(controller.surfaceMap?.get(stream8.id)).isEqualTo(fakeSurface8)
+        assertThat(controller.surfaceMap?.get(stream9.id)).isEqualTo(fakeSurface9)
 
         fakeSurface1A.release()
         fakeSurface1B.release()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
index fce96de..862313f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
@@ -88,6 +88,27 @@
         mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
         timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_MONOTONIC
     )
+    val streamConfig6 = CameraStream.Config.create(
+        size = Size(200, 200),
+        format = StreamFormat.YUV_420_888,
+        camera = camera2,
+        outputType = OutputStream.OutputType.SURFACE_TEXTURE,
+        mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
+        timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_DEFAULT,
+        dynamicRangeProfile = OutputStream.DynamicRangeProfile.PUBLIC_MAX
+    )
+
+    val streamConfig7 = CameraStream.Config.create(
+        size = Size(200, 200),
+        format = StreamFormat.YUV_420_888,
+        camera = camera2,
+        outputType = OutputStream.OutputType.SURFACE_TEXTURE,
+        mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
+        timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_DEFAULT,
+        dynamicRangeProfile = OutputStream.DynamicRangeProfile.STANDARD,
+        streamUseCase = OutputStream.StreamUseCase.VIDEO_RECORD
+    )
+
     val sharedOutputConfig = OutputStream.Config.create(
         size = Size(200, 200),
         format = StreamFormat.YUV_420_888,
@@ -104,6 +125,8 @@
             streamConfig3,
             streamConfig4,
             streamConfig5,
+            streamConfig6,
+            streamConfig7,
             sharedStreamConfig1,
             sharedStreamConfig2
         ),
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 1c9b2d0..725316b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -16,12 +16,15 @@
 
 package androidx.camera.camera2.pipe.testing
 
+import androidx.camera.camera2.pipe.GraphState
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.graph.GraphListener
 import androidx.camera.camera2.pipe.graph.GraphProcessor
 import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
 import androidx.camera.camera2.pipe.graph.GraphState3A
 import androidx.camera.camera2.pipe.putAllMetadata
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 
 /**
  * Fake implementation of a [GraphProcessor] for tests.
@@ -43,6 +46,11 @@
     private val _requestQueue = mutableListOf<List<Request>>()
     private var processor: GraphRequestProcessor? = null
 
+    private val _graphState = MutableStateFlow<GraphState>(GraphState.GraphStateStopped)
+
+    override val graphState: StateFlow<GraphState>
+        get() = _graphState
+
     override fun startRepeating(request: Request) {
         repeatingRequest = request
     }
@@ -58,6 +66,7 @@
     override fun submit(requests: List<Request>) {
         _requestQueue.add(requests)
     }
+
     override suspend fun submit(parameters: Map<*, Any?>): Boolean {
         if (closed) {
             return false
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 325da4f..6ebf92f 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -69,6 +69,9 @@
 import androidx.camera.testing.fakes.FakeCameraInfoInternal;
 import androidx.camera.testing.fakes.FakeUseCase;
 import androidx.camera.testing.fakes.FakeUseCaseConfig;
+import androidx.camera.testing.mocks.MockObserver;
+import androidx.camera.testing.mocks.helpers.CallTimes;
+import androidx.camera.testing.mocks.helpers.CallTimesAtLeast;
 import androidx.core.os.HandlerCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -91,12 +94,9 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Semaphore;
@@ -534,10 +534,11 @@
 
     @Test
     public void cameraTransitionsThroughPendingState_whenNoCamerasAvailable() {
-        CameraInternalStateMockObserver mockObserver = new CameraInternalStateMockObserver();
+        MockObserver<CameraInternal.State> mockObserver = new MockObserver<>();
 
         // Ensure real camera can't open due to max cameras being open
         Camera mockCamera = mock(Camera.class);
+
         mCameraStateRegistry.registerCamera(mockCamera, CameraXExecutors.directExecutor(),
                 () -> {
                 });
@@ -550,7 +551,7 @@
 
         // Ensure that the camera gets to a PENDING_OPEN state
         mockObserver.verifyOnNewDataCall(CameraInternal.State.PENDING_OPEN, 3000,
-                times -> times > 0);
+                new CallTimesAtLeast(1));
 
         // Allow camera to be opened
         mCameraStateRegistry.markCameraState(mockCamera, CameraInternal.State.CLOSED);
@@ -593,7 +594,7 @@
         // Wait for the secondary capture session is configured.
         assertTrue(mSessionStateCallback.waitForOnConfigured(1));
 
-        CameraInternalStateMockObserver mockObserver = new CameraInternalStateMockObserver();
+        MockObserver<CameraInternal.State> mockObserver = new MockObserver<>();
 
         mCamera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(),
                 mockObserver);
@@ -602,7 +603,8 @@
 
         // Wait for the CLOSED state. If the test fail, the CameraX might in wrong internal state,
         // and the Camera2CameraImpl#release() might stuck.
-        mockObserver.verifyOnNewDataCall(CameraInternal.State.CLOSED, 4000, times -> times == 1);
+        mockObserver.verifyOnNewDataCall(CameraInternal.State.CLOSED, 4000,
+                new CallTimes(1));
     }
 
     @Test
@@ -1044,64 +1046,4 @@
             return suggestedResolution;
         }
     }
-
-    static class CameraInternalStateMockObserver
-            implements Observable.Observer<CameraInternal.State> {
-        public interface CallTimesCondition {
-            boolean check(int times);
-        }
-
-        private final Map<CameraInternal.State, Integer> mNewDataCount = new HashMap<>();
-
-        private CallTimesCondition mCallTimesCondition;
-        private CameraInternal.State mValueToVerify;
-        private CountDownLatch mLatch;
-
-        private boolean isVerified() {
-            int count = mNewDataCount.get(mValueToVerify) == null
-                    ? 0 : mNewDataCount.get(mValueToVerify);
-            return mCallTimesCondition.check(count);
-        }
-
-        public void verifyOnNewDataCall(@Nullable CameraInternal.State value,
-                long timeoutInMillis) {
-            verifyOnNewDataCall(value, timeoutInMillis, times -> times == 1);
-        }
-
-        public void verifyOnNewDataCall(@Nullable CameraInternal.State value,
-                long timeoutInMillis, CallTimesCondition callTimesCondition) {
-            mValueToVerify = value;
-            mCallTimesCondition = callTimesCondition;
-
-            if (!isVerified()) {
-                mLatch = new CountDownLatch(1);
-
-                try {
-                    assertTrue("Test failed for a timeout of " + timeoutInMillis + " ms",
-                            mLatch.await(timeoutInMillis, TimeUnit.MILLISECONDS));
-                } catch (InterruptedException e) {
-                    throw new RuntimeException(e);
-                }
-
-                assertTrue("onNewData called "
-                                + mNewDataCount.get(value) + " time(s) with " + value,
-                        isVerified());
-
-                mLatch = null;
-            }
-        }
-
-        @Override
-        public void onNewData(@Nullable CameraInternal.State value) {
-            Integer prevCount = mNewDataCount.get(value);
-            mNewDataCount.put(value, (prevCount == null ? 0 : prevCount) + 1);
-
-            if (mLatch != null && isVerified()) {
-                mLatch.countDown();
-            }
-        }
-
-        @Override
-        public void onError(@NonNull Throwable t) {}
-    };
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
index 748e0c5..8be1ac2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
@@ -32,6 +32,8 @@
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class DisplayInfoManager {
+
+    private static final String TAG = "DisplayInfoManager";
     private static final Size MAX_PREVIEW_SIZE = new Size(1920, 1080);
     private static final Object INSTANCE_LOCK = new Object();
     private static volatile DisplayInfoManager sInstance;
@@ -85,24 +87,41 @@
             return displays[0];
         }
 
-        Display maxDisplay = null;
-        int maxDisplaySize = -1;
+        Display maxDisplayWhenStateOn = null;
+        int maxDisplaySizeWhenStateOn = -1;
         for (Display display : displays) {
             if (display.getState() != Display.STATE_OFF) {
                 Point displaySize = new Point();
                 display.getRealSize(displaySize);
+                if (displaySize.x * displaySize.y > maxDisplaySizeWhenStateOn) {
+                    maxDisplaySizeWhenStateOn = displaySize.x * displaySize.y;
+                    maxDisplayWhenStateOn = display;
+                }
+            }
+        }
+
+        if (maxDisplayWhenStateOn == null) {
+            // If there is no display found with state on, search display with state off as well.
+            // If still no display found, throw IllegalArgumentException.
+            Display maxDisplay = null;
+            int maxDisplaySize = -1;
+            for (Display display : displays) {
+                Point displaySize = new Point();
+                display.getRealSize(displaySize);
                 if (displaySize.x * displaySize.y > maxDisplaySize) {
                     maxDisplaySize = displaySize.x * displaySize.y;
                     maxDisplay = display;
                 }
             }
-        }
 
-        if (maxDisplay == null) {
-            throw new IllegalArgumentException("No display can be found from the input display "
-                    + "manager!");
+            if (maxDisplay == null) {
+                throw new IllegalArgumentException("No display can be found from the input display "
+                        + "manager!");
+            }
+
+            return maxDisplay;
         }
-        return maxDisplay;
+        return maxDisplayWhenStateOn;
     }
 
     /**
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
index fafca66..8fff586 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
@@ -34,8 +34,9 @@
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 import org.robolectric.shadow.api.Shadow
-import org.robolectric.shadows.ShadowDisplayManager
 import org.robolectric.shadows.ShadowDisplay
+import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
 
 @RunWith(RobolectricTestRunner::class)
 @DoNotInstrument
@@ -109,6 +110,36 @@
         assertThat(size).isEqualTo(Point(480, 640))
     }
 
+    @Test(expected = IllegalArgumentException::class)
+    fun canReturnMaxSizeDisplay_noDisplay() {
+        // Arrange
+        removeDisplay(0)
+
+        // Act
+        val displayInfoManager = DisplayInfoManager
+            .getInstance(ApplicationProvider.getApplicationContext())
+        val size = Point()
+        displayInfoManager.maxSizeDisplay.getRealSize(size)
+    }
+
+    @Test
+    fun canReturnMaxSizeDisplay_allOffState() {
+        // Arrange
+        addDisplay(480, 640, Display.STATE_OFF)
+        addDisplay(200, 300, Display.STATE_OFF)
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        removeDisplay(0)
+
+        // Act
+        val displayInfoManager = DisplayInfoManager
+            .getInstance(ApplicationProvider.getApplicationContext())
+        val size = Point()
+        displayInfoManager.maxSizeDisplay.getRealSize(size)
+
+        // Assert
+        assertThat(size).isEqualTo(Point(2000, 3000))
+    }
+
     @Test
     fun canReturnPreviewSize_displaySmallerThan1080P() {
         // Arrange
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
index 6ff346a..24f4e72 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
@@ -171,6 +171,23 @@
     }
 
     @Test
+    fun handleInvalidate_runWhenInvalidateCalled() {
+        // Arrange.
+        var isCalled = false
+        val request = createNewRequest(FAKE_SIZE) {
+            isCalled = true
+        }
+        Truth.assertThat(isCalled).isFalse()
+
+        // Act.
+        request.willNotProvideSurface()
+        request.invalidate()
+
+        // Assert.
+        Truth.assertThat(isCalled).isTrue()
+    }
+
+    @Test
     @Suppress("UNCHECKED_CAST")
     fun cancelledRequest_resultsInREQUEST_CANCELLED() {
         val request = createNewRequest(FAKE_SIZE)
@@ -331,9 +348,10 @@
     private fun createNewRequest(
         size: Size,
         expectedFrameRate: Range<Int>? = null,
-        autoCleanup: Boolean = true
+        autoCleanup: Boolean = true,
+        onInvalidated: () -> Unit = {},
     ): SurfaceRequest {
-        val request = SurfaceRequest(size, FakeCamera(), false, expectedFrameRate)
+        val request = SurfaceRequest(size, FakeCamera(), false, expectedFrameRate, onInvalidated)
         if (autoCleanup) {
             surfaceRequests.add(request)
         }
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallback.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallback.kt
index 76601d7..706d6ec 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallback.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallback.kt
@@ -28,19 +28,33 @@
  */
 class FakeTakePictureCallback : TakePictureCallback {
 
-    private lateinit var inMemoryResultCont: Continuation<ImageProxy>
-    private lateinit var onDiskResultCont: Continuation<OutputFileResults>
+    private var inMemoryResult: ImageProxy? = null
+    private var inMemoryResultCont: Continuation<ImageProxy>? = null
+    private var onDiskResult: OutputFileResults? = null
+    private var onDiskResultCont: Continuation<OutputFileResults>? = null
 
     override fun onImageCaptured() {
         TODO("Not yet implemented")
     }
 
     override fun onFinalResult(outputFileResults: OutputFileResults) {
-        onDiskResultCont.resume(outputFileResults)
+        val cont = onDiskResultCont
+        if (cont != null) {
+            cont.resume(outputFileResults)
+            >
+        } else {
+            >
+        }
     }
 
     override fun onFinalResult(imageProxy: ImageProxy) {
-        inMemoryResultCont.resume(imageProxy)
+        val cont = inMemoryResultCont
+        if (cont != null) {
+            cont.resume(imageProxy)
+            inMemoryResultCont = null
+        } else {
+            inMemoryResult = imageProxy
+        }
     }
 
     override fun onCaptureFailure(imageCaptureException: ImageCaptureException) {
@@ -56,10 +70,18 @@
     }
 
     internal suspend fun getInMemoryResult() = suspendCoroutine { cont ->
-        inMemoryResultCont = cont
+        if (inMemoryResult != null) {
+            cont.resume(inMemoryResult!!)
+        } else {
+            inMemoryResultCont = cont
+        }
     }
 
     internal suspend fun getOnDiskResult() = suspendCoroutine { cont ->
-        >
+        if (onDiskResult != null) {
+            cont.resume(onDiskResult!!)
+        } else {
+            >
+        }
     }
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallbackDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallbackDeviceTest.kt
new file mode 100644
index 0000000..e7cb20b
--- /dev/null
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallbackDeviceTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.imagecapture
+
+import androidx.annotation.RequiresApi
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.testing.fakes.FakeImageInfo
+import androidx.camera.testing.fakes.FakeImageProxy
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+
+/**
+ * Unit tests for [FakeTakePictureCallbackDeviceTest]
+ */
+@RequiresApi(21)
+@SdkSuppress(minSdkVersion = 21)
+class FakeTakePictureCallbackDeviceTest {
+
+    private val fakeTakePictureCallback = FakeTakePictureCallback()
+
+    @Test
+    fun onDiskResultArrivesBeforeGet_canGetResult() = runBlocking {
+        // Arrange.
+        val >
+        // Assert.
+        fakeTakePictureCallback.onFinalResult(onDiskResult)
+        // Act.
+        assertThat(fakeTakePictureCallback.getOnDiskResult()).isEqualTo(onDiskResult)
+    }
+
+    @Test
+    fun inMemoryResultArrivesBeforeGet_canGetResult() = runBlocking {
+        // Arrange.
+        val inMemoryResult = FakeImageProxy(FakeImageInfo())
+        // Assert.
+        fakeTakePictureCallback.onFinalResult(inMemoryResult)
+        // Act.
+        assertThat(fakeTakePictureCallback.getInMemoryResult()).isEqualTo(inMemoryResult)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
index 05fdc22..3f88ffa 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
@@ -304,7 +304,7 @@
     }
 
     private fun createInputSurfaceRequest(): SurfaceRequest {
-        return SurfaceRequest(Size(WIDTH, HEIGHT), fakeCamera, false).apply {
+        return SurfaceRequest(Size(WIDTH, HEIGHT), fakeCamera, false) {}.apply {
             inputSurfaceRequestsToClose.add(this)
         }
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index a13c3ca..a689360 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -279,7 +279,7 @@
     @FlashMode
     private static final int DEFAULT_FLASH_MODE = FLASH_MODE_OFF;
 
-    boolean mUseProcessingPipeline = false;
+    boolean mUseProcessingPipeline = true;
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     static final ExifRotationAvailability EXIF_ROTATION_AVAILABILITY =
@@ -1939,10 +1939,15 @@
         checkMainThread();
         Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, resolution: %s)",
                 cameraId, resolution));
+
         checkState(mImagePipeline == null);
         mImagePipeline = new ImagePipeline(config, resolution, mCameraEffect);
-        checkState(mTakePictureManager == null);
-        mTakePictureManager = new TakePictureManager(mImageCaptureControl, mImagePipeline);
+
+        if (mTakePictureManager == null) {
+            // mTakePictureManager is reused when the Surface is reset.
+            mTakePictureManager = new TakePictureManager(mImageCaptureControl);
+        }
+        mTakePictureManager.setImagePipeline(mImagePipeline);
 
         SessionConfig.Builder sessionConfigBuilder = mImagePipeline.createSessionConfigBuilder();
         if (Build.VERSION.SDK_INT >= 23 && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG) {
@@ -1953,7 +1958,8 @@
             //  to this use case so we don't need to do this check.
             if (isCurrentCamera(cameraId)) {
                 mTakePictureManager.pause();
-                // TODO: we might need to ask mImagePipeline to recreate camera Surface.
+                clearPipelineWithNode(/*keepTakePictureManager=*/ true);
+                mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
                 updateSessionConfig(mSessionConfigBuilder.build());
                 notifyReset();
                 mTakePictureManager.resume();
@@ -2028,6 +2034,15 @@
         return new Rect(0, 0, resolution.getWidth(), resolution.getHeight());
     }
 
+
+    /**
+     * Clears the pipeline without keeping the {@link TakePictureManager}.
+     */
+    @MainThread
+    private void clearPipelineWithNode() {
+        clearPipelineWithNode(/*keepTakePictureManager=*/false);
+    }
+
     /**
      * Clears the pipeline.
      *
@@ -2035,13 +2050,15 @@
      * resources.
      */
     @MainThread
-    private void clearPipelineWithNode() {
+    private void clearPipelineWithNode(boolean keepTakePictureManager) {
         Log.d(TAG, "clearPipelineWithNode");
         checkMainThread();
         mImagePipeline.close();
         mImagePipeline = null;
-        mTakePictureManager.abortRequests();
-        mTakePictureManager = null;
+        if (!keepTakePictureManager) {
+            mTakePictureManager.abortRequests();
+            mTakePictureManager = null;
+        }
     }
 
     /**
@@ -2082,6 +2099,12 @@
         return mCameraEffect;
     }
 
+    @VisibleForTesting
+    @NonNull
+    TakePictureManager getTakePictureManager() {
+        return mTakePictureManager;
+    }
+
     // ===== New architecture end =====
 
     /**
@@ -3081,7 +3104,8 @@
          * will be finally selected will depend on the camera device's hardware level and the
          * bound use cases combination. For more details see the guaranteed supported
          * configurations tables in {@link android.hardware.camera2.CameraDevice}'s
-         * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a> section.
+         * href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice
+         * #regular-capture">Regular capture</a> section.
          *
          * @param resolution The target resolution to choose from supported output sizes list.
          * @return The current Builder.
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 dab5cfd..8eff18d 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
@@ -228,8 +228,12 @@
         clearPipeline();
 
         boolean isRGBA8888SurfaceRequired = config.isRgba8888SurfaceRequired(false);
+
+        // TODO: Can be improved by only restarting part of the pipeline when using the
+        //  CaptureProcessor. e.g. only update the output Surface (between Processor/App), and
+        //  still use the same input Surface (between Camera/Processor). It's just simpler for now.
         final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
-                isRGBA8888SurfaceRequired);
+                isRGBA8888SurfaceRequired, this::notifyReset);
         mCurrentSurfaceRequest = surfaceRequest;
 
         if (mSurfaceProvider != null) {
@@ -319,7 +323,8 @@
                 /*hasEmbeddedTransform=*/true,
                 requireNonNull(getCropRect(resolution)),
                 getRelativeRotation(camera),
-                /*mirroring=*/false);
+                /*mirroring=*/false,
+                this::notifyReset);
         SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
         SurfaceEdge outputEdge = mNode.transform(inputEdge);
         SettableSurface appSurface = outputEdge.getSurfaces().get(0);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
index 159949c..f7bdce3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
@@ -23,6 +23,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
 import androidx.camera.core.impl.ImageReaderProxy;
 
@@ -140,6 +141,16 @@
     }
 
     /**
+     * Check if the {@link SafeCloseImageReaderProxy} is closed for testing.
+     */
+    @VisibleForTesting
+    public boolean isClosed() {
+        synchronized (mLock) {
+            return mIsClosed;
+        }
+    }
+
+    /**
      * Returns the number of empty slots in the queue.
      */
     public int getCapacity() {
@@ -150,6 +161,7 @@
 
     /**
      * Sets a listener for close calls on this image.
+     *
      * @param listener to set
      */
     public void setOnImageCloseListener(@NonNull OnImageCloseListener listener) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
index 374f3a2..523ad9d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
@@ -84,6 +84,10 @@
     // from the camera.
     private final ListenableFuture<Void> mSessionStatusFuture;
 
+    // For notification of surface recreated.
+    @NonNull
+    private final CallbackToFutureAdapter.Completer<Void> mSurfaceRecreationCompleter;
+
     // For notification of surface request cancellation. Should only be used to register
     // cancellation listeners.
     private final CallbackToFutureAdapter.Completer<Void> mRequestCancellationCompleter;
@@ -110,13 +114,15 @@
     public SurfaceRequest(
             @NonNull Size resolution,
             @NonNull CameraInternal camera,
-            boolean isRGBA8888Required) {
-        this(resolution, camera, isRGBA8888Required, /*expectedFrameRate=*/null);
+            boolean isRGBA8888Required,
+            @NonNull Runnable onInvalidated) {
+        this(resolution, camera, isRGBA8888Required, /*expectedFrameRate=*/null, onInvalidated);
     }
 
     /**
      * Creates a new surface request with the given resolution, {@link Camera}, and an optional
      * expected frame rate.
+     *
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -124,7 +130,8 @@
             @NonNull Size resolution,
             @NonNull CameraInternal camera,
             boolean isRGBA8888Required,
-            @Nullable Range<Integer> expectedFrameRate) {
+            @Nullable Range<Integer> expectedFrameRate,
+            @NonNull Runnable onInvalidated) {
         super();
         mResolution = resolution;
         mCamera = camera;
@@ -244,6 +251,9 @@
         //    finished with the surface, so cancelling the surface future below will be a no-op.
         terminationFuture.addListener(() -> mSurfaceFuture.cancel(true),
                 CameraXExecutors.directExecutor());
+
+        mSurfaceRecreationCompleter = initialSurfaceRecreationCompleter(
+                CameraXExecutors.directExecutor(), onInvalidated);
     }
 
     /**
@@ -420,6 +430,72 @@
     }
 
     /**
+     * Sets a {@link Runnable} that handles the situation where {@link Surface} is no longer valid
+     * and triggers the process to request a new {@link Surface}.
+     *
+     * @param executor Executor used to execute the {@code runnable}.
+     * @param runnable The code which will be run when {@link Surface} is no longer valid.
+     */
+    private CallbackToFutureAdapter.Completer<Void> initialSurfaceRecreationCompleter(
+            @NonNull Executor executor, @NonNull Runnable runnable) {
+        AtomicReference<CallbackToFutureAdapter.Completer<Void>> completerRef =
+                new AtomicReference<>(null);
+        final ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
+            completerRef.set(completer);
+            return "SurfaceRequest-surface-recreation(" + SurfaceRequest.this.hashCode() + ")";
+        });
+
+        Futures.addCallback(future, new FutureCallback<Void>() {
+            @Override
+            public void onSuccess(@Nullable Void result) {
+                runnable.run();
+            }
+
+            @Override
+            public void onFailure(@NonNull Throwable t) {
+                // Do nothing
+            }
+        }, executor);
+
+        return Preconditions.checkNotNull(completerRef.get());
+    }
+
+    /**
+     * Invalidate the previously provided {@link Surface} to provide a new {@link Surface}.
+     *
+     * <p>Call this method to inform the surface requester that the previously provided
+     * {@link Surface} is no longer valid (e.g. when the SurfaceView destroys its surface due to
+     * page being slid out in ViewPager2) and should re-send a {@link SurfaceRequest} to obtain a
+     * new {@link Surface}.
+     *
+     * <p>Calling this method will cause the camera to be reconfigured. The app should call this
+     * method when the surface provider is ready to provide a new {@link Surface}. (e.g. a
+     * SurfaceView's surface is created when its window is visible.)
+     *
+     * <p>If the provided {@link Surface} was already invalidated, invoking this method will return
+     * {@code false}, and will have no effect. The surface requester will not be notified again, so
+     * there will not be another {@link SurfaceRequest}.
+     *
+     * <p>Calling this method without {@link #provideSurface(Surface, Executor, Consumer)}
+     * (regardless of whether @link #willNotProvideSurface()} has been called) will still trigger
+     * the surface requester to re-send a {@link SurfaceRequest}.
+     *
+     * <p>Since calling this method also means that the {@link SurfaceRequest} will not be
+     * fulfilled, if the {@link SurfaceRequest} is not completed, it will be completed as if
+     * calling {@link #willNotProvideSurface()}.
+     *
+     * @return true if the provided {@link Surface} is invalidated or false if it was already
+     * invalidated.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public boolean invalidate() {
+        willNotProvideSurface();
+        return mSurfaceRecreationCompleter.set(null);
+    }
+
+    /**
      * Adds a listener to be informed when the camera cancels the surface request.
      *
      * <p>A surface request may be cancelled by the camera if the surface is no longer required.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
index 3199bcb..07e2fd7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
@@ -29,6 +29,7 @@
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.ForwardingImageProxy;
@@ -56,7 +57,6 @@
  *
  * <p>It's also responsible for managing the {@link ImageReaderProxy}. It makes sure that the
  * queue is not overflowed.
- *
  */
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 class CaptureNode implements Node<CaptureNode.In, CaptureNode.Out> {
@@ -72,13 +72,18 @@
     private final Set<ImageProxy> mPendingImages = new HashSet<>();
     private ProcessingRequest mCurrentRequest = null;
 
+    @Nullable
     SafeCloseImageReaderProxy mSafeCloseImageReaderProxy;
+    @Nullable
     private Out mOutputEdge;
+    @Nullable
     private In mInputEdge;
 
     @NonNull
     @Override
     public Out transform(@NonNull In inputEdge) {
+        checkState(mInputEdge == null && mSafeCloseImageReaderProxy == null,
+                "CaptureNode does not support recreation yet.");
         mInputEdge = inputEdge;
         Size size = inputEdge.getSize();
         int format = inputEdge.getFormat();
@@ -127,7 +132,7 @@
         }
 
         // Send the image downstream.
-        mOutputEdge.getImageEdge().accept(imageProxy);
+        requireNonNull(mOutputEdge).getImageEdge().accept(imageProxy);
     }
 
     @VisibleForTesting
@@ -146,7 +151,7 @@
         mPendingStageIds.addAll(request.getStageIds());
 
         // Send the request downstream.
-        mOutputEdge.getRequestEdge().accept(request);
+        requireNonNull(mOutputEdge).getRequestEdge().accept(request);
 
         // Match pending images and send them downstream.
         for (ImageProxy imageProxy : mPendingImages) {
@@ -159,18 +164,23 @@
     @Override
     public void release() {
         checkMainThread();
-        if (mSafeCloseImageReaderProxy != null) {
-            mSafeCloseImageReaderProxy.safeClose();
-        }
-        if (mInputEdge != null) {
-            mInputEdge.closeSurface();
-        }
+        releaseInputResources(requireNonNull(mInputEdge),
+                requireNonNull(mSafeCloseImageReaderProxy));
+    }
+
+    private void releaseInputResources(@NonNull CaptureNode.In inputEdge,
+            @NonNull SafeCloseImageReaderProxy imageReader) {
+        inputEdge.getSurface().close();
+        // Wait for the termination to close the ImageReader or the Surface may be released
+        // prematurely before it can be used by camera2.
+        inputEdge.getSurface().getTerminationFuture().addListener(
+                imageReader::safeClose, mainThreadExecutor());
     }
 
     @VisibleForTesting
     @NonNull
     In getInputEdge() {
-        return mInputEdge;
+        return requireNonNull(mInputEdge);
     }
 
     @MainThread
@@ -230,10 +240,6 @@
             mSurface = new ImmediateSurface(surface);
         }
 
-        void closeSurface() {
-            mSurface.close();
-        }
-
         /**
          * Edge that accepts image metadata.
          *
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
index e9af56a..c9816f5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
@@ -42,21 +42,29 @@
 class RequestWithCallback implements TakePictureCallback {
 
     private final TakePictureRequest mTakePictureRequest;
+    private final TakePictureRequest.RetryControl mRetryControl;
     private final ListenableFuture<Void> mCaptureFuture;
+    private final ListenableFuture<Void> mCompleteFuture;
     private CallbackToFutureAdapter.Completer<Void> mCaptureCompleter;
-    // Tombstone flag that indicates that this callback should not be invoked anymore.
-    private boolean mIsComplete = false;
+    private CallbackToFutureAdapter.Completer<Void> mCompleteCompleter;
     // Flag tracks if the request has been aborted by the UseCase. Once aborted, this class stops
     // propagating callbacks to the app.
     private boolean mIsAborted = false;
 
-    RequestWithCallback(@NonNull TakePictureRequest takePictureRequest) {
+    RequestWithCallback(@NonNull TakePictureRequest takePictureRequest,
+            @NonNull TakePictureRequest.RetryControl retryControl) {
         mTakePictureRequest = takePictureRequest;
+        mRetryControl = retryControl;
         mCaptureFuture = CallbackToFutureAdapter.getFuture(
                 completer -> {
                     mCaptureCompleter = completer;
                     return "CaptureCompleteFuture";
                 });
+        mCompleteFuture = CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    mCompleteCompleter = completer;
+                    return "RequestCompleteFuture";
+                });
     }
 
     @MainThread
@@ -98,7 +106,6 @@
         mTakePictureRequest.onResult(imageProxy);
     }
 
-
     @MainThread
     @Override
     public void onProcessFailure(@NonNull ImageCaptureException imageCaptureException) {
@@ -125,19 +132,35 @@
             // Fail silently if the request has been aborted.
             return;
         }
+        if (mTakePictureRequest.decrementRetryCounter()) {
+            mRetryControl.retryRequest(mTakePictureRequest);
+        } else {
+            onFailure(imageCaptureException);
+        }
         markComplete();
         mCaptureCompleter.set(null);
+    }
 
-        // TODO(b/242683221): Add retry logic.
+    @MainThread
+    void abortAndSendErrorToApp(@NonNull ImageCaptureException imageCaptureException) {
+        checkMainThread();
+        abort();
         onFailure(imageCaptureException);
     }
 
     @MainThread
-    void abort(@NonNull ImageCaptureException imageCaptureException) {
+    void abortSilentlyAndRetry() {
+        checkMainThread();
+        abort();
+        mRetryControl.retryRequest(mTakePictureRequest);
+    }
+
+    @MainThread
+    private void abort() {
         checkMainThread();
         mIsAborted = true;
         mCaptureCompleter.set(null);
-        onFailure(imageCaptureException);
+        mCompleteCompleter.set(null);
     }
 
     /**
@@ -152,14 +175,26 @@
         return mCaptureFuture;
     }
 
+    /**
+     * Gets a {@link ListenableFuture} that finishes when the capture is completed.
+     *
+     * <p>A request is completed when it gets either a result or an unrecoverable error.
+     */
+    @MainThread
+    @NonNull
+    ListenableFuture<Void> getCompleteFuture() {
+        checkMainThread();
+        return mCompleteFuture;
+    }
+
     private void checkOnImageCaptured() {
         checkState(mCaptureFuture.isDone(),
                 "onImageCaptured() must be called before onFinalResult()");
     }
 
     private void markComplete() {
-        checkState(!mIsComplete, "The callback can only complete once.");
-        mIsComplete = true;
+        checkState(!mCompleteFuture.isDone(), "The callback can only complete once.");
+        mCompleteCompleter.set(null);
     }
 
     @MainThread
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index 2ecfcd5..49f346b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -44,7 +44,9 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Deque;
+import java.util.List;
 
 /**
  * Manages {@link ImageCapture#takePicture} calls.
@@ -61,34 +63,43 @@
  * <p>The thread safety is guaranteed by using the main thread.
  */
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class TakePictureManager implements OnImageCloseListener {
+public class TakePictureManager implements OnImageCloseListener, TakePictureRequest.RetryControl {
 
     private static final String TAG = "TakePictureManager";
 
     // Queue of new requests that have not been sent to the pipeline/camera.
     @VisibleForTesting
     final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
-    final ImagePipeline mImagePipeline;
     final ImageCaptureControl mImageCaptureControl;
+    ImagePipeline mImagePipeline;
 
-    // The current request being processed by camera. Null if the camera is idle.
-    @VisibleForTesting
+    // The current request being processed by the camera. Only one request can be processed by
+    // the camera at the same time. Null if the camera is idle.
     @Nullable
-    RequestWithCallback mInFlightRequest;
+    private RequestWithCallback mCapturingRequest;
+    // The current requests that have not received a result or an error.
+    private final List<RequestWithCallback> mIncompleteRequests;
 
     // Once paused, the class waits until the class is resumed to handle new requests.
     boolean mPaused = false;
 
     /**
      * @param imageCaptureControl for controlling {@link ImageCapture}
-     * @param imagePipeline       for building capture requests and post-processing camera output.
      */
     @MainThread
-    public TakePictureManager(
-            @NonNull ImageCaptureControl imageCaptureControl,
-            @NonNull ImagePipeline imagePipeline) {
+    public TakePictureManager(@NonNull ImageCaptureControl imageCaptureControl) {
         checkMainThread();
         mImageCaptureControl = imageCaptureControl;
+        mIncompleteRequests = new ArrayList<>();
+    }
+
+    /**
+     * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
+     * output.
+     */
+    @MainThread
+    public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
+        checkMainThread();
         mImagePipeline = imagePipeline;
         mImagePipeline.setOnImageCloseListener(this);
     }
@@ -105,6 +116,14 @@
         issueNextRequest();
     }
 
+    @MainThread
+    @Override
+    public void retryRequest(@NonNull TakePictureRequest request) {
+        checkMainThread();
+        // Insert the request to the front of the queue.
+        mNewRequests.addFirst(request);
+    }
+
     /**
      * Pauses sending request to camera.
      */
@@ -112,8 +131,11 @@
     public void pause() {
         checkMainThread();
         mPaused = true;
-        // TODO(b/242683221): increment the retry counter on the in-flight request. The
-        //  mInFlightRequest may fail due to the pausing and need one more retry.
+
+        // Always retry because the camera may not send an error callback during the reset.
+        if (mCapturingRequest != null) {
+            mCapturingRequest.abortSilentlyAndRetry();
+        }
     }
 
     /**
@@ -142,9 +164,11 @@
         mNewRequests.clear();
 
         // Abort the in-flight request after clearing the pending requests.
-        if (mInFlightRequest != null) {
-            // TODO: optimize the performance by aborting early in CaptureNode and BundlingNode
-            mInFlightRequest.abort(exception);
+        // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
+        List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
+        for (RequestWithCallback request : requestsSnapshot) {
+            // TODO: optimize the performance by not processing aborted requests.
+            request.abortAndSendErrorToApp(exception);
         }
     }
 
@@ -155,7 +179,7 @@
     void issueNextRequest() {
         checkMainThread();
         Log.d(TAG, "Issue the next TakePictureRequest.");
-        if (hasInFlightRequest()) {
+        if (hasCapturingRequest()) {
             Log.d(TAG, "There is already a request in-flight.");
             return;
         }
@@ -173,8 +197,8 @@
             return;
         }
 
-        RequestWithCallback requestWithCallback = new RequestWithCallback(request);
-        trackCurrentRequest(requestWithCallback);
+        RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
+        trackCurrentRequests(requestWithCallback);
 
         // Send requests.
         Pair<CameraRequest, ProcessingRequest> requests =
@@ -187,13 +211,21 @@
     /**
      * Waits for the request to finish before issuing the next.
      */
-    private void trackCurrentRequest(@NonNull RequestWithCallback requestWithCallback) {
-        checkState(!hasInFlightRequest());
-        mInFlightRequest = requestWithCallback;
-        mInFlightRequest.getCaptureFuture().addListener(() -> {
-            mInFlightRequest = null;
+    private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
+        checkState(!hasCapturingRequest());
+        mCapturingRequest = requestWithCallback;
+
+        // Waits for the capture to finish before issuing the next.
+        mCapturingRequest.getCaptureFuture().addListener(() -> {
+            mCapturingRequest = null;
             issueNextRequest();
         }, directExecutor());
+
+        // Track all incomplete requests so we can abort them when UseCase is detached.
+        mIncompleteRequests.add(requestWithCallback);
+        requestWithCallback.getCompleteFuture().addListener(() -> {
+            mIncompleteRequests.remove(requestWithCallback);
+        }, directExecutor());
     }
 
     /**
@@ -232,8 +264,19 @@
     }
 
     @VisibleForTesting
-    boolean hasInFlightRequest() {
-        return mInFlightRequest != null;
+    boolean hasCapturingRequest() {
+        return mCapturingRequest != null;
+    }
+
+    @VisibleForTesting
+    List<RequestWithCallback> getIncompleteRequests() {
+        return mIncompleteRequests;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    public ImagePipeline getImagePipeline() {
+        return mImagePipeline;
     }
 
     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
index f77c182..c8e398c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.core.imagecapture;
 
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.core.util.Preconditions.checkArgument;
 
 import static java.util.Objects.requireNonNull;
@@ -25,9 +26,11 @@
 import android.os.Build;
 
 import androidx.annotation.IntRange;
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.ImageProxy;
@@ -35,6 +38,7 @@
 import androidx.camera.core.impl.ImageCaptureConfig;
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.internal.compat.workaround.CaptureFailedRetryEnabler;
 
 import com.google.auto.value.AutoValue;
 
@@ -51,7 +55,12 @@
 @AutoValue
 public abstract class TakePictureRequest {
 
-    // TODO(b/242683221): add a retry counter.
+    /**
+     * By default, ImageCapture does not retry requests. For some problematic devices, the
+     * capture request can become success after retrying. The allowed retry count will be
+     * provided by the {@link CaptureFailedRetryEnabler}.
+     */
+    private int mRemainingRetires = new CaptureFailedRetryEnabler().getRetryCount();
 
     /**
      * Gets the callback {@link Executor} provided by the app.
@@ -123,6 +132,43 @@
     abstract List<CameraCaptureCallback> getSessionConfigCameraCaptureCallbacks();
 
     /**
+     * Decrements retry counter.
+     *
+     * @return true if there is still remaining retries at the time of calling. In that case, the
+     * request should be retried. False when there is no retry left. The caller needs to fail the
+     * request.
+     */
+    @MainThread
+    boolean decrementRetryCounter() {
+        checkMainThread();
+        if (mRemainingRetires > 0) {
+            mRemainingRetires--;
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Increments retry counter.
+     */
+    @MainThread
+    void incrementRetryCounter() {
+        checkMainThread();
+        mRemainingRetires++;
+    }
+
+    /**
+     * Gets the current retry count for testing.
+     */
+    @MainThread
+    @VisibleForTesting
+    int getRemainingRetries() {
+        checkMainThread();
+        return mRemainingRetires;
+    }
+
+    /**
      * Delivers {@link ImageCaptureException} to the app.
      */
     void onError(@NonNull ImageCaptureException imageCaptureException) {
@@ -177,4 +223,17 @@
                 onDiskCallback, outputFileOptions, cropRect, sensorToBufferTransform,
                 rotationDegrees, jpegQuality, captureMode, sessionConfigCameraCaptureCallbacks);
     }
+
+    /**
+     * Interface for retrying a {@link TakePictureRequest}.
+     */
+    interface RetryControl {
+
+        /**
+         * Retries the given {@link TakePictureRequest}.
+         *
+         * <p>The request should be injected to the front of the queue.
+         */
+        void retryRequest(@NonNull TakePictureRequest takePictureRequest);
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/CaptureFailedRetryQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/CaptureFailedRetryQuirk.java
new file mode 100644
index 0000000..cd4f709
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/CaptureFailedRetryQuirk.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.quirk;
+
+import android.os.Build;
+import android.util.Pair;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.internal.compat.workaround.CaptureFailedRetryEnabler;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: 242683221
+ *     Description: Quirk that allows ImageCapture to retry once when encountering capture
+ *     failures. On Samsung sm-g981u1, image capture requests might fail when continuously taking
+ *     multiple pictures. The capture failure might be related to some problems in the CamX
+ *     pipeline. Re-issue the original capture request to retry can workaround the issue.
+ *     Device(s): Samsung sm-g981u1
+ *     @see CaptureFailedRetryEnabler
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class CaptureFailedRetryQuirk implements Quirk {
+
+    private static final Set<Pair<String, String>> FAILED_RETRY_ALLOW_LIST = new HashSet<>(
+            Collections.singletonList(Pair.create("SAMSUNG", "SM-G981U1")));
+
+    static boolean load() {
+        String brand = Build.BRAND.toUpperCase(Locale.US);
+        String model = Build.MODEL.toUpperCase(Locale.US);
+
+        return FAILED_RETRY_ALLOW_LIST.contains(Pair.create(brand, model));
+    }
+
+    /**
+     * Returns the count which the image capture request can be retried. Currently returns 1
+     * always if the quirk is loaded.
+     */
+    public int getRetryCount() {
+        return 1;
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 79ddcbb..afcaf8e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -46,6 +46,9 @@
         if (SurfaceOrderQuirk.load()) {
             quirks.add(new SurfaceOrderQuirk());
         }
+        if (CaptureFailedRetryQuirk.load()) {
+            quirks.add(new CaptureFailedRetryQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnabler.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnabler.java
new file mode 100644
index 0000000..e1b88953
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnabler.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.workaround;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.internal.compat.quirk.CaptureFailedRetryQuirk;
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
+
+/**
+ * Workaround that allows the {@link ImageCapture} to retry the same capture request once when
+ * encountering capture failures.
+ *
+ * @see CaptureFailedRetryQuirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class CaptureFailedRetryEnabler {
+
+    private final CaptureFailedRetryQuirk mCaptureFailedRetryQuirk = DeviceQuirks.get(
+            CaptureFailedRetryQuirk.class);
+
+    /**
+     * Returns the count which the image capture request can be retried.
+     */
+    public int getRetryCount() {
+        return mCaptureFailedRetryQuirk == null ? 0 : mCaptureFailedRetryQuirk.getRetryCount();
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
index dad0080..6fae56c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
@@ -94,6 +94,9 @@
     @Nullable
     private SurfaceRequest mProviderSurfaceRequest;
 
+    @NonNull
+    private final Runnable mOnInvalidated;
+
     /**
      * Please see the getters to understand the parameters.
      */
@@ -105,7 +108,8 @@
             boolean hasEmbeddedTransform,
             @NonNull Rect cropRect,
             int rotationDegrees,
-            boolean mirroring) {
+            boolean mirroring,
+            @NonNull Runnable onInvalidated) {
         super(size, format);
         mTargets = targets;
         mSensorToBufferTransform = sensorToBufferTransform;
@@ -113,6 +117,7 @@
         mCropRect = cropRect;
         mRotationDegrees = rotationDegrees;
         mMirroring = mirroring;
+        mOnInvalidated = onInvalidated;
         mSurfaceFuture = CallbackToFutureAdapter.getFuture(
                 completer -> {
                     mCompleter = completer;
@@ -223,8 +228,8 @@
             @Nullable Range<Integer> expectedFpsRange) {
         checkMainThread();
         // TODO(b/238230154) figure out how to support HDR.
-        SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal, true,
-                expectedFpsRange);
+        SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal, false,
+                expectedFpsRange, this::invalidate);
         try {
             setProvider(surfaceRequest.getDeferrableSurface());
         } catch (SurfaceClosedException e) {
@@ -282,6 +287,22 @@
     }
 
     /**
+     * Invalidate the previously provided {@link Surface} to provide a new one.
+     *
+     * Calls to inform that the {@link Surface} previously provided via
+     * {@link #createSurfaceRequest(CameraInternal)} or
+     * {@link #createSurfaceRequest(CameraInternal, Range)} is no longer valid and should reacquire
+     * a new {@link Surface}.
+     *
+     * <p>The method <strong>must</strong> be called when the surface provider is ready to provide
+     * a new {@link Surface}. (e.g. SurfaceView's surface is created when its window is visible.)
+     */
+    public void invalidate() {
+        close();
+        mOnInvalidated.run();
+    }
+
+    /**
      * Closes the {@link DeferrableSurface} and notifies linked objects for the closure.
      */
     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index fd1e4bcf8..6007535 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -106,6 +106,11 @@
 
     @NonNull
     private SettableSurface createOutputSurface(@NonNull SettableSurface inputSurface) {
+        // TODO: Can be improved by only restarting part of the pipeline. e.g. only update the
+        //  output Surface (between Effect/App), and still use the same input Surface (between
+        //  Camera/Effect). It's just simpler for now.
+        final Runnable >
+
         SettableSurface outputSurface;
         switch (mGlTransformOptions) {
             case APPLY_CROP_ROTATE_AND_MIRRORING:
@@ -135,7 +140,8 @@
                         /*hasEmbeddedTransform=*/false,
                         sizeToRect(rotatedCroppedSize),
                         /*rotationDegrees=*/0,
-                        /*mirroring=*/false);
+                        /*mirroring=*/false,
+                        onSurfaceInvalidated);
                 break;
             case USE_SURFACE_TEXTURE_TRANSFORM:
                 // No transform output as placeholder.
@@ -148,7 +154,8 @@
                         /*hasEmbeddedTransform=*/false,
                         inputSurface.getCropRect(),
                         inputSurface.getRotationDegrees(),
-                        inputSurface.getMirroring());
+                        inputSurface.getMirroring(),
+                        onSurfaceInvalidated);
                 break;
             default:
                 throw new AssertionError("Unknown GlTransformOptions: " + mGlTransformOptions);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index a7f51e4..1ef77cb 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -192,13 +192,32 @@
     }
 
     @Test
-    fun processingPipelineOffByDefault() {
-        assertThat(
-            bindImageCapture(
-                useProcessingPipeline = false,
-                bufferFormat = ImageFormat.JPEG,
-            ).isProcessingPipelineEnabled
-        ).isFalse()
+    fun onError_surfaceIsRecreated() {
+        // Arrange: create ImageCapture and get the Surface
+        val imageCapture = bindImageCapture(
+            bufferFormat = ImageFormat.JPEG,
+        )
+        val oldSurface = imageCapture.sessionConfig.surfaces.single().surface.get()
+        assertTakePictureManagerHasTheSameSurface(imageCapture)
+
+        // Act: invoke onError callback.
+        imageCapture.sessionConfig.errorListeners.single().onError(
+            imageCapture.sessionConfig,
+            SessionConfig.SessionError.SESSION_ERROR_SURFACE_NEEDS_RESET
+        )
+
+        // Assert: the surface has been recreated.
+        val newSurface = imageCapture.sessionConfig.surfaces.single().surface.get()
+        assertThat(newSurface).isNotEqualTo(oldSurface)
+        assertTakePictureManagerHasTheSameSurface(imageCapture)
+    }
+
+    private fun assertTakePictureManagerHasTheSameSurface(imageCapture: ImageCapture) {
+        val takePictureManagerSurface =
+            imageCapture.takePictureManager.imagePipeline.createSessionConfigBuilder()
+                .build().surfaces.single().surface.get()
+        val useCaseSurface = imageCapture.sessionConfig.surfaces.single().surface.get()
+        assertThat(takePictureManagerSurface).isEqualTo(useCaseSurface)
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
index 151ab67..c783150 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
@@ -18,15 +18,18 @@
 
 import android.graphics.ImageFormat
 import android.os.Build
+import android.os.Looper.getMainLooper
 import android.util.Size
 import androidx.camera.core.ImageProxy
 import androidx.camera.core.imagecapture.Utils.createCaptureBundle
 import androidx.camera.core.imagecapture.Utils.createFakeImage
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 
@@ -58,10 +61,32 @@
         }
     }
 
+    @After
+    fun tearDown() {
+        captureNode.release()
+    }
+
+    @Test
+    fun release_imageReaderNotClosedUntilTermination() {
+        // Arrange: increment the DeferrableSurface's use count to prevent it from being terminated.
+        captureNode.inputEdge.surface.incrementUseCount()
+        // Act: release.
+        captureNode.release()
+        shadowOf(getMainLooper()).idle()
+        // Assert: ImageReader is not closed.
+        assertThat(captureNode.mSafeCloseImageReaderProxy!!.isClosed).isFalse()
+
+        // Act: decrease the use count. Now the DeferrableSurface will terminate.
+        captureNode.inputEdge.surface.decrementUseCount()
+        shadowOf(getMainLooper()).idle()
+        // Assert: ImageReader is closed.
+        assertThat(captureNode.mSafeCloseImageReaderProxy!!.isClosed).isTrue()
+    }
+
     @Test
     fun transform_verifyInputSurface() {
         assertThat(captureNodeIn.surface.surface.get())
-            .isEqualTo(captureNode.mSafeCloseImageReaderProxy.surface)
+            .isEqualTo(captureNode.mSafeCloseImageReaderProxy!!.surface)
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
index b1fe7d6c..26bba9a 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
@@ -18,6 +18,7 @@
 
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.utils.futures.Futures
+import androidx.concurrent.futures.CallbackToFutureAdapter
 import com.google.common.util.concurrent.ListenableFuture
 
 /**
@@ -25,9 +26,21 @@
  */
 class FakeImageCaptureControl : ImageCaptureControl {
 
+    companion object {
+        // By default, this fake object returns an immediate successful result.
+        private val IMMEDIATE_RESULT: ListenableFuture<Void> = Futures.immediateFuture(null)
+    }
+
     val actions = arrayListOf<Action>()
     var latestCaptureConfigs: List<CaptureConfig?> = arrayListOf()
-    var response: ListenableFuture<Void> = Futures.immediateFuture(null)
+
+    // Flip this flag to return a custom result using pendingResultCompleter.
+    var shouldUsePendingResult = false
+    lateinit var pendingResultCompleter: CallbackToFutureAdapter.Completer<Void>
+    var pendingResult = CallbackToFutureAdapter.getFuture { completer ->
+        pendingResultCompleter = completer
+        "FakeImageCaptureControl's pendingResult"
+    }
 
     override fun lockFlashMode() {
         actions.add(Action.LOCK_FLASH)
@@ -42,7 +55,10 @@
     ): ListenableFuture<Void> {
         actions.add(Action.SUBMIT_REQUESTS)
         latestCaptureConfigs = captureConfigs
-        return response
+        if (shouldUsePendingResult) {
+            return pendingResult
+        }
+        return IMMEDIATE_RESULT
     }
 
     enum class Action {
diff --git a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeRetryControl.kt
similarity index 60%
copy from room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java
copy to camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeRetryControl.kt
index 4e95b3d..e58d031 100644
--- a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeRetryControl.kt
@@ -14,15 +14,18 @@
  * limitations under the License.
  */
 
-package androidx.room;
+package androidx.camera.core.imagecapture
 
-import kotlin.coroutines.Continuation;
-import kotlin.jvm.functions.Function1;
+import androidx.camera.core.imagecapture.TakePictureRequest.RetryControl
 
-public class RoomDatabaseKt {
-    public static final <R> Object withTransaction(RoomDatabase db,
-            Function1<? super Continuation<? super R>, ? extends Object> block,
-            Continuation<? super R> cont) {
-        return null;
+/**
+ * Fake [RetryControl] that keeps track of the retried request.
+ */
+class FakeRetryControl : RetryControl {
+
+    var retriedRequest: TakePictureRequest? = null
+
+    override fun retryRequest(takePictureRequest: TakePictureRequest) {
+        retriedRequest = takePictureRequest
     }
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
index a40665a..f2d3f38 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
@@ -291,7 +291,7 @@
         for (i in 0 until MAX_IMAGES) {
             val imageInfo = FakeImageInfo()
             imageReaderProxy.triggerImageAvailable(imageInfo.tagBundle, 0)
-            imagePipeline.captureNode.mSafeCloseImageReaderProxy.acquireNextImage()
+            imagePipeline.captureNode.mSafeCloseImageReaderProxy!!.acquireNextImage()
                 ?.let { images.add(it) }
         }
 
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
index 8789904..f0b794e 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
@@ -46,6 +46,7 @@
     private lateinit var otherError: ImageCaptureException
     private lateinit var imageResult: ImageProxy
     private lateinit var fileResult: ImageCapture.OutputFileResults
+    private lateinit var retryControl: FakeRetryControl
 
     @Before
     fun setUp() {
@@ -53,15 +54,49 @@
         otherError = ImageCaptureException(ERROR_CAPTURE_FAILED, "", null)
         imageResult = FakeImageProxy(FakeImageInfo())
         fileResult = ImageCapture.OutputFileResults(null)
+        retryControl = FakeRetryControl()
+    }
+
+    @Test
+    fun failCaptureWithRemainingRetries_requestIsRetried() {
+        // Arrange: create a request the a retry counter of 1.
+        val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        request.incrementRetryCounter()
+        assertThat(request.remainingRetries).isEqualTo(1)
+        val callback = RequestWithCallback(request, retryControl)
+
+        // Act.
+        callback.onCaptureFailure(otherError)
+        shadowOf(getMainLooper()).idle()
+
+        // Assert.
+        assertThat(retryControl.retriedRequest).isEqualTo(request)
+        assertThat(request.remainingRetries).isEqualTo(0)
+    }
+
+    @Test
+    fun failCaptureWithoutRemainingRetries_requestNotRetried() {
+        // Arrange: create a request the a retry counter of 0.
+        val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        assertThat(request.remainingRetries).isEqualTo(0)
+        val callback = RequestWithCallback(request, retryControl)
+
+        // Act.
+        callback.onCaptureFailure(otherError)
+        shadowOf(getMainLooper()).idle()
+
+        // Assert.
+        assertThat(retryControl.retriedRequest).isNull()
+        assertThat(request.exceptionReceived).isEqualTo(otherError)
     }
 
     @Test
     fun abortRequestThenSendOtherErrors_receiveAbortError() {
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
-        val callback = RequestWithCallback(request)
+        val callback = RequestWithCallback(request, retryControl)
         // Act.
-        callback.abort(abortError)
+        callback.abortAndSendErrorToApp(abortError)
         callback.onCaptureFailure(otherError)
         callback.onProcessFailure(otherError)
         shadowOf(getMainLooper()).idle()
@@ -73,7 +108,7 @@
     fun sendInMemoryResult_receiveResult() {
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
-        val callback = RequestWithCallback(request)
+        val callback = RequestWithCallback(request, retryControl)
         // Act.
         callback.onImageCaptured()
         callback.onFinalResult(imageResult)
@@ -86,9 +121,9 @@
     fun abortRequestAndSendInMemoryResult_doNotReceiveResult() {
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
-        val callback = RequestWithCallback(request)
+        val callback = RequestWithCallback(request, retryControl)
         // Act.
-        callback.abort(abortError)
+        callback.abortAndSendErrorToApp(abortError)
         callback.onFinalResult(imageResult)
         shadowOf(getMainLooper()).idle()
         // Assert.
@@ -99,7 +134,7 @@
     fun sendOnDiskResult_receiveResult() {
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.ON_DISK)
-        val callback = RequestWithCallback(request)
+        val callback = RequestWithCallback(request, retryControl)
         // Act.
         callback.onImageCaptured()
         callback.onFinalResult(fileResult)
@@ -112,9 +147,9 @@
     fun abortRequestAndSendOnDiskResult_doNotReceiveResult() {
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.ON_DISK)
-        val callback = RequestWithCallback(request)
+        val callback = RequestWithCallback(request, retryControl)
         // Act.
-        callback.abort(abortError)
+        callback.abortAndSendErrorToApp(abortError)
         callback.onFinalResult(imageResult)
         shadowOf(getMainLooper()).idle()
         // Assert.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index 5335ee6..5ed2782 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -24,7 +24,6 @@
 import androidx.camera.core.ImageCapture.OutputFileResults
 import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.utils.futures.Futures
 import androidx.camera.testing.fakes.FakeImageInfo
 import androidx.camera.testing.fakes.FakeImageProxy
 import com.google.common.truth.Truth.assertThat
@@ -46,7 +45,9 @@
 
     private val imagePipeline = FakeImagePipeline()
     private val imageCaptureControl = FakeImageCaptureControl()
-    private val takePictureManager = TakePictureManager(imageCaptureControl, imagePipeline)
+    private val takePictureManager =
+        TakePictureManager(imageCaptureControl).also { it.setImagePipeline(imagePipeline) }
+    private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
 
     @After
     fun tearDown() {
@@ -54,14 +55,55 @@
     }
 
     @Test
-    fun abortRequests_receiveErrorCallback() {
+    fun pause_inFlightRequestRetried() {
+        // Arrange: create 2 requests and offer them to the manager.
+        imageCaptureControl.shouldUsePendingResult = true
+        val request1 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        val request2 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        takePictureManager.offerRequest(request1)
+        takePictureManager.offerRequest(request2)
+        assertThat(takePictureManager.mNewRequests).containsExactly(request2)
+
+        // Act: pause the manager.
+        takePictureManager.pause()
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: the in-flight request is retried.
+        assertThat(takePictureManager.mNewRequests).containsExactly(request1, request2).inOrder()
+        assertThat(request1.remainingRetries).isEqualTo(0)
+    }
+
+    @Test
+    fun abortPostProcessingRequests_receiveErrorCallback() {
+        // Arrange: setup a request that is captured but not processed.
+        val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        takePictureManager.offerRequest(request)
+        val processingRequest = imagePipeline.getProcessingRequest(request)
+        processingRequest.onImageCaptured()
+        shadowOf(getMainLooper()).idle()
+        // Verify the "captured, not processed" status.
+        assertThat(takePictureManager.hasCapturingRequest()).isFalse()
+        assertThat(takePictureManager.incompleteRequests[0].captureFuture.isDone).isTrue()
+        assertThat(takePictureManager.incompleteRequests[0].completeFuture.isDone).isFalse()
+
+        // Act: abort all requests.
+        takePictureManager.abortRequests()
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: the user has received a CAMERA_CLOSED error.
+        assertThat((request.exceptionReceived as ImageCaptureException).imageCaptureError)
+            .isEqualTo(ERROR_CAMERA_CLOSED)
+    }
+
+    @Test
+    fun abortNewRequests_receiveErrorCallback() {
         // Arrange: send 2 request.
         val request1 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
         val request2 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
         takePictureManager.offerRequest(request1)
         takePictureManager.offerRequest(request2)
 
-        // Act: ab the manage and finish the 1st request.
+        // Act: abort the manager and finish the 1st request.
         takePictureManager.abortRequests()
         // Camera returns the image, but it should be ignored.
         val processingRequest = imagePipeline.getProcessingRequest(request1)
@@ -88,7 +130,6 @@
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
         takePictureManager.offerRequest(request)
-        val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
         val processingRequest = imagePipeline.getProcessingRequest(request)
         processingRequest.onImageCaptured()
         // Act.
@@ -147,24 +188,25 @@
     fun submitRequestFailsWithImageCaptureException_appGetsTheSameException() {
         // Arrange: configure ImageCaptureControl to always fail.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
-        val cause = ImageCaptureException(ERROR_CAMERA_CLOSED, "", null)
-        imageCaptureControl.response = Futures.immediateFailedFuture(cause)
+        imageCaptureControl.shouldUsePendingResult = true
+        imageCaptureControl.pendingResultCompleter.setException(exception)
 
         // Act.
         takePictureManager.offerRequest(request)
         shadowOf(getMainLooper()).idle()
 
         // Assert.
-        assertThat(request.exceptionReceived!!).isEqualTo(cause)
-        assertThat(takePictureManager.hasInFlightRequest()).isFalse()
+        assertThat(request.exceptionReceived!!).isEqualTo(exception)
+        assertThat(takePictureManager.hasCapturingRequest()).isFalse()
     }
 
     @Test
     fun submitRequestFailsWithGenericException_appGetsWrappedException() {
         // Arrange: configure ImageCaptureControl to always fail.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
-        val cause = Exception()
-        imageCaptureControl.response = Futures.immediateFailedFuture(cause)
+        val genericException = Exception()
+        imageCaptureControl.shouldUsePendingResult = true
+        imageCaptureControl.pendingResultCompleter.setException(genericException)
 
         // Act.
         takePictureManager.offerRequest(request)
@@ -172,8 +214,8 @@
 
         // Assert.
         assertThat(request.exceptionReceived!!.imageCaptureError).isEqualTo(ERROR_CAPTURE_FAILED)
-        assertThat(request.exceptionReceived!!.cause).isEqualTo(cause)
-        assertThat(takePictureManager.hasInFlightRequest()).isFalse()
+        assertThat(request.exceptionReceived!!.cause).isEqualTo(genericException)
+        assertThat(takePictureManager.hasCapturingRequest()).isFalse()
     }
 
     @Test
@@ -233,7 +275,7 @@
         takePictureManager.offerRequest(request2)
         takePictureManager.offerRequest(request3)
         shadowOf(getMainLooper()).idle()
-        val response1 = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
+        val response1 = exception
         val response2 = OutputFileResults(null)
         val response3 = FakeImageProxy(FakeImageInfo())
         imagePipeline.getProcessingRequest(request1).onImageCaptured()
@@ -259,7 +301,6 @@
         takePictureManager.offerRequest(request)
 
         // Act: send exception via ImagePipeline
-        val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
         imagePipeline.getProcessingRequest(request).onImageCaptured()
         imagePipeline.getProcessingRequest(request).onProcessFailure(exception)
         shadowOf(getMainLooper()).idle()
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnablerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnablerTest.kt
new file mode 100644
index 0000000..74b8ffe
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnablerTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.workaround
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+/**
+ * Unit test for [CaptureFailedRetryEnabler]
+ */
+@SmallTest
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class CaptureFailedRetryEnablerTest(
+    private val brand: String,
+    private val model: String,
+    private val expectedResult: Int
+) {
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "brand={0}, model={1}, expectedResult={2}")
+        fun data() = mutableListOf<Array<Any?>>().apply {
+            add(arrayOf("SAMSUNG", "SM-G981U1", 1))
+            add(arrayOf("samsung", "sm-g981u1", 1))
+            add(arrayOf("samsung", "sm-g981u10", 0))
+            add(arrayOf("samsung", "sm-g981u", 0))
+            add(arrayOf("fakeBrand", "sm-g981u1", 0))
+            add(arrayOf("", "", 0))
+        }
+    }
+
+    @Before
+    fun setup() {
+        ReflectionHelpers.setStaticField(Build::class.java, "BRAND", brand)
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
+    }
+
+    @Test
+    fun shouldRetry() {
+        assertThat(CaptureFailedRetryEnabler().retryCount).isEqualTo(expectedResult)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
index 64bebab..96048cd 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
@@ -72,7 +72,7 @@
         settableSurface = SettableSurface(
             CameraEffect.PREVIEW, Size(640, 480), ImageFormat.PRIVATE,
             Matrix(), true, Rect(), 0, false
-        )
+        ) {}
         fakeSurfaceTexture = SurfaceTexture(0)
         fakeSurface = Surface(fakeSurfaceTexture)
     }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 0724a8d..3027eceb 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -265,7 +265,7 @@
             cropRect,
             rotationDegrees,
             mirroring
-        )
+        ) {}
         inputEdge = SurfaceEdge.create(listOf(surface))
     }
 
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
index b07183a..9047555 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
@@ -93,7 +93,7 @@
                 }
             }, executor)
         // Act: invoke methods.
-        processorWithExecutor.onInputSurface(SurfaceRequest(SIZE, FakeCamera(), false))
+        processorWithExecutor.onInputSurface(SurfaceRequest(SIZE, FakeCamera(), false) {})
         processorWithExecutor.onOutputSurface(mock(SurfaceOutput::class.java))
         shadowOf(getMainLooper()).idle()
         shadowOf(executorThread.looper).idle()
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java
new file mode 100644
index 0000000..7d8d4ae
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks;
+
+import static junit.framework.TestCase.assertTrue;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.testing.mocks.helpers.ArgumentCaptor;
+import androidx.camera.testing.mocks.helpers.CallTimes;
+import androidx.camera.testing.mocks.helpers.CallTimesAtLeast;
+import androidx.core.util.Consumer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A mock implementation of {@link Consumer} to verify proper invocations in tests.
+ *
+ * @param <T> the parameter type to be consumed
+ */
+public class MockConsumer<T> implements Consumer<T> {
+
+    private CountDownLatch mLatch;
+
+    private final List<T> mEventList = new ArrayList<>();
+    private int mIndexLastVerifiedInOrder = -1;
+
+    private Class<?> mClassTypeToVerify;
+    private CallTimes mCallTimes;
+    private boolean mInOrder = false;
+
+    private int getEventCount() {
+        int count = 0;
+        int startIndex = mInOrder ? mIndexLastVerifiedInOrder + 1 : 0;
+        for (int i = startIndex; i < mEventList.size(); i++) {
+            if (mClassTypeToVerify.isInstance(mEventList.get(i))) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    private int getLastVerifiedEventInOrder() {
+        int count = 0;
+        for (int i = mIndexLastVerifiedInOrder + 1; i < mEventList.size(); i++) {
+            if (mClassTypeToVerify.isInstance(mEventList.get(i))) {
+                count++;
+            }
+
+            if (mCallTimes.isSatisfied(count)) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    private boolean isVerified() {
+        if (mClassTypeToVerify == null || mCallTimes == null) {
+            return false;
+        }
+
+        return mCallTimes.isSatisfied(getEventCount());
+    }
+
+    /**
+     * Verifies if {@link #accept} method was invoked properly during test.
+     *
+     * @param captor the argument captor to record the arguments passed to {@link #accept} method
+     *               matching the provided {@code classType}
+     * @see #verifyAcceptCall(Class, boolean, long, CallTimes)
+     */
+    public void verifyAcceptCall(@NonNull Class<?> classType, boolean inOrder,
+            @NonNull CallTimes callTimes, @Nullable ArgumentCaptor<T> captor) {
+        mClassTypeToVerify = classType;
+        mCallTimes = callTimes;
+        mInOrder = inOrder;
+
+        assertTrue("accept() called " + getEventCount() + " time(s) with "
+                + classType.getSimpleName() + (inOrder ? " in order" : "") + ", expected "
+                + (callTimes instanceof CallTimesAtLeast ? " at least " : "")
+                + callTimes.getTimes() + " times",
+                isVerified());
+
+        if (inOrder) {
+            mIndexLastVerifiedInOrder = getLastVerifiedEventInOrder();
+        }
+
+        if (captor != null) {
+            captor.setArguments(mEventList);
+        }
+    }
+
+    /**
+     * Verifies if {@link #accept} method was invoked properly during test.
+     *
+     * <p>{@code callTimes} defaults to {@code new CallTimes(1)} which ensures that
+     *      {@link #accept} method was invoked exactly once with given verification parameters.
+     *
+     * @see #verifyAcceptCall(Class, boolean, long, CallTimes)
+     */
+    public void verifyAcceptCall(@NonNull Class<?> classType, boolean inOrder,
+            long timeoutInMillis) {
+        verifyAcceptCall(classType, inOrder, timeoutInMillis, new CallTimes(1));
+    }
+
+    /**
+     * Verifies if {@link #accept} method was invoked properly during test.
+     *
+     * <p>Usually invoked from a method with {@link org.junit.Test} annotation.
+     *
+     * @param classType       the class type to verify for the parameter of {@link #accept(Object)}
+     *                        method, this is checked with {@link Class#isInstance(Object)} method
+     * @param inOrder         the {@link #verifyAcceptCall} method invocations with {@code inOrder =
+     *                        true} are chained together to make sure they were in order
+     * @param timeoutInMillis the time limit to wait for asynchronous operation after which
+     *                        {@link junit.framework.AssertionFailedError} is thrown
+     * @param callTimes       the condition for how many times {@link #accept} method should be
+     *                        called
+     */
+    public void verifyAcceptCall(@NonNull Class<?> classType, boolean inOrder,
+            long timeoutInMillis, @NonNull CallTimes callTimes) {
+        mClassTypeToVerify = classType;
+        mCallTimes = callTimes;
+        mInOrder = inOrder;
+
+        if (!isVerified()) {
+            mLatch = new CountDownLatch(1);
+
+            try {
+                assertTrue("Test failed for a timeout of " + timeoutInMillis + " ms",
+                        mLatch.await(timeoutInMillis, TimeUnit.MILLISECONDS));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            assertTrue("accept() called " + getEventCount() + " time(s) with "
+                            + classType.getSimpleName()
+                            + (mInOrder ? " in order" : "") + ", expected "
+                            + (callTimes instanceof CallTimesAtLeast ? " at least " : "")
+                            + callTimes.getTimes() + " times",
+                    isVerified());
+
+            if (inOrder) {
+                mIndexLastVerifiedInOrder = getLastVerifiedEventInOrder();
+            }
+
+            mLatch = null;
+        }
+    }
+
+    @Override
+    public void accept(T event) {
+        mEventList.add(event);
+
+        if (mLatch != null && isVerified()) {
+            mLatch.countDown();
+        }
+    }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockObserver.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockObserver.java
new file mode 100644
index 0000000..bb54016
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockObserver.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks;
+
+import static junit.framework.TestCase.assertTrue;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.testing.mocks.helpers.CallTimes;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A mock implementation of {@link Observable.Observer} to verify proper invocations in tests.
+ *
+ * @param <T> the parameter type to be observed
+ */
+public class MockObserver<T> implements Observable.Observer<T> {
+    private final Map<T, Integer> mNewDataCount = new HashMap<>();
+
+    private CallTimes mCallTimes;
+    private T mValueToVerify;
+    private CountDownLatch mLatch;
+
+    private boolean isVerified() {
+        if (mCallTimes == null) {
+            return false;
+        }
+
+        int count = mNewDataCount.get(mValueToVerify) == null
+                ? 0 : mNewDataCount.get(mValueToVerify);
+        return mCallTimes.isSatisfied(count);
+    }
+
+    /**
+     * Verifies if {@link #onNewData} method was invoked properly during test.
+     *
+     * <p>{@code callTimes} defaults to {@code new CallTimes(1)} which ensures that
+     * {@link #onNewData(Object)} method was invoked exactly once with given verification
+     * parameters.
+     *
+     * @see #verifyOnNewDataCall(Object, long, CallTimes)
+     */
+    public void verifyOnNewDataCall(@Nullable T value,
+            long timeoutInMillis) {
+        verifyOnNewDataCall(value, timeoutInMillis, new CallTimes(1));
+    }
+
+    /**
+     * Verifies if {@link #onNewData} method was invoked properly during test.
+     *
+     * @param value the value to match with the value parameter of {@link #onNewData(Object)}
+     * @param timeoutInMillis the time limit to wait for asynchronous operation after which
+     *                       {@link junit.framework.AssertionFailedError} is thrown
+     * @param callTimes the condition for how many times {@link #onNewData} method should be called
+     */
+    public void verifyOnNewDataCall(@Nullable T value,
+            long timeoutInMillis, @NonNull CallTimes callTimes) {
+        mValueToVerify = value;
+        mCallTimes = callTimes;
+
+        if (!isVerified()) {
+            mLatch = new CountDownLatch(1);
+
+            try {
+                assertTrue("Test failed for a timeout of " + timeoutInMillis + " ms",
+                        mLatch.await(timeoutInMillis, TimeUnit.MILLISECONDS));
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            assertTrue("onNewData() called "
+                            + mNewDataCount.get(value) + " time(s) with " + value,
+                    isVerified());
+
+            mLatch = null;
+        }
+    }
+
+    @Override
+    public void onNewData(@Nullable T value) {
+        Integer prevCount = mNewDataCount.get(value);
+        mNewDataCount.put(value, (prevCount == null ? 0 : prevCount) + 1);
+
+        if (mLatch != null && isVerified()) {
+            mLatch.countDown();
+        }
+    }
+
+    @Override
+    public void onError(@NonNull Throwable t) {}
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentCaptor.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentCaptor.java
new file mode 100644
index 0000000..59b71ba
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentCaptor.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility for capturing arguments, usually in fake class method invocations for testing.
+ *
+ * @param <T> the type of the arguments to capture
+ */
+public class ArgumentCaptor<T> {
+    private final List<T> mArguments = new ArrayList<>();
+    private ArgumentMatcher<T> mArgumentMatcher;
+
+    /**
+     * Creates a new instance of {@link ArgumentCaptor}.
+     */
+    public ArgumentCaptor() {}
+
+    /**
+     * Creates a new instance of {@link ArgumentCaptor} with the given parameter.
+     *
+     * @param argumentMatcher specifies the matching criteria for capturing
+     */
+    public ArgumentCaptor(@NonNull ArgumentMatcher<T> argumentMatcher) {
+        mArgumentMatcher = argumentMatcher;
+    }
+
+    /**
+     * Returns the last value captured, {@code null} if no value has been captured yet
+     */
+    @Nullable
+    public T getValue() {
+        if (mArguments.size() == 0) {
+            return null;
+        }
+
+        return mArguments.get(mArguments.size() - 1);
+    }
+
+    /**
+     * Adds arguments to capture list according to argument matching rule (if exists).
+     *
+     * @param argumentList the list of arguments to capture
+     */
+    public void setArguments(@NonNull List<T> argumentList) {
+        for (T argument : argumentList) {
+            if (mArgumentMatcher == null || mArgumentMatcher.matches(argument)) {
+                mArguments.add(argument);
+            }
+        }
+    }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentMatcher.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentMatcher.java
new file mode 100644
index 0000000..c95163a
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentMatcher.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+/**
+ * An interface for matching arguments in {@link ArgumentCaptor} class.
+ *
+ * @param <T> the type of the arguments to capture
+ */
+public interface ArgumentMatcher<T> {
+
+    /**
+     * Matches an argument according to matching criteria and returns if it is matched or not.
+     *
+     * @param argument the argument to match according to criteria
+     * @return  {@code true} if argument is matched according desired criteria,
+     *          {@code false} otherwise
+     */
+    boolean matches(T argument);
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimes.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimes.java
new file mode 100644
index 0000000..8601807
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimes.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+/**
+ * Utility for defining the number of invocations allowed while testing fake class methods.
+ */
+public class CallTimes {
+    protected int mTimes;
+
+    /**
+     * Creates a new instance of {@link CallTimes} with the given parameter.
+     *
+     * @param times the number of invocations allowed
+     */
+    public CallTimes(int times) {
+        mTimes = times;
+    }
+
+    public int getTimes() {
+        return mTimes;
+    }
+
+    /**
+     * Checks if the number of invocation is exactly the same as specified.
+     *
+     * @param actualCallCount the occurred number of invocations
+     *
+     * @return {@code true} if the number of invocations is exactly the same as specified,
+     *          {@code false} otherwise
+     */
+    public boolean isSatisfied(int actualCallCount) {
+        return actualCallCount == mTimes;
+    }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeast.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeast.java
new file mode 100644
index 0000000..5b78740
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeast.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+/**
+ * Utility for defining the number of invocations allowed while testing fake class methods.
+ */
+public class CallTimesAtLeast extends CallTimes {
+    /**
+     * Creates a new instance of {@link CallTimesAtLeast} with the given parameter.
+     *
+     * @param times the minimum number of invocations that should be occurring
+     */
+    public CallTimesAtLeast(int times) {
+        super(times);
+    }
+
+    /**
+     * Checks if the number of invocation is at least {@link #mTimes}.
+     *
+     * @param actualCallCount the occurred number of invocations
+     * @return {@code true} if the number of invocations is at least the times specified,
+     *          {@code false} otherwise
+     */
+    @Override
+    public boolean isSatisfied(int actualCallCount) {
+        return actualCallCount >= mTimes;
+    }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockConsumerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockConsumerTest.java
new file mode 100644
index 0000000..fc01c74
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockConsumerTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.mocks.helpers.ArgumentCaptor;
+import androidx.camera.testing.mocks.helpers.CallTimes;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockConsumerTest {
+    public static final String DUMMY_STRING_1 = "dummy1";
+    public static final String DUMMY_STRING_2 = "dummy2";
+
+    public static final int DUMMY_INT_1 = 1;
+
+    private MockConsumer<Object> mMockConsumer;
+
+    @Before
+    public void setUp() {
+        mMockConsumer = new MockConsumer<>();
+    }
+
+    @Test
+    public void callTimesMatches_verifyAcceptCallPasses() {
+        mMockConsumer.accept(new Object());
+        mMockConsumer.accept(new Object());
+        mMockConsumer.verifyAcceptCall(Object.class, false, new CallTimes(2), null);
+    }
+
+    @Test
+    public void callTimesMismatched_verifyAcceptCallFailsWithProperMessage() {
+        mMockConsumer.accept(new Object());
+        mMockConsumer.accept(new Object());
+
+        AssertionFailedError error = assertThrows(AssertionFailedError.class,
+                () -> mMockConsumer.verifyAcceptCall(Object.class, false, new CallTimes(1), null));
+        assertEquals("accept() called 2 time(s) with Object, expected 1 times", error.getMessage());
+    }
+
+    @Test
+    public void verifiedInOrder_verifyAcceptCallSucceeds() {
+        mMockConsumer.accept(DUMMY_STRING_1);
+        mMockConsumer.accept(DUMMY_STRING_1);
+        mMockConsumer.accept(DUMMY_INT_1);
+        mMockConsumer.accept(DUMMY_INT_1);
+        mMockConsumer.accept(DUMMY_INT_1);
+
+        mMockConsumer.verifyAcceptCall(String.class, true, new CallTimes(2), null);
+        mMockConsumer.verifyAcceptCall(Integer.class, true, new CallTimes(3), null);
+    }
+
+    @Test
+    public void verifiedInOrderWithoutConsecutiveness_verifyAcceptCallSucceeds() {
+        mMockConsumer.accept(DUMMY_STRING_1);
+        mMockConsumer.accept(DUMMY_INT_1);
+        mMockConsumer.accept(DUMMY_STRING_1);
+
+        mMockConsumer.verifyAcceptCall(String.class, true, new CallTimes(2), null);
+    }
+
+    @Test
+    public void verifiedWithArgumentCaptor_captorHasCorrectValue() {
+        ArgumentCaptor<Object> captor = new ArgumentCaptor<>();
+
+        mMockConsumer.accept(DUMMY_STRING_1);
+        mMockConsumer.accept(DUMMY_STRING_2);
+
+        mMockConsumer.verifyAcceptCall(String.class, false, new CallTimes(2), captor);
+
+        assertEquals(DUMMY_STRING_2, captor.getValue());
+    }
+
+    @Test
+    public void acceptCalledWithinTimeout_verifyAcceptCallPasses() {
+        new Thread(() -> {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            mMockConsumer.accept(DUMMY_STRING_1);
+        }, "test thread").start();
+
+        mMockConsumer.verifyAcceptCall(String.class, false, 500);
+    }
+
+    @Test
+    public void acceptCalledAfterTimeout_verifyAcceptCallFailsWithProperMessage() {
+        new Thread(() -> {
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            mMockConsumer.accept(DUMMY_STRING_1);
+        }, "test thread").start();
+
+        AssertionFailedError error = assertThrows(AssertionFailedError.class,
+                () -> {
+                    mMockConsumer.verifyAcceptCall(String.class, false, 500);
+                });
+
+        assertEquals("Test failed for a timeout of 500 ms", error.getMessage());
+    }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockObserverTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockObserverTest.java
new file mode 100644
index 0000000..4653944
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockObserverTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.mocks.helpers.CallTimes;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockObserverTest {
+    private static final Object DUMMY_NEW_DATA = new Object();
+    private static final Object DUMMY_NEW_DATA_2 = new Object();
+
+    private MockObserver<Object> mMockObserver;
+
+    @Before
+    public void setUp() {
+        mMockObserver = new MockObserver<>();
+    }
+
+    @Test
+    public void callTimesMatches_verifyOnNewDataCallPasses() {
+        mMockObserver.onNewData(DUMMY_NEW_DATA);
+        mMockObserver.onNewData(DUMMY_NEW_DATA);
+        mMockObserver.onNewData(DUMMY_NEW_DATA_2);
+
+        mMockObserver.verifyOnNewDataCall(DUMMY_NEW_DATA, 100, new CallTimes(2));
+    }
+
+    @Test
+    public void verifiedWithinTimeout_verifyAcceptCallPasses() {
+        new Thread(() -> {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            mMockObserver.onNewData(DUMMY_NEW_DATA);
+        }, "test thread").start();
+
+        mMockObserver.verifyOnNewDataCall(DUMMY_NEW_DATA, 500);
+    }
+
+
+    @Test
+    public void notVerifiedWithinTimeout_verifyOnNewDataCallFailsWithProperMessage() {
+        mMockObserver.onNewData(DUMMY_NEW_DATA);
+
+        AssertionFailedError error = assertThrows(AssertionFailedError.class,
+                () -> mMockObserver.verifyOnNewDataCall(DUMMY_NEW_DATA, 100, new CallTimes(2)));
+        assertEquals("Test failed for a timeout of 100 ms",
+                error.getMessage());
+    }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/ArgumentCaptorTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/ArgumentCaptorTest.java
new file mode 100644
index 0000000..5ef9c2b
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/ArgumentCaptorTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ArgumentCaptorTest {
+    private static final Object DUMMY_ARGUMENT_1 = new Object();
+    private static final Object DUMMY_ARGUMENT_2 = new Object();
+
+    private static final List<Object> DUMMY_ARGUMENTS_1 = Arrays.asList(DUMMY_ARGUMENT_1,
+            new Object(), DUMMY_ARGUMENT_2, new Object());
+    private static final List<Object> DUMMY_ARGUMENTS_2 = Arrays.asList(new Object(), new Object());
+
+    private ArgumentCaptor<Object> mArgumentCaptor;
+
+    @Before
+    public void setUp() {
+        mArgumentCaptor = new ArgumentCaptor<>();
+    }
+
+    @Test
+    public void noArgumentProvided_getValueReturnsNull() {
+        assertNull(mArgumentCaptor.getValue());
+    }
+
+    @Test
+    public void argumentsProvided_getValueReturnsLastValue() {
+        mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_1);
+
+        assertEquals(DUMMY_ARGUMENTS_1.get(DUMMY_ARGUMENTS_1.size() - 1),
+                mArgumentCaptor.getValue());
+    }
+
+    @Test
+    public void argumentsProvidedTwice_getValueReturnsLastValue() {
+        mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_1);
+        mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_2);
+
+        assertEquals(DUMMY_ARGUMENTS_2.get(DUMMY_ARGUMENTS_2.size() - 1),
+                mArgumentCaptor.getValue());
+    }
+
+
+    @Test
+    public void argumentsProvidedWithMatcher_getValueReturnsLastMatchedValue() {
+        mArgumentCaptor = new ArgumentCaptor<>(argument -> argument.equals(DUMMY_ARGUMENT_1)
+                        || argument.equals(DUMMY_ARGUMENT_2));
+
+        mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_1);
+
+        assertEquals(DUMMY_ARGUMENT_2, mArgumentCaptor.getValue());
+    }
+
+    @Test
+    public void argumentsProvidedWithMatcher_getValueReturnsNullForNoMatch() {
+        mArgumentCaptor = new ArgumentCaptor<>(argument -> false);
+
+        mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_1);
+
+        assertNull(mArgumentCaptor.getValue());
+    }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeastTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeastTest.java
new file mode 100644
index 0000000..d4bbd0c
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeastTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class CallTimesAtLeastTest {
+    private final CallTimesAtLeast mCallTimes = new CallTimesAtLeast(5);
+
+    @Test
+    public void actualCallCountMatchesExactly_isSatisfiedReturnsTrue() {
+        assertTrue(mCallTimes.isSatisfied(5));
+    }
+
+    @Test
+    public void actualCallCountIsLess_isSatisfiedReturnsFalse() {
+        assertFalse(mCallTimes.isSatisfied(2));
+    }
+
+    @Test
+    public void actualCallCountIsMore_isSatisfiedReturnsTrue() {
+        assertTrue(mCallTimes.isSatisfied(8));
+    }
+
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesTest.java
new file mode 100644
index 0000000..659bec8
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class CallTimesTest {
+    private final CallTimes mCallTimes = new CallTimes(5);
+
+    @Test
+    public void getTimesReturnsCorrectly() {
+        assertEquals(5, mCallTimes.getTimes());
+    }
+
+    @Test
+    public void actualCallCountMatchesExactly_isSatisfiedReturnsTrue() {
+        assertTrue(mCallTimes.isSatisfied(5));
+    }
+
+    @Test
+    public void actualCallCountIsLess_isSatisfiedReturnsFalse() {
+        assertFalse(mCallTimes.isSatisfied(2));
+    }
+
+    @Test
+    public void actualCallCountIsMore_isSatisfiedReturnsFalse() {
+        assertFalse(mCallTimes.isSatisfied(8));
+    }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 2f0af13..2f648b8 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.video
 
+import androidx.camera.testing.mocks.helpers.ArgumentCaptor as ArgumentCaptorCameraX
 import android.Manifest
 import android.annotation.SuppressLint
 import android.app.AppOpsManager
@@ -52,6 +53,8 @@
 import androidx.camera.testing.LabTestRule
 import androidx.camera.testing.SurfaceTextureProvider
 import androidx.camera.testing.asFlow
+import androidx.camera.testing.mocks.MockConsumer
+import androidx.camera.testing.mocks.helpers.CallTimesAtLeast
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INVALID_OUTPUT_OPTIONS
@@ -87,7 +90,6 @@
 import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeTrue
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TestName
@@ -259,7 +261,8 @@
 
     @Test
     fun canRecordToFile() {
-        clearInvocations(videoRecordEventListener)
+        val videoRecordEventListener = MockConsumer<VideoRecordEvent>()
+
         invokeSurfaceRequest()
         val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
 
@@ -268,23 +271,30 @@
                 .withAudioEnabled()
                 .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
 
-        val inOrder = inOrder(videoRecordEventListener)
-        inOrder.verify(videoRecordEventListener, timeout(5000L))
-            .accept(any(VideoRecordEvent.Start::class.java))
-        inOrder.verify(videoRecordEventListener, timeout(15000L).atLeast(5))
-            .accept(any(VideoRecordEvent.Status::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Start::class.java,
+            true, 5000L)
+
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Status::class.java,
+            true, 15000L, CallTimesAtLeast(5))
 
         recording.stopSafely()
 
-        inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
-            .accept(any(VideoRecordEvent.Finalize::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
+            true, FINALIZE_TIMEOUT)
 
         val uri = Uri.fromFile(file)
         checkFileHasAudioAndVideo(uri)
 
         // Check the output Uri from the finalize event match the Uri from the given file.
-        val captor = ArgumentCaptor.forClass(VideoRecordEvent::class.java)
-        verify(videoRecordEventListener, atLeastOnce()).accept(captor.capture())
+        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+            VideoRecordEvent::class.java.isInstance(
+                argument
+            )
+        }
+
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java, false,
+            CallTimesAtLeast(1), captor)
+
         val finalize = captor.value as VideoRecordEvent.Finalize
         assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
 
@@ -415,9 +425,10 @@
     }
 
     @Test
-    @Ignore("b/239752223")
     fun canPauseResume() {
-        clearInvocations(videoRecordEventListener)
+        val videoRecordEventListener =
+            MockConsumer<VideoRecordEvent>()
+
         invokeSurfaceRequest()
 
         val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
@@ -429,23 +440,23 @@
 
         recording.pause()
 
-        val inOrder = inOrder(videoRecordEventListener)
-        inOrder.verify(videoRecordEventListener, timeout(5000L))
-            .accept(any(VideoRecordEvent.Pause::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Pause::class.java,
+            true, 5000L)
 
         recording.resume()
 
-        inOrder.verify(videoRecordEventListener, timeout(5000L))
-            .accept(any(VideoRecordEvent.Resume::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Resume::class.java,
+            true, 5000L)
+
         // Check there are data being encoded after resuming.
-        inOrder.verify(videoRecordEventListener, timeout(15000L).atLeast(5))
-            .accept(any(VideoRecordEvent.Status::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Status::class.java,
+            true, 15000L, CallTimesAtLeast(5))
 
         recording.stopSafely()
 
         // Wait for the recording to be finalized.
-        inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
-            .accept(any(VideoRecordEvent.Finalize::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
+            true, FINALIZE_TIMEOUT)
 
         checkFileHasAudioAndVideo(Uri.fromFile(file))
 
@@ -555,7 +566,6 @@
     }
 
     @Test
-    @Ignore("b/239752223")
     fun setFileSizeLimit() {
         val fileSizeLimit = 500L * 1024L // 500 KB
         runFileSizeLimitTest(fileSizeLimit)
@@ -565,7 +575,6 @@
     // the encoder. This will ensure that the recording will be finalized even if it has no data
     // written to it.
     @Test
-    @Ignore("b/239752223")
     fun setFileSizeLimitLowerThanInitialDataSize() {
         val fileSizeLimit = 1L // 1 byte
         runFileSizeLimitTest(fileSizeLimit)
@@ -1039,9 +1048,10 @@
     }
 
     @Test
-    @Ignore("b/239752223")
     fun canRecordWithoutAudio() {
-        clearInvocations(videoRecordEventListener)
+        val videoRecordEventListener =
+            MockConsumer<VideoRecordEvent>()
+
         invokeSurfaceRequest()
         val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
 
@@ -1049,15 +1059,22 @@
             recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
                 .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
 
-        val inOrder = inOrder(videoRecordEventListener)
-        inOrder.verify(videoRecordEventListener, timeout(5000L))
-            .accept(any(VideoRecordEvent.Start::class.java))
-        inOrder.verify(videoRecordEventListener, timeout(15000L).atLeast(5))
-            .accept(any(VideoRecordEvent.Status::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Start::class.java,
+            true, 5000L)
+
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Status::class.java,
+            true, 15000L, CallTimesAtLeast(5))
 
         // Check the audio information reports state as disabled.
-        val captor = ArgumentCaptor.forClass(VideoRecordEvent::class.java)
-        verify(videoRecordEventListener, atLeastOnce()).accept(captor.capture())
+        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+            VideoRecordEvent::class.java.isInstance(
+                argument
+            )
+        }
+
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java,
+            false, CallTimesAtLeast(1), captor)
+
         assertThat(captor.value).isInstanceOf(VideoRecordEvent.Status::class.java)
         val status = captor.value as VideoRecordEvent.Status
         assertThat(status.recordingStats.audioStats.audioState)
@@ -1065,8 +1082,8 @@
 
         recording.stopSafely()
 
-        verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
-            .accept(any(VideoRecordEvent.Finalize::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
+            false, FINALIZE_TIMEOUT)
 
         checkFileAudio(Uri.fromFile(file), false)
         checkFileVideo(Uri.fromFile(file), true)
@@ -1324,18 +1341,23 @@
             .setFileSizeLimit(fileSizeLimit)
             .build()
 
+        val videoRecordEventListener =
+            MockConsumer<VideoRecordEvent>()
+
         val recording = recorder
             .prepareRecording(context, outputOptions)
             .withAudioEnabled()
             .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
 
-        verify(
-            videoRecordEventListener,
-            timeout(60000L)
-        ).accept(any(VideoRecordEvent.Finalize::class.java))
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
+            false, 60000L)
 
-        val captor = ArgumentCaptor.forClass(VideoRecordEvent::class.java)
-        verify(videoRecordEventListener, atLeastOnce()).accept(captor.capture())
+        val captor = ArgumentCaptorCameraX<VideoRecordEvent> {
+                argument -> VideoRecordEvent::class.java.isInstance(argument)
+        }
+
+        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java,
+            false, CallTimesAtLeast(1), captor)
 
         assertThat(captor.value).isInstanceOf(VideoRecordEvent.Finalize::class.java)
         val finalize = captor.value as VideoRecordEvent.Finalize
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
index d0eea0a..e3f640c 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
@@ -41,6 +41,8 @@
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.processing.DefaultSurfaceProcessor
+import androidx.camera.core.processing.SurfaceProcessorInternal
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
@@ -50,7 +52,7 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import java.io.File
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
@@ -130,6 +132,7 @@
 
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context: Context = ApplicationProvider.getApplicationContext()
+    private val surfaceProcessorsToRelease = mutableListOf<SurfaceProcessorInternal>()
     private lateinit var cameraProvider: ProcessCameraProvider
     private lateinit var lifecycleOwner: FakeLifecycleOwner
     private lateinit var cameraInfo: CameraInfo
@@ -172,20 +175,42 @@
             }
             cameraProvider.shutdown()[10, TimeUnit.SECONDS]
         }
+        for (surfaceProcessor in surfaceProcessorsToRelease) {
+            surfaceProcessor.release()
+        }
+        surfaceProcessorsToRelease.clear()
     }
 
     @Test
     fun qualityOptionCanRecordVideo() {
+        testQualityOptionRecordVideo()
+    }
+
+    @Test
+    fun qualityOptionCanRecordVideo_enableSurfaceProcessor() {
+        assumeSuccessfulSurfaceProcessing()
+
+        testQualityOptionRecordVideo(surfaceProcessor = createSurfaceProcessor())
+    }
+
+    private fun testQualityOptionRecordVideo(surfaceProcessor: SurfaceProcessorInternal? = null) {
         // Arrange.
         val recorder = Recorder.Builder().setQualitySelector(QualitySelector.from(quality)).build()
         val videoCapture = VideoCapture.withOutput(recorder)
+        videoCapture.setProcessor(surfaceProcessor)
         val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val latchForVideoRecording = CountDownLatch(5)
-        val eventListener = Consumer<VideoRecordEvent> {
-            when (it) {
+        val latchForRecordingStatus = CountDownLatch(5)
+        val latchForRecordingFinalized = CountDownLatch(1)
+        var finalizedEvent: VideoRecordEvent.Finalize? = null
+        val eventListener = Consumer<VideoRecordEvent> { event ->
+            when (event) {
                 is VideoRecordEvent.Status -> {
                     // Make sure the recording proceed for a while.
-                    latchForVideoRecording.countDown()
+                    latchForRecordingStatus.countDown()
+                }
+                is VideoRecordEvent.Finalize -> {
+                    finalizedEvent = event
+                    latchForRecordingFinalized.countDown()
                 }
                 else -> {
                     // Ignore other events.
@@ -203,20 +228,30 @@
 
         // Act.
         videoCapture.startVideoRecording(file, eventListener).use {
-
             // Verify the recording proceed for a while.
-            Truth.assertThat(
-                latchForVideoRecording.await(
-                    VIDEO_TIMEOUT_SEC,
-                    TimeUnit.SECONDS
-                )
-            ).isTrue()
+            assertThat(latchForRecordingStatus.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
         }
 
+        // Verify the recording is finalized without error.
+        assertThat(latchForRecordingFinalized.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+        assertThat(finalizedEvent!!.error).isEqualTo(VideoRecordEvent.Finalize.ERROR_NONE)
+
         // Clean up
         file.delete()
     }
 
+    private fun createSurfaceProcessor(): SurfaceProcessorInternal =
+        DefaultSurfaceProcessor().apply { surfaceProcessorsToRelease.add(this) }
+
+    /** Skips tests which will enable surface processing and encounter device specific issues. */
+    private fun assumeSuccessfulSurfaceProcessing() {
+        // Skip for b/253211491
+        Assume.assumeFalse(
+            "Skip tests for Cuttlefish API 30 eglCreateWindowSurface issue",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 30
+        )
+    }
+
     private fun VideoCapture<Recorder>.startVideoRecording(
         file: File,
         eventListener: Consumer<VideoRecordEvent>
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 4f61481..2df259f 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -497,6 +497,11 @@
         Threads.checkMainThread();
         CameraInternal camera = Preconditions.checkNotNull(getCamera());
 
+        // Currently, VideoCapture uses StreamInfo to handle requests for surface, so
+        // handleInvalidate() is not used. But if a different approach is asked in the future,
+        // handleInvalidate() can be used as an alternative.
+        Runnable >
+
         // TODO(b/229410005): The expected FPS range will need to come from the camera rather
         //  than what is requested in the config. For now we use the default range of (30, 30)
         //  for behavioral consistency.
@@ -520,7 +525,8 @@
                     /*hasEmbeddedTransform=*/true,
                     mCropRect,
                     getRelativeRotation(camera),
-                    /*mirroring=*/false);
+                    /*mirroring=*/false,
+                    onSurfaceInvalidated);
             SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
             SurfaceEdge outputEdge = mNode.transform(inputEdge);
             SettableSurface appSurface = outputEdge.getSurfaces().get(0);
@@ -534,7 +540,8 @@
                 }
             }, CameraXExecutors.mainThreadExecutor());
         } else {
-            mSurfaceRequest = new SurfaceRequest(resolution, camera, false, targetFpsRange);
+            mSurfaceRequest = new SurfaceRequest(resolution, camera, false, targetFpsRange,
+                    onSurfaceInvalidated);
             mDeferrableSurface = mSurfaceRequest.getDeferrableSurface();
             // When camera buffers from a REALTIME device are passed directly to a video encoder
             // from the camera, automatic compensation is done to account for differing timebases
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
index 2a9f7f3..41b0c0e 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -89,9 +89,6 @@
         if (ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.load()) {
             quirks.add(new ImageCaptureFailedWhenVideoCaptureIsBoundQuirk());
         }
-        if (MediaCodecDoesNotSendEos.load()) {
-            quirks.add(new MediaCodecDoesNotSendEos());
-        }
 
         return quirks;
     }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java
deleted file mode 100644
index 46ccb85..0000000
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.video.internal.compat.quirk;
-
-import android.media.MediaCodec;
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.Quirk;
-
-/**
- * <p>QuirkSummary
- * Bug Id: b/248189542
- * Description: When recording video with effect pipeline enabled, calling
- *              {@link MediaCodec#signalEndOfInputStream()} doesn't trigger an EOS buffer to
- *              {@link MediaCodec.Callback}.
- * Device(s): twist 2 pro.
- */
-@RequiresApi(21)
-public class MediaCodecDoesNotSendEos implements Quirk {
-
-    public static boolean isPositivoTwist2Pro() {
-        return "positivo".equalsIgnoreCase(Build.BRAND) && "twist 2 pro".equalsIgnoreCase(
-                Build.MODEL);
-    }
-
-    static boolean load() {
-        return isPositivoTwist2Pro();
-    }
-}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index c5d5bf1..b618ac7 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -30,6 +30,7 @@
 
 import android.annotation.SuppressLint;
 import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
 import android.media.MediaCodecInfo;
 import android.media.MediaFormat;
 import android.os.Bundle;
@@ -52,7 +53,6 @@
 import androidx.camera.video.internal.compat.quirk.CameraUseInconsistentTimebaseQuirk;
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
-import androidx.camera.video.internal.compat.quirk.MediaCodecDoesNotSendEos;
 import androidx.camera.video.internal.compat.quirk.VideoEncoderSuspendDoesNotIncludeSuspendTimeQuirk;
 import androidx.camera.video.internal.workaround.EncoderFinder;
 import androidx.camera.video.internal.workaround.VideoTimebaseConverter;
@@ -202,11 +202,10 @@
     Long mLastDataStopTimestamp = null;
     @SuppressWarnings("WeakerAccess") // synthetic accessor
     Future<?> mStopTimeoutFuture = null;
-    @Nullable
-    private MediaCodecCallback mMediaCodecCallback;
 
     private boolean mIsFlushedAfterEndOfStream = false;
     private boolean mSourceStoppedSignalled = false;
+    boolean mMediaCodecEosSignalled = false;
 
     final EncoderFinder mEncoderFinder = new EncoderFinder();
 
@@ -277,13 +276,13 @@
         mMediaCodec.reset();
         mIsFlushedAfterEndOfStream = false;
         mSourceStoppedSignalled = false;
+        mMediaCodecEosSignalled = false;
         mPendingCodecStop = false;
         if (mStopTimeoutFuture != null) {
             mStopTimeoutFuture.cancel(true);
             mStopTimeoutFuture = null;
         }
-        mMediaCodecCallback = new MediaCodecCallback();
-        mMediaCodec.setCallback(mMediaCodecCallback);
+        mMediaCodec.setCallback(new MediaCodecCallback());
         mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 
         if (mEncoderInput instanceof SurfaceInput) {
@@ -516,15 +515,13 @@
             Futures.successfulAsList(futures).addListener(this::signalEndOfInputStream,
                     mEncoderExecutor);
         } else if (mEncoderInput instanceof SurfaceInput) {
-            if (DeviceQuirks.get(MediaCodecDoesNotSendEos.class) != null) {
-                requireNonNull(mMediaCodecCallback).onOutputBufferAvailable(mMediaCodec,
-                        FAKE_BUFFER_INDEX, createFakeEosBufferInfo());
-            } else {
-                try {
-                    mMediaCodec.signalEndOfInputStream();
-                } catch (MediaCodec.CodecException e) {
-                    handleEncodeError(e);
-                }
+            try {
+                mMediaCodec.signalEndOfInputStream();
+                // On some devices, MediaCodec#signalEndOfInputStream() doesn't work.
+                // See b/255209101.
+                mMediaCodecEosSignalled = true;
+            } catch (MediaCodec.CodecException e) {
+                handleEncodeError(e);
             }
         }
     }
@@ -891,7 +888,7 @@
 
     @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
-    long getAdjustedTimeUs(@NonNull MediaCodec.BufferInfo bufferInfo) {
+    long getAdjustedTimeUs(@NonNull BufferInfo bufferInfo) {
         long adjustedTimeUs;
         if (mTotalPausedDurationUs > 0L) {
             adjustedTimeUs = bufferInfo.presentationTimeUs - mTotalPausedDurationUs;
@@ -953,13 +950,6 @@
         }
     }
 
-    @NonNull
-    private MediaCodec.BufferInfo createFakeEosBufferInfo() {
-        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
-        bufferInfo.set(0, 0, generatePresentationTimeUs(), MediaCodec.BUFFER_FLAG_END_OF_STREAM);
-        return bufferInfo;
-    }
-
     @SuppressWarnings("WeakerAccess") // synthetic accessor
     @ExecutedBy("mEncoderExecutor")
     void matchAcquisitionsAndFreeBufferIndexes() {
@@ -997,12 +987,12 @@
     }
 
     @SuppressWarnings("WeakerAccess") // synthetic accessor
-    static boolean isKeyFrame(@NonNull MediaCodec.BufferInfo bufferInfo) {
+    static boolean isKeyFrame(@NonNull BufferInfo bufferInfo) {
         return (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
     }
 
     @SuppressWarnings("WeakerAccess") // synthetic accessor
-    static boolean isEndOfStream(@NonNull MediaCodec.BufferInfo bufferInfo) {
+    static boolean hasEndOfStreamFlag(@NonNull BufferInfo bufferInfo) {
         return (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
     }
 
@@ -1021,7 +1011,7 @@
          * The last sent presentation time of BufferInfo. The value could be adjusted by total
          * pause duration.
          */
-        private long mLastSentPresentationTimeUs = 0L;
+        private long mLastSentAdjustedTimeUs = 0L;
         private boolean mIsOutputBufferInPauseState = false;
         private boolean mIsKeyFrameRequired = false;
 
@@ -1065,7 +1055,7 @@
 
         @Override
         public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
-                @NonNull MediaCodec.BufferInfo bufferInfo) {
+                @NonNull BufferInfo bufferInfo) {
             mEncoderExecutor.execute(() -> {
                 switch (mState) {
                     case STARTED:
@@ -1099,22 +1089,11 @@
                             if (!mHasFirstData) {
                                 mHasFirstData = true;
                             }
-                            long adjustedTimeUs = getAdjustedTimeUs(bufferInfo);
-                            if (bufferInfo.presentationTimeUs != adjustedTimeUs) {
-                                // If adjusted time <= last sent time, the buffer should have been
-                                // detected and dropped in checkBufferInfo().
-                                Preconditions.checkState(
-                                        adjustedTimeUs > mLastSentPresentationTimeUs);
-                                bufferInfo.presentationTimeUs = adjustedTimeUs;
-                                if (DEBUG) {
-                                    Logger.d(mTag, "Adjust bufferInfo.presentationTimeUs to "
-                                            + DebugUtils.readableUs(adjustedTimeUs));
-                                }
-                            }
-                            mLastSentPresentationTimeUs = bufferInfo.presentationTimeUs;
+                            BufferInfo outBufferInfo = resolveOutputBufferInfo(bufferInfo);
+                            mLastSentAdjustedTimeUs = outBufferInfo.presentationTimeUs;
                             try {
                                 EncodedDataImpl encodedData = new EncodedDataImpl(mediaCodec, index,
-                                        bufferInfo);
+                                        outBufferInfo);
                                 sendEncodedData(encodedData, encoderCallback, executor);
                             } catch (MediaCodec.CodecException e) {
                                 handleEncodeError(e);
@@ -1160,6 +1139,26 @@
         }
 
         @ExecutedBy("mEncoderExecutor")
+        @NonNull
+        private BufferInfo resolveOutputBufferInfo(@NonNull BufferInfo bufferInfo) {
+            long adjustedTimeUs = getAdjustedTimeUs(bufferInfo);
+            if (bufferInfo.presentationTimeUs == adjustedTimeUs) {
+                return bufferInfo;
+            }
+
+            // If adjusted time <= last sent time, the buffer should have been detected and
+            // dropped in checkBufferInfo().
+            Preconditions.checkState(adjustedTimeUs > mLastSentAdjustedTimeUs);
+            if (DEBUG) {
+                Logger.d(mTag, "Adjust bufferInfo.presentationTimeUs to "
+                        + DebugUtils.readableUs(adjustedTimeUs));
+            }
+            BufferInfo newBufferInfo = new BufferInfo();
+            newBufferInfo.set(bufferInfo.offset, bufferInfo.size, adjustedTimeUs, bufferInfo.flags);
+            return newBufferInfo;
+        }
+
+        @ExecutedBy("mEncoderExecutor")
         private void sendEncodedData(@NonNull EncodedDataImpl encodedData,
                 @NonNull EncoderCallback callback, @NonNull Executor executor) {
             mEncodedDataSet.add(encodedData);
@@ -1191,12 +1190,12 @@
         }
 
         /**
-         * Checks the {@link android.media.MediaCodec.BufferInfo} and updates related states.
+         * Checks the {@link BufferInfo} and updates related states.
          *
          * @return {@code true} if the buffer is valid, otherwise {@code false}.
          */
         @ExecutedBy("mEncoderExecutor")
-        private boolean checkBufferInfo(@NonNull MediaCodec.BufferInfo bufferInfo) {
+        private boolean checkBufferInfo(@NonNull BufferInfo bufferInfo) {
             if (mHasEndData) {
                 Logger.d(mTag, "Drop buffer by already reach end of stream.");
                 return false;
@@ -1251,7 +1250,7 @@
             }
 
             // We should check if the adjusted time is valid. see b/189114207.
-            if (getAdjustedTimeUs(bufferInfo) <= mLastSentPresentationTimeUs) {
+            if (getAdjustedTimeUs(bufferInfo) <= mLastSentAdjustedTimeUs) {
                 Logger.d(mTag, "Drop buffer by adjusted time is less than the last sent time.");
                 if (mIsVideoEncoder && isKeyFrame(bufferInfo)) {
                     mIsKeyFrameRequired = true;
@@ -1275,10 +1274,21 @@
             return true;
         }
 
+        @ExecutedBy("mEncoderExecutor")
+        private boolean isEndOfStream(@NonNull BufferInfo bufferInfo) {
+            return hasEndOfStreamFlag(bufferInfo) || isEosSignalledAndStopTimeReached(bufferInfo);
+        }
+
+        @ExecutedBy("mEncoderExecutor")
+        private boolean isEosSignalledAndStopTimeReached(@NonNull BufferInfo bufferInfo) {
+            return mMediaCodecEosSignalled
+                    && bufferInfo.presentationTimeUs > mStartStopTimeRangeUs.getUpper();
+        }
+
         @SuppressWarnings("StatementWithEmptyBody") // to better organize the logic and comments
         @ExecutedBy("mEncoderExecutor")
         private boolean updatePauseRangeStateAndCheckIfBufferPaused(
-                @NonNull MediaCodec.BufferInfo bufferInfo) {
+                @NonNull BufferInfo bufferInfo) {
             updateTotalPausedDuration(bufferInfo.presentationTimeUs);
             boolean isInPauseRange = isInPauseRange(bufferInfo.presentationTimeUs);
             if (!mIsOutputBufferInPauseState && isInPauseRange) {
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
index d342afa..c553bc3 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
@@ -1072,7 +1072,7 @@
         val surfaceRequest = SurfaceRequest(
             DEFAULT_SURFACE_SIZE, fakeCamera,
             isRGBA8888Required
-        )
+        ) {}
         surfaceRequestList.add(surfaceRequest)
         return surfaceRequest
     }
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
index 0401800..3e0fede 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
@@ -68,7 +68,7 @@
         mParent = FrameLayout(mContext)
         setContentView(mParent)
 
-        mSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false)
+        mSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
         mImplementation = SurfaceViewImplementation(mParent, PreviewTransformation())
     }
 
@@ -92,7 +92,7 @@
         val previousSurfaceView = mImplementation.mSurfaceView
 
         // Act.
-        val sameResolutionSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false)
+        val sameResolutionSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
         mImplementation.testSurfaceRequest(sameResolutionSurfaceRequest)
         val newSurfaceView = mImplementation.mSurfaceView
 
@@ -109,7 +109,8 @@
 
         // Act.
         val differentSize: Size by lazy { Size(720, 480) }
-        val differentResolutionSurfaceRequest = SurfaceRequest(differentSize, FakeCamera(), false)
+        val differentResolutionSurfaceRequest =
+            SurfaceRequest(differentSize, FakeCamera(), false) {}
         mImplementation.testSurfaceRequest(differentResolutionSurfaceRequest)
         val newSurfaceView = mImplementation.mSurfaceView
 
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
index f69e55e..c6998bd 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
@@ -50,7 +50,7 @@
     private val surfaceRequest: SurfaceRequest
         get() {
             if (_surfaceRequest == null) {
-                _surfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false)
+                _surfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
             }
             return _surfaceRequest!!
         }
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 8963ecc..1d141c6 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
@@ -1006,8 +1006,10 @@
         if (mImplementation instanceof TextureViewImplementation) {
             matrix.postConcat(getMatrix());
         } else {
-            Logger.w(TAG, "PreviewView needs to be in COMPATIBLE mode for the transform"
-                    + " to work correctly.");
+            if (!getMatrix().isIdentity()) {
+                Logger.w(TAG, "PreviewView needs to be in COMPATIBLE mode for the transform"
+                        + " to work correctly.");
+            }
         }
 
         return new OutputTransform(matrix, new Size(surfaceCropRect.width(),
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/SurfaceViewImplementation.java b/camera/camera-view/src/main/java/androidx/camera/view/SurfaceViewImplementation.java
index f2d3255..dcd4fec 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/SurfaceViewImplementation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/SurfaceViewImplementation.java
@@ -77,6 +77,12 @@
                     ContextCompat.getMainExecutor(mSurfaceView.getContext()),
                     onSurfaceNotInUseListener::onSurfaceNotInUse);
         }
+
+        // Note that View.post will add the Runnable to SurfaceView's message queue. This means
+        // that if this line is called while the SurfaceView is detached from window,
+        // "setSurfaceRequest" will be pending til the SurfaceView is attached to window and its
+        // view is prepared. In other words, "setSurfaceRequest" will happen after
+        // "surfaceCreated" is triggered.
         mSurfaceView.post(() -> mSurfaceRequestCallback.setSurfaceRequest(surfaceRequest,
                 onSurfaceNotInUseListener));
     }
@@ -161,6 +167,9 @@
         private SurfaceRequest mSurfaceRequest;
 
         @Nullable
+        private SurfaceRequest mSurfaceRequestToBeInvalidated;
+
+        @Nullable
         private OnSurfaceNotInUseListener mOnSurfaceNotInUseListener;
 
         // The cached size of the current Surface.
@@ -171,6 +180,8 @@
         // Guarded by the UI thread.
         private boolean mWasSurfaceProvided = false;
 
+        private boolean mNeedToInvalidate = false;
+
         /**
          * Sets the completer and the size. The completer will only be set if the current size of
          * the Surface matches the target size.
@@ -181,17 +192,29 @@
             // Cancel the previous request, if any
             cancelPreviousRequest();
 
-            mSurfaceRequest = surfaceRequest;
-            mOnSurfaceNotInUseListener = onSurfaceNotInUseListener;
-            Size targetSize = surfaceRequest.getResolution();
-            mTargetSize = targetSize;
-            mWasSurfaceProvided = false;
+            if (mNeedToInvalidate) {
+                // In some edge cases, the DeferrableSurface behind the SurfaceRequest is timed-out.
+                // Since we can not tell if the timeout happened, we invalidate the
+                // SurfaceRequest to get a new one when the situation is abnormal. (Normally,
+                // invalidate is called when the surface is recreated.)
+                // It's not ideal to track the "timed out" state of the SurfaceRequest this way.
+                // A better way would be making it part of SurfaceRequest. e.g. something like
+                // SurfaceRequest.isTimedOut().
+                mNeedToInvalidate = false;
+                surfaceRequest.invalidate();
+            } else {
+                mSurfaceRequest = surfaceRequest;
+                mOnSurfaceNotInUseListener = onSurfaceNotInUseListener;
+                Size targetSize = surfaceRequest.getResolution();
+                mTargetSize = targetSize;
+                mWasSurfaceProvided = false;
 
-            if (!tryToComplete()) {
-                // The current size is incorrect. Wait for it to change.
-                Logger.d(TAG, "Wait for new Surface creation.");
-                mSurfaceView.getHolder().setFixedSize(targetSize.getWidth(),
-                        targetSize.getHeight());
+                if (!tryToComplete()) {
+                    // The current size is incorrect. Wait for it to change.
+                    Logger.d(TAG, "Wait for new Surface creation.");
+                    mSurfaceView.getHolder().setFixedSize(targetSize.getWidth(),
+                            targetSize.getHeight());
+                }
             }
         }
 
@@ -239,9 +262,9 @@
 
         @UiThread
         @SuppressWarnings("ObjectToString")
-        private void invalidateSurface() {
+        private void closeSurface() {
             if (mSurfaceRequest != null) {
-                Logger.d(TAG, "Surface invalidated " + mSurfaceRequest);
+                Logger.d(TAG, "Surface closed " + mSurfaceRequest);
                 mSurfaceRequest.getDeferrableSurface().close();
             }
         }
@@ -249,7 +272,14 @@
         @Override
         public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
             Logger.d(TAG, "Surface created.");
-            // No-op. Handling surfaceChanged() is enough because it's always called afterwards.
+
+            // Invalidate the surface request so that the requester is notified that the previously
+            // obtained surface is no longer valid and should request a new one.
+            if (mNeedToInvalidate && mSurfaceRequestToBeInvalidated != null) {
+                mSurfaceRequestToBeInvalidated.invalidate();
+                mSurfaceRequestToBeInvalidated = null;
+                mNeedToInvalidate = false;
+            }
         }
 
         @Override
@@ -264,14 +294,21 @@
         public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
             Logger.d(TAG, "Surface destroyed.");
 
-            // If a surface was already provided to the camera, invalidate it so that it requests
-            // a new valid one. Otherwise, cancel the surface request.
+            // If a surface was already provided to the camera, close the surface. Otherwise,
+            // cancel the surface request.
             if (mWasSurfaceProvided) {
-                invalidateSurface();
+                closeSurface();
             } else {
                 cancelPreviousRequest();
             }
 
+            // The surface is no longer valid. The surface request will be invalidated when the new
+            // surface is ready so that the requester can get the new one.
+            mNeedToInvalidate = true;
+            if (mSurfaceRequest != null) {
+                mSurfaceRequestToBeInvalidated = mSurfaceRequest;
+            }
+
             // Reset state
             mWasSurfaceProvided = false;
             mSurfaceRequest = null;
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
index 0ca07ad..804de8a 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
@@ -140,6 +140,6 @@
         cameraInfoInternal.setImplementationType(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
         return new SurfaceRequest(new Size(800, 600),
                 new FakeCamera(null, cameraInfoInternal),
-                /*isRGB8888Required*/ false);
+                /*isRGB8888Required*/ false, () -> {});
     }
 }
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
index 9f5d0e3..4930577 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
@@ -25,6 +25,8 @@
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.CameraSelector
 import androidx.camera.integration.uiwidgets.R
+import androidx.camera.integration.uiwidgets.viewpager.BaseActivity.Companion.COMPATIBLE_MODE
+import androidx.camera.integration.uiwidgets.viewpager.BaseActivity.Companion.PERFORMANCE_MODE
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
@@ -57,27 +59,37 @@
 
 @RunWith(Parameterized::class)
 @LargeTest
-class ViewPager2ActivityTest(private val lensFacing: Int, private val cameraXConfig: String) {
+class ViewPager2ActivityTest(
+    private val lensFacing: Int,
+    private val implementationMode: Int,
+    private val cameraXConfig: String
+) {
 
     companion object {
         private const val ACTION_IDLE_TIMEOUT: Long = 5000
+        private const val PREVIEW_UPDATE_COUNT = 10
 
         @JvmStatic
         private val lensFacingList =
             arrayOf(CameraSelector.LENS_FACING_BACK, CameraSelector.LENS_FACING_FRONT)
 
         @JvmStatic
+        private val implementationModeList = arrayOf(COMPATIBLE_MODE, PERFORMANCE_MODE)
+
+        @JvmStatic
         private val cameraXConfigList = arrayOf(
             CameraFragment.CAMERA2_IMPLEMENTATION_OPTION,
             CameraFragment.CAMERA_PIPE_IMPLEMENTATION_OPTION
         )
 
         @JvmStatic
-        @Parameterized.Parameters(name = "lensFacing={0}, cameraXConfig={1}")
+        @Parameterized.Parameters(name = "lensFacing={0}, mode={1}, cameraXConfig={2}")
         fun data() = mutableListOf<Array<Any?>>().apply {
             lensFacingList.forEach { lens ->
-                cameraXConfigList.forEach { cameraXConfig ->
-                    add(arrayOf(lens, cameraXConfig))
+                implementationModeList.forEach { mode ->
+                    cameraXConfigList.forEach { cameraXConfig ->
+                        add(arrayOf(lens, mode, cameraXConfig))
+                    }
                 }
             }
         }
@@ -139,7 +151,7 @@
 
             assertStreamState(scenario, PreviewView.StreamState.STREAMING)
             // Make sure the surface texture of TextureView continues getting updates.
-            assertSurfaceTextureFramesUpdate(scenario)
+            assertPreviewViewUpdate(scenario)
         }
     }
 
@@ -161,7 +173,7 @@
 
             // For b/149877652, need to check if the surface texture of TextureView continues
             // getting updates after detaching from window and then attaching to window.
-            assertSurfaceTextureFramesUpdate(scenario)
+            assertPreviewViewUpdate(scenario)
         }
     }
 
@@ -188,7 +200,7 @@
 
             // The test covers pause/resume and ViewPager2 switch behaviors. Hence, need to
             // check the surface texture of TextureView continues getting updates for b/149877652.
-            assertSurfaceTextureFramesUpdate(scenario)
+            assertPreviewViewUpdate(scenario)
         }
     }
 
@@ -198,14 +210,15 @@
     ):
         ActivityScenario<ViewPager2Activity> {
             val intent = Intent(
-                ApplicationProvider.getApplicationContext<Context>(),
+                ApplicationProvider.getApplicationContext(),
                 ViewPager2Activity::class.java
             ).apply {
                 putExtra(BaseActivity.INTENT_LENS_FACING, lensFacing)
+                putExtra(BaseActivity.INTENT_IMPLEMENTATION_MODE, implementationMode)
                 putExtra(CameraFragment.KEY_CAMERA_IMPLEMENTATION, cameraXConfig)
                 putExtra(CameraFragment.KEY_CAMERA_IMPLEMENTATION_NO_HISTORY, true)
             }
-            return ActivityScenario.launch<ViewPager2Activity>(intent)
+            return ActivityScenario.launch(intent)
         }
 
     private fun getTextureView(previewView: PreviewView): TextureView? {
@@ -238,6 +251,14 @@
         assertThat(result.await()).isTrue()
     }
 
+    private fun assertPreviewViewUpdate(scenario: ActivityScenario<ViewPager2Activity>) {
+        when (implementationMode) {
+            COMPATIBLE_MODE -> assertSurfaceTextureFramesUpdate(scenario)
+            PERFORMANCE_MODE -> assertPreviewUpdate(scenario)
+            else -> throw IllegalArgumentException()
+        }
+    }
+
     private fun assertSurfaceTextureFramesUpdate(scenario: ActivityScenario<ViewPager2Activity>) {
         var newSurfaceTexture: SurfaceTexture? = null
         lateinit var previewView: PreviewView
@@ -253,4 +274,24 @@
         }
         assertThat(latchForFrameUpdate.await(ACTION_IDLE_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
     }
+
+    /**
+     * Checks update from Preview instead of SurfaceView, since the SurfaceView's content can not
+     * be got.
+     */
+    private fun assertPreviewUpdate(scenario: ActivityScenario<ViewPager2Activity>) {
+        val latch = CountDownLatch(PREVIEW_UPDATE_COUNT)
+        getCameraFragment(scenario)?.setPreviewUpdatingLatch(latch)
+        assertThat(latch.await(ACTION_IDLE_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    private fun getCameraFragment(scenario: ActivityScenario<ViewPager2Activity>): CameraFragment? {
+        var fragment: CameraFragment? = null
+        scenario.onActivity { activity ->
+            val fragments = activity.supportFragmentManager.fragments
+            fragment = fragments.firstOrNull { it is CameraFragment } as? CameraFragment
+        }
+
+        return fragment
+    }
 }
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/BaseActivity.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/BaseActivity.kt
index 32047a0..2a54dfc 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/BaseActivity.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/BaseActivity.kt
@@ -31,6 +31,9 @@
         private const val TAG = "BasicActivity"
         private const val LATCH_TIMEOUT: Long = 5000
         internal const val INTENT_LENS_FACING = "lens-facing"
+        internal const val INTENT_IMPLEMENTATION_MODE = "implementation-mode"
+        internal const val PERFORMANCE_MODE = 0
+        internal const val COMPATIBLE_MODE = 1
     }
 
     // The expected final streamState
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/CameraFragment.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/CameraFragment.kt
index 4e6ae5b..964d3d8 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/CameraFragment.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/CameraFragment.kt
@@ -18,6 +18,9 @@
 
 import android.content.Context
 import android.content.Intent
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.TotalCaptureResult
 import android.os.Bundle
 import android.text.TextUtils
 import android.util.Log
@@ -25,18 +28,24 @@
 import android.view.View
 import android.view.ViewGroup
 import androidx.annotation.OptIn
+import androidx.annotation.VisibleForTesting
 import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.interop.Camera2Interop
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.Preview
 import androidx.camera.integration.uiwidgets.databinding.FragmentTextureviewBinding
+import androidx.camera.integration.uiwidgets.viewpager.BaseActivity.Companion.COMPATIBLE_MODE
+import androidx.camera.integration.uiwidgets.viewpager.BaseActivity.Companion.PERFORMANCE_MODE
 import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration
 import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.view.PreviewView
+import androidx.camera.view.PreviewView.ImplementationMode
 import androidx.core.content.ContextCompat
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.Lifecycle
 import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.CountDownLatch
 
 /** A Fragment that displays a {@link PreviewView} with TextureView mode. */
 class CameraFragment : Fragment() {
@@ -57,6 +66,9 @@
     private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
     private lateinit var cameraProvider: ProcessCameraProvider
 
+    // for testing preview updates
+    private var previewUpdatingLatch: CountDownLatch? = null
+
     @OptIn(ExperimentalCameraProviderConfiguration::class)
     override fun onAttach(context: Context) {
         super.onAttach(context)
@@ -128,13 +140,15 @@
     private fun bindPreview() {
         Log.d(TAG, "bindPreview")
 
-        val preview = Preview.Builder()
+        val previewBuilder = Preview.Builder()
+        previewBuilder.addCaptureCompletedCallback()
+        val preview = previewBuilder
             .setTargetName("Preview")
             .build()
 
         cameraProvider.bindToLifecycle(this, getCameraSelector(), preview)
 
-        binding.previewTextureview.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
+        binding.previewTextureview.implementationMode = getImplementationMode()
         preview.setSurfaceProvider(binding.previewTextureview.surfaceProvider)
     }
 
@@ -148,4 +162,46 @@
             .requireLensFacing(lensFacing)
             .build()
     }
+
+    /**
+     * Returns the implementation mode from the intent, or return the compatibility mode if not set.
+     */
+    private fun getImplementationMode(): ImplementationMode {
+        val mode = (requireActivity() as BaseActivity).intent.getIntExtra(
+            BaseActivity.INTENT_IMPLEMENTATION_MODE, COMPATIBLE_MODE
+        )
+
+        return when (mode) {
+            PERFORMANCE_MODE -> ImplementationMode.PERFORMANCE
+            else -> ImplementationMode.COMPATIBLE
+        }
+    }
+
+    /**
+     * Implements preview updating latch with interop to workaround the situation that SurfaceView's
+     * content can not be got.
+     */
+    @OptIn(ExperimentalCamera2Interop::class)
+    private fun Preview.Builder.addCaptureCompletedCallback() {
+        val captureCallback = object : CameraCaptureSession.CaptureCallback() {
+            override fun onCaptureCompleted(
+                session: CameraCaptureSession,
+                request: CaptureRequest,
+                result: TotalCaptureResult
+            ) {
+                super.onCaptureCompleted(session, request, result)
+
+                if (previewUpdatingLatch != null) {
+                    previewUpdatingLatch!!.countDown()
+                }
+            }
+        }
+
+        Camera2Interop.Extender(this).setSessionCaptureCallback(captureCallback)
+    }
+
+    @VisibleForTesting
+    fun setPreviewUpdatingLatch(latch: CountDownLatch) {
+        previewUpdatingLatch = latch
+    }
 }
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
index eeefe36..b451e89 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
@@ -35,6 +35,7 @@
 import static android.car.VehiclePropertyIds.RANGE_REMAINING;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.hardware.common.CarValue.STATUS_UNKNOWN;
 import static androidx.car.app.hardware.common.CarValueUtils.getCarValue;
 
 import static java.util.Objects.requireNonNull;
@@ -97,7 +98,6 @@
     // VEHICLE_SPEED_DISPLAY_UNIT in VehiclePropertyIds. The property is added after Android Q.
     public static final int SPEED_DISPLAY_UNIT_ID = 289408516;
 
-    private static final float UNKNOWN_CAPACITY = Float.NEGATIVE_INFINITY;
     static final ImmutableMap<Integer, List<CarZone>> ENERGY_LEVEL_REQUEST = ImmutableMap.<Integer,
                     List<CarZone>>builder()
             .put(EV_BATTERY_LEVEL, GLOBAL_CAR_ZONE)
@@ -284,14 +284,12 @@
                 return;
             }
             for (CarPropertyResponse<?> carPropertyResponse : carPropertyResponses) {
-                if (carPropertyResponse.getPropertyId() == INFO_EV_BATTERY_CAPACITY
-                        && carPropertyResponse.getValue() != null) {
-                    energyLevelListener.updateEvBatteryCapacity(
-                            (Float) carPropertyResponse.getValue());
+                if (carPropertyResponse.getPropertyId() == INFO_EV_BATTERY_CAPACITY) {
+                    energyLevelListener.updateEvBatteryCapacityPropertyResponse(
+                            carPropertyResponse);
                 }
-                if (carPropertyResponse.getPropertyId() == INFO_FUEL_CAPACITY
-                        && carPropertyResponse.getValue() != null) {
-                    energyLevelListener.updateFuelCapacity((Float) carPropertyResponse.getValue());
+                if (carPropertyResponse.getPropertyId() == INFO_FUEL_CAPACITY) {
+                    energyLevelListener.updateFuelCapacityPropertyResponse(carPropertyResponse);
                 }
             }
         }, executor);
@@ -563,20 +561,25 @@
         private final OnCarDataAvailableListener<EnergyLevel>
                 mEnergyLevelOnCarDataAvailableListener;
         private final Executor mExecutor;
-        private float mEvBatteryCapacity = UNKNOWN_CAPACITY;
-        private float mFuelCapacity = UNKNOWN_CAPACITY;
+        private CarPropertyResponse<?> mEvBatteryCapacityPropertyResponse = CarPropertyResponse
+                .builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_UNKNOWN)
+                .build();
+        private CarPropertyResponse<?> mFuelCapacityPropertyResponse = CarPropertyResponse.builder()
+                .setPropertyId(INFO_FUEL_CAPACITY).setStatus(STATUS_UNKNOWN).build();
 
         EnergyLevelListener(OnCarDataAvailableListener<EnergyLevel> listener, Executor executor) {
             mEnergyLevelOnCarDataAvailableListener = listener;
             mExecutor = executor;
         }
 
-        void updateEvBatteryCapacity(float evBatteryCapacity) {
-            mEvBatteryCapacity = evBatteryCapacity;
+        void updateEvBatteryCapacityPropertyResponse(
+                CarPropertyResponse<?> evBatteryCapacityPropertyResponse) {
+            mEvBatteryCapacityPropertyResponse = evBatteryCapacityPropertyResponse;
         }
 
-        void updateFuelCapacity(float fuelCapacity) {
-            mFuelCapacity = fuelCapacity;
+        void updateFuelCapacityPropertyResponse(
+                CarPropertyResponse<?> fuelCapacityPropertyResponse) {
+            mFuelCapacityPropertyResponse = fuelCapacityPropertyResponse;
         }
 
         // TODO(b/202303614): Remove this annotation once FuelVolumeDisplayUnit is ready.
@@ -589,29 +592,27 @@
                 for (CarPropertyResponse<?> response : carPropertyResponses) {
                     switch (response.getPropertyId()) {
                         case EV_BATTERY_LEVEL:
-                            if (mEvBatteryCapacity == UNKNOWN_CAPACITY) {
-                                Log.w(LogTags.TAG_CAR_HARDWARE, "EV battery capacity is still "
-                                        + "unknown, skipping EV_BATTERY_LEVEL update");
-                                continue;
-                            }
-                            if (response.getValue() != null) {
-                                energyLevelBuilder.setBatteryPercent(getCarValue(response,
-                                        (Float) response.getValue() / mEvBatteryCapacity * 100));
-                            } else {
+                            if (response.getValue() == null) {
                                 energyLevelBuilder.setBatteryPercent(getCarValue(response));
+                            } else if (mEvBatteryCapacityPropertyResponse.getValue() == null) {
+                                energyLevelBuilder.setBatteryPercent(
+                                        getCarValue(mEvBatteryCapacityPropertyResponse));
+                            } else {
+                                energyLevelBuilder.setBatteryPercent(getCarValue(response,
+                                        (Float) response.getValue() / (Float)
+                                        mEvBatteryCapacityPropertyResponse.getValue() * 100));
                             }
                             break;
                         case FUEL_LEVEL:
-                            if (mFuelCapacity == UNKNOWN_CAPACITY) {
-                                Log.w(LogTags.TAG_CAR_HARDWARE, "Fuel capacity is still unknown, "
-                                        + "skipping FUEL_LEVEL update");
-                                continue;
-                            }
-                            if (response.getValue() != null) {
-                                energyLevelBuilder.setFuelPercent(getCarValue(response,
-                                        (Float) response.getValue() / mFuelCapacity * 100));
-                            } else {
+                            if (response.getValue() == null) {
                                 energyLevelBuilder.setFuelPercent(getCarValue(response));
+                            } else if (mFuelCapacityPropertyResponse.getValue() == null) {
+                                energyLevelBuilder.setFuelPercent(
+                                        getCarValue(mFuelCapacityPropertyResponse));
+                            } else {
+                                energyLevelBuilder.setFuelPercent(getCarValue(response,
+                                        (Float) response.getValue() / (Float)
+                                        mFuelCapacityPropertyResponse.getValue() * 100));
                             }
                             break;
                         case FUEL_LEVEL_LOW:
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
index e6818f2..55a1dba 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
@@ -36,6 +36,7 @@
 
 import static androidx.car.app.hardware.common.CarValue.STATUS_SUCCESS;
 import static androidx.car.app.hardware.common.CarValue.STATUS_UNAVAILABLE;
+import static androidx.car.app.hardware.common.CarValue.STATUS_UNIMPLEMENTED;
 import static androidx.car.app.hardware.common.CarValue.STATUS_UNKNOWN;
 import static androidx.car.app.hardware.info.AutomotiveCarInfo.DEFAULT_SAMPLE_RATE;
 import static androidx.car.app.hardware.info.AutomotiveCarInfo.SPEED_DISPLAY_UNIT_ID;
@@ -118,6 +119,62 @@
     private static final List<CarZone> GLOBAL_ZONE = Collections.singletonList(
             CarZone.CAR_ZONE_GLOBAL);
 
+    private static final int METER_DISTANCE_UNIT = 0x21;
+    private static final int METER_VOLUME_UNIT = 0x40;
+    private static final float EV_BATTERY_CAPACITY = 100f;
+    private static final float EV_BATTERY_LEVEL_VALUE = 50f;
+    private static final float FUEL_CAPACITY = 120f;
+    private static final float FUEL_LEVEL_VALUE = 50f;
+    private static final long DEFAULT_TIMESTAMP_MILLIS = 1L;
+    private static final boolean FUEL_LEVEL_LOW_VALUE = true;
+    private static final float RANGE_REMAINING_VALUE = 5f;
+    private static final CarPropertyResponse<?> EV_BATTERY_CAPACITY_RESPONSE_SUCCESS =
+            CarPropertyResponse.builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(
+                    STATUS_SUCCESS).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).setValue(
+                    EV_BATTERY_CAPACITY).build();
+    private static final CarPropertyResponse<?> FUEL_CAPACITY_RESPONSE_SUCCESS =
+            CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
+                    STATUS_SUCCESS).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).setValue(
+                    FUEL_CAPACITY).build();
+    private static final CarPropertyResponse<?> EV_BATTERY_LEVEL_RESPONSE_SUCCESS =
+            CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
+                    STATUS_SUCCESS).setValue(EV_BATTERY_LEVEL_VALUE).setTimestampMillis(
+                    DEFAULT_TIMESTAMP_MILLIS).build();
+    private static final CarPropertyResponse<?> FUEL_LEVEL_RESPONSE_SUCCESS =
+            CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
+                    STATUS_SUCCESS).setValue(FUEL_LEVEL_VALUE).setTimestampMillis(
+                    DEFAULT_TIMESTAMP_MILLIS).build();
+    private static final CarPropertyResponse<?> FUEL_LEVEL_LOW_RESPONSE_SUCCESS =
+            CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
+                    STATUS_SUCCESS).setValue(FUEL_LEVEL_LOW_VALUE).setTimestampMillis(
+                    DEFAULT_TIMESTAMP_MILLIS).build();
+    private static final CarPropertyResponse<?> RANGE_REMAINING_RESPONSE_SUCCESS =
+            CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
+                    STATUS_SUCCESS).setValue(RANGE_REMAINING_VALUE).setTimestampMillis(
+                    DEFAULT_TIMESTAMP_MILLIS).build();
+    private static final CarPropertyResponse<?> DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS =
+            CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
+                    STATUS_SUCCESS).setValue(METER_DISTANCE_UNIT).setTimestampMillis(
+                    DEFAULT_TIMESTAMP_MILLIS).build();
+    private static final CarPropertyResponse<?> FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS =
+            CarPropertyResponse.builder().setPropertyId(FUEL_VOLUME_DISPLAY_UNITS).setStatus(
+                    STATUS_SUCCESS).setValue(METER_VOLUME_UNIT).setTimestampMillis(
+                    DEFAULT_TIMESTAMP_MILLIS).build();
+    private static final CarValue<Float> EXPECTED_BATTERY_PERCENT_SUCCESS =
+            new CarValue<>(EV_BATTERY_LEVEL_VALUE / EV_BATTERY_CAPACITY * 100,
+                    DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+    private static final CarValue<Float> EXPECTED_FUEL_PERCENT_SUCCESS =
+            new CarValue<>(FUEL_LEVEL_VALUE / FUEL_CAPACITY * 100, DEFAULT_TIMESTAMP_MILLIS,
+                    STATUS_SUCCESS);
+    private static final CarValue<Boolean> EXPECTED_ENERGY_IS_LOW_SUCCESS =
+            new CarValue<>(FUEL_LEVEL_LOW_VALUE, DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+    private static final CarValue<Float> EXPECTED_RANGE_REMAINING_METERS_SUCCESS =
+            new CarValue<>(RANGE_REMAINING_VALUE, DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+    private static final CarValue<Integer> EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS =
+            new CarValue<>(2, DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+    private static final CarValue<Integer> EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS =
+            new CarValue<>(201, DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+
     @Before
     public void setUp() {
         ShadowCar.setCar(mCarMock);
@@ -857,27 +914,15 @@
         assertThat(loadedResult.get()).isEqualTo(new Speed.Builder().build());
     }
 
-    @Test
-    public void getEnergyLevel_verifyResponse() throws InterruptedException {
+    private void getEnergyLevelHelperFunction(List<CarPropertyResponse<?>> energyCapacities,
+            List<CarPropertyResponse<?>> energyResponses, EnergyLevel expectedEnergyLevel) throws
+            InterruptedException {
         // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
         mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
         mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
 
-        ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
-                OnCarPropertyResponseListener.class);
-        int meterDistanceUnit = 0x21;
-        int meterVolumeUnit = 0x40;
-        float evBatteryCapacity = 100f;
-        float evBatteryLevelValue = 50f;
-        float fuelCapacity = 120f;
-        float fuelLevelValue = 50f;
-        List<CarPropertyResponse<?>> capacities = new ArrayList<>();
-        capacities.add(CarPropertyResponse.builder().setPropertyId(
-                INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_SUCCESS).setValue(
-                evBatteryCapacity).setTimestampMillis(1L).build());
-        capacities.add(CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
-                STATUS_SUCCESS).setValue(fuelCapacity).setTimestampMillis(1L).build());
-        ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(capacities);
+        ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(
+                energyCapacities);
         when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
                 future);
 
@@ -901,309 +946,273 @@
                         .put(DISTANCE_DISPLAY_UNITS, mCarZones)
                         .put(FUEL_VOLUME_DISPLAY_UNITS, mCarZones)
                         .buildKeepingLast();
+        ArgumentCaptor<OnCarPropertyResponseListener> listenerCaptor = ArgumentCaptor.forClass(
+                OnCarPropertyResponseListener.class);
 
-        verify(mPropertyManager, times(1)).submitGetPropertyRequest(eq(mGetPropertyRequests),
-                eq(mExecutor));
-        verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
-                eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
-                eq(mExecutor));
+        verify(mPropertyManager).submitGetPropertyRequest(mGetPropertyRequests, mExecutor);
+        verify(mPropertyManager).submitRegisterListenerRequest(eq(propertyIdsWithCarZones),
+                eq(DEFAULT_SAMPLE_RATE), listenerCaptor.capture(), eq(mExecutor));
 
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
-                STATUS_SUCCESS).setValue(evBatteryLevelValue).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
-                STATUS_SUCCESS).setValue(fuelLevelValue).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
-                STATUS_SUCCESS).setValue(true).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
-                STATUS_SUCCESS).setValue(5f).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
-                STATUS_SUCCESS).setValue(meterDistanceUnit).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(
-                FUEL_VOLUME_DISPLAY_UNITS).setStatus(STATUS_SUCCESS).setValue(
-                meterVolumeUnit).setTimestampMillis(1L).build());
+        mResponse.addAll(energyResponses);
 
-        captor.getValue().onCarPropertyResponses(mResponse);
+        listenerCaptor.getValue().onCarPropertyResponses(mResponse);
         mCountDownLatch.await();
 
-        EnergyLevel energyLevel = loadedResult.get();
-        assertThat(energyLevel.getBatteryPercent().getValue()).isEqualTo(
-                evBatteryLevelValue / evBatteryCapacity * 100);
-        assertThat(energyLevel.getFuelPercent().getValue()).isEqualTo(
-                fuelLevelValue / fuelCapacity * 100);
-        assertThat(energyLevel.getEnergyIsLow().getValue()).isEqualTo(true);
-        assertThat(energyLevel.getRangeRemainingMeters().getValue()).isEqualTo(5f);
-        assertThat(energyLevel.getDistanceDisplayUnit().getValue()).isEqualTo(2);
-        assertThat(energyLevel.getFuelVolumeDisplayUnit().getValue()).isEqualTo(201);
+        assertThat(loadedResult.get()).isEqualTo(expectedEnergyLevel);
+    }
+
+    @Test
+    public void getEnergyLevel_verifyResponse() throws InterruptedException {
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, FUEL_CAPACITY_RESPONSE_SUCCESS);
+
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+                EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
     }
 
     @Test
     public void addEnergyLevelListener_handlesReponsesWithDifferentStatuses()
             throws InterruptedException {
-        // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
-        mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
-        mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, FUEL_CAPACITY_RESPONSE_SUCCESS);
 
-        ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
-                OnCarPropertyResponseListener.class);
-        int meterDistanceUnit = 0x21;
-        float evBatteryCapacity = 100f;
-        float fuelCapacity = 120f;
-        List<CarPropertyResponse<?>> capacities = new ArrayList<>();
-        capacities.add(CarPropertyResponse.builder().setPropertyId(
-                INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_SUCCESS).setValue(
-                evBatteryCapacity).setTimestampMillis(1L).build());
-        capacities.add(CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
-                STATUS_SUCCESS).setValue(fuelCapacity).setTimestampMillis(1L).build());
-        ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(capacities);
-        when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
-                future);
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
+                        STATUS_UNKNOWN).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+                CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
+                        STATUS_UNAVAILABLE).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS,
+                CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
+                        STATUS_UNKNOWN).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                CarPropertyResponse.builder().setPropertyId(FUEL_VOLUME_DISPLAY_UNITS).setStatus(
+                        STATUS_UNAVAILABLE).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build());
 
-        AtomicReference<EnergyLevel> loadedResult = new AtomicReference<>();
-        OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
-            loadedResult.set(data);
-            mCountDownLatch.countDown();
-        };
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNKNOWN)).setFuelPercent(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNAVAILABLE)).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS,
+                        STATUS_UNKNOWN)).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNAVAILABLE)).build();
 
-        mAutomotiveCarInfo.addEnergyLevelListener(mExecutor, listener);
-
-        // Create "EV_BATTERY_LEVEL", "FUEL_LEVEL", "FUEL_LEVEL_LOW", "RANGE_REMAINING",
-        // "DISTANCE_DISPLAY_UNITS" and "FUEL_VOLUME_DISPLAY_UNITS" property IDs list with car
-        // zones.
-        Map<Integer, List<CarZone>> propertyIdsWithCarZones =
-                ImmutableMap.<Integer, List<CarZone>>builder().put(EV_BATTERY_LEVEL, mCarZones).put(
-                        FUEL_LEVEL, mCarZones).put(FUEL_LEVEL_LOW, mCarZones).put(RANGE_REMAINING,
-                        mCarZones).put(DISTANCE_DISPLAY_UNITS, mCarZones).put(
-                        FUEL_VOLUME_DISPLAY_UNITS, mCarZones).buildKeepingLast();
-
-        verify(mPropertyManager, times(1)).submitGetPropertyRequest(eq(mGetPropertyRequests),
-                eq(mExecutor));
-        verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
-                eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
-                eq(mExecutor));
-
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
-                STATUS_UNKNOWN).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
-                STATUS_UNAVAILABLE).setTimestampMillis(2L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
-                STATUS_SUCCESS).setValue(true).setTimestampMillis(3L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
-                STATUS_UNKNOWN).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
-                STATUS_SUCCESS).setValue(meterDistanceUnit).setTimestampMillis(4L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(
-                FUEL_VOLUME_DISPLAY_UNITS).setStatus(STATUS_UNAVAILABLE).setTimestampMillis(
-                6L).build());
-
-        captor.getValue().onCarPropertyResponses(mResponse);
-        mCountDownLatch.await();
-
-        assertThat(loadedResult.get()).isEqualTo(new EnergyLevel.Builder().setBatteryPercent(
-                new CarValue<>(null, 1, STATUS_UNKNOWN)).setFuelPercent(
-                new CarValue<>(null, 2, STATUS_UNAVAILABLE)).setEnergyIsLow(
-                new CarValue<>(true, 3, STATUS_SUCCESS)).setRangeRemainingMeters(
-                new CarValue<>(null, 1, STATUS_UNKNOWN)).setFuelVolumeDisplayUnit(
-                new CarValue<>(null, 6, STATUS_UNAVAILABLE)).setDistanceDisplayUnit(
-                new CarValue<>(CarUnit.METER, 4, STATUS_SUCCESS)).build());
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
     }
 
     @Test
     public void addEnergyLevelListener_returnsEnergyLevelWithUnknownValuesIfNoResponses()
             throws InterruptedException {
-        // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
-        mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
-        mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
-        List<CarPropertyResponse<?>> capacities = new ArrayList<>();
-        when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
-                Futures.immediateFuture(capacities));
-        AtomicReference<EnergyLevel> loadedResult = new AtomicReference<>();
-        OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
-            loadedResult.set(data);
-            mCountDownLatch.countDown();
-        };
+        List<CarPropertyResponse<?>> energyCapacities = new ArrayList<>();
 
-        mAutomotiveCarInfo.addEnergyLevelListener(mExecutor, listener);
+        List<CarPropertyResponse<?>> energyResponses = new ArrayList<>();
 
-        verify(mPropertyManager, times(1)).submitGetPropertyRequest(
-                eq(mGetPropertyRequests), eq(mExecutor));
-        // Create "EV_BATTERY_LEVEL", "FUEL_LEVEL", "FUEL_LEVEL_LOW", "RANGE_REMAINING",
-        // "DISTANCE_DISPLAY_UNITS" and "FUEL_VOLUME_DISPLAY_UNITS" property IDs list with car
-        // zones.
-        Map<Integer, List<CarZone>> propertyIdsWithCarZones =
-                ImmutableMap.<Integer, List<CarZone>>builder()
-                        .put(EV_BATTERY_LEVEL, mCarZones)
-                        .put(FUEL_LEVEL, mCarZones)
-                        .put(FUEL_LEVEL_LOW, mCarZones)
-                        .put(RANGE_REMAINING, mCarZones)
-                        .put(DISTANCE_DISPLAY_UNITS, mCarZones)
-                        .put(FUEL_VOLUME_DISPLAY_UNITS, mCarZones)
-                        .buildKeepingLast();
-        ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
-                OnCarPropertyResponseListener.class);
-        verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
-                eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
-                eq(mExecutor));
-        captor.getValue().onCarPropertyResponses(mResponse);
-        mCountDownLatch.await();
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().build();
 
-        assertThat(loadedResult.get()).isEqualTo(new EnergyLevel.Builder().build());
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
     }
 
     @Test
     public void getEnergyLevel_withUnavailableCapacityValues() throws InterruptedException {
-        // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
-        mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
-        mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                CarPropertyResponse.builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(
+                        STATUS_UNAVAILABLE).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+                CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
+                        STATUS_UNAVAILABLE).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build());
 
-        ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
-                OnCarPropertyResponseListener.class);
-        int meterDistanceUnit = 0x21;
-        int meterVolumeUnit = 0x40;
-        float evBatteryLevelValue = 50f;
-        float fuelLevelValue = 50f;
-        List<CarPropertyResponse<?>> capacities = new ArrayList<>();
-        capacities.add(CarPropertyResponse.builder().setPropertyId(
-                INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_UNAVAILABLE).setTimestampMillis(
-                1L).build());
-        capacities.add(CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
-                STATUS_UNAVAILABLE).setTimestampMillis(1L).build());
-        ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(capacities);
-        when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
-                future);
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
 
-        AtomicReference<EnergyLevel> loadedResult = new AtomicReference<>();
-        OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
-            loadedResult.set(data);
-            mCountDownLatch.countDown();
-        };
+        // Battery percent and fuel percent should be null since we can not get the capacity of
+        // battery and fuel property. The other properties should still work without capacity
+        // values.
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNAVAILABLE)).setFuelPercent(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNAVAILABLE)).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
 
-        mAutomotiveCarInfo.addEnergyLevelListener(mExecutor, listener);
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+    }
 
-        // Create "EV_BATTERY_LEVEL", "FUEL_LEVEL", "FUEL_LEVEL_LOW", "RANGE_REMAINING",
-        // "DISTANCE_DISPLAY_UNITS" and "FUEL_VOLUME_DISPLAY_UNITS" property IDs list with car
-        // zones.
-        Map<Integer, List<CarZone>> propertyIdsWithCarZones =
-                ImmutableMap.<Integer, List<CarZone>>builder()
-                        .put(EV_BATTERY_LEVEL, mCarZones)
-                        .put(FUEL_LEVEL, mCarZones)
-                        .put(FUEL_LEVEL_LOW, mCarZones)
-                        .put(RANGE_REMAINING, mCarZones)
-                        .put(DISTANCE_DISPLAY_UNITS, mCarZones)
-                        .put(FUEL_VOLUME_DISPLAY_UNITS, mCarZones)
-                        .buildKeepingLast();
+    @Test
+    public void getEnergyLevel_withUnimplementedEvBatteryCapacity() throws InterruptedException {
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                CarPropertyResponse.builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(
+                        STATUS_UNIMPLEMENTED).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+                FUEL_CAPACITY_RESPONSE_SUCCESS);
 
-        verify(mPropertyManager, times(1)).submitGetPropertyRequest(
-                eq(mGetPropertyRequests),
-                eq(mExecutor));
-        verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
-                eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
-                eq(mExecutor));
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
 
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
-                STATUS_SUCCESS).setValue(evBatteryLevelValue).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
-                STATUS_SUCCESS).setValue(fuelLevelValue).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
-                STATUS_SUCCESS).setValue(true).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
-                STATUS_SUCCESS).setValue(5f).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
-                STATUS_SUCCESS).setValue(meterDistanceUnit).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(
-                FUEL_VOLUME_DISPLAY_UNITS).setStatus(STATUS_SUCCESS).setValue(
-                meterVolumeUnit).setTimestampMillis(1L).build());
-        captor.getValue().onCarPropertyResponses(mResponse);
-        mCountDownLatch.await();
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS,
+                        STATUS_UNIMPLEMENTED)).setFuelPercent(
+                EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
 
-        EnergyLevel energyLevel = loadedResult.get();
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+    }
 
-        // Battery percent and fuel percent should be UNKNOWN_FLOAT since we can not get
-        // the capacity of battery and fuel property.
-        assertThat(energyLevel.getBatteryPercent().getValue()).isEqualTo(
-                CarValue.UNKNOWN_FLOAT.getValue());
-        assertThat(energyLevel.getFuelPercent().getValue()).isEqualTo(
-                CarValue.UNKNOWN_FLOAT.getValue());
+    @Test
+    public void getEnergyLevel_withUnimplementedFuelCapacity() throws
+            InterruptedException {
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, CarPropertyResponse.builder().setPropertyId(
+                INFO_FUEL_CAPACITY).setStatus(STATUS_UNIMPLEMENTED).setTimestampMillis(
+                DEFAULT_TIMESTAMP_MILLIS).build());
 
-        // The other properties should still work without capacity values
-        assertThat(energyLevel.getEnergyIsLow().getValue()).isEqualTo(true);
-        assertThat(energyLevel.getRangeRemainingMeters().getValue()).isEqualTo(5f);
-        assertThat(energyLevel.getDistanceDisplayUnit().getValue()).isEqualTo(2);
-        assertThat(energyLevel.getFuelVolumeDisplayUnit().getValue()).isEqualTo(201);
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS,
+                        STATUS_UNIMPLEMENTED)).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+    }
+
+    @Test
+    public void getEnergyLevel_withUnknownEvBatteryCapacity() throws InterruptedException {
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                CarPropertyResponse.builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(
+                        STATUS_UNKNOWN).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+                FUEL_CAPACITY_RESPONSE_SUCCESS);
+
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNKNOWN)).setFuelPercent(
+                EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+    }
+
+    @Test
+    public void getEnergyLevel_withUnknownFuelCapacity() throws InterruptedException {
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, CarPropertyResponse.builder().setPropertyId(
+                        INFO_FUEL_CAPACITY).setStatus(STATUS_UNKNOWN).setTimestampMillis(
+                        DEFAULT_TIMESTAMP_MILLIS).build());
+
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+                new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNKNOWN)).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+    }
+
+    @Test
+    public void getEnergyLevel_withNoEvBatteryCapacityResponse() throws InterruptedException {
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                FUEL_CAPACITY_RESPONSE_SUCCESS);
+
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                new CarValue<>(null, 0, STATUS_UNKNOWN)).setFuelPercent(
+                EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+    }
+
+    @Test
+    public void getEnergyLevel_withNoFuelCapacityResponse() throws InterruptedException {
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                EV_BATTERY_CAPACITY_RESPONSE_SUCCESS);
+
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+                FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+                new CarValue<>(null, 0, STATUS_UNKNOWN)).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+                EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
     }
 
     @Test
     public void getEnergyLevel_SuccessfulPartialResponses() throws InterruptedException {
-        // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
-        mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
-        mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
+        List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+                EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, FUEL_CAPACITY_RESPONSE_SUCCESS);
 
-        ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
-                OnCarPropertyResponseListener.class);
-        int meterDistanceUnit = 0x21;
-        float evBatteryCapacity = 100f;
-        float evBatteryLevelValue = 50f;
-        float fuelCapacity = 120f;
-        float fuelLevelValue = 50f;
-        List<CarPropertyResponse<?>> capacities = new ArrayList<>();
-        capacities.add(CarPropertyResponse.builder().setPropertyId(
-                INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_SUCCESS).setValue(
-                evBatteryCapacity).setTimestampMillis(1L).build());
-        capacities.add(CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
-                STATUS_SUCCESS).setValue(fuelCapacity).setTimestampMillis(1L).build());
-        ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(capacities);
-        when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
-                future);
+        List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+                EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+                FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+                DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS);
 
-        AtomicReference<EnergyLevel> loadedResult = new AtomicReference<>();
-        OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
-            loadedResult.set(data);
-            mCountDownLatch.countDown();
-        };
+        EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+                EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+                EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+                EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+                EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+                EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).build();
 
-        mAutomotiveCarInfo.addEnergyLevelListener(mExecutor, listener);
-
-        // Create "EV_BATTERY_LEVEL", "FUEL_LEVEL", "FUEL_LEVEL_LOW", "RANGE_REMAINING",
-        // "DISTANCE_DISPLAY_UNITS" and "FUEL_VOLUME_DISPLAY_UNITS" property IDs list with car
-        // zones.
-        Map<Integer, List<CarZone>> propertyIdsWithCarZones =
-                ImmutableMap.<Integer, List<CarZone>>builder()
-                        .put(EV_BATTERY_LEVEL, mCarZones)
-                        .put(FUEL_LEVEL, mCarZones)
-                        .put(FUEL_LEVEL_LOW, mCarZones)
-                        .put(RANGE_REMAINING, mCarZones)
-                        .put(DISTANCE_DISPLAY_UNITS, mCarZones)
-                        .put(FUEL_VOLUME_DISPLAY_UNITS, mCarZones)
-                        .buildKeepingLast();
-
-        verify(mPropertyManager, times(1)).submitGetPropertyRequest(eq(mGetPropertyRequests),
-                eq(mExecutor));
-        verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
-                eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
-                eq(mExecutor));
-
-        // Missing response for FUEL_VOLUME_DISPLAY_UNITS.
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
-                STATUS_SUCCESS).setValue(evBatteryLevelValue).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
-                STATUS_SUCCESS).setValue(fuelLevelValue).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
-                STATUS_SUCCESS).setValue(true).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
-                STATUS_SUCCESS).setValue(5f).setTimestampMillis(1L).build());
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
-                STATUS_SUCCESS).setValue(meterDistanceUnit).setTimestampMillis(1L).build());
-
-        captor.getValue().onCarPropertyResponses(mResponse);
-        mCountDownLatch.await();
-
-        // The partial responses will be returned successfully.
-        EnergyLevel energyLevel = loadedResult.get();
-        assertThat(energyLevel.getBatteryPercent().getValue()).isEqualTo(
-                evBatteryLevelValue / evBatteryCapacity * 100);
-        assertThat(energyLevel.getFuelPercent().getValue()).isEqualTo(
-                fuelLevelValue / fuelCapacity * 100);
-        assertThat(energyLevel.getEnergyIsLow().getValue()).isEqualTo(true);
-        assertThat(energyLevel.getRangeRemainingMeters().getValue()).isEqualTo(5f);
-        assertThat(energyLevel.getDistanceDisplayUnit().getValue()).isEqualTo(2);
+        getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
     }
 }
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
index 8bd55d5..01cd107 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
@@ -34,6 +34,7 @@
 import androidx.car.app.sample.showcase.common.R;
 import androidx.car.app.sample.showcase.common.audio.VoiceInteraction;
 import androidx.car.app.sample.showcase.common.screens.userinteractions.RequestPermissionMenuDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.userinteractions.TaskOverflowDemoScreen;
 import androidx.core.graphics.drawable.IconCompat;
 
 /** A screen demonstrating User Interactions */
@@ -104,21 +105,40 @@
      * Returns the row for TaskRestriction Demo
      */
     private Item buildRowForTaskRestrictionDemo() {
+        boolean isInOverflow = mStep >= MAX_STEPS_ALLOWED;
+        String title = isInOverflow ? getCarContext().getString(
+                R.string.application_overflow_title) :
+                getCarContext().getString(R.string.task_step_of_title, mStep,
+                        MAX_STEPS_ALLOWED);
+        String subTitle = isInOverflow
+                ?
+                getCarContext().getString(R.string.task_step_of_title, mStep,
+                        MAX_STEPS_ALLOWED) :
+                getCarContext().getString(R.string.task_step_of_text);
         return new Row.Builder()
-                .setTitle(getCarContext().getString(R.string.task_step_of_title, mStep,
-                        MAX_STEPS_ALLOWED))
-                .addText(getCarContext().getString(R.string.task_step_of_text))
+                .setTitle(title)
+                .addText(subTitle)
                 .setImage(new CarIcon.Builder(
                         IconCompat.createWithResource(
                                 getCarContext(), R.drawable.baseline_task_24))
                         .build(), Row.IMAGE_TYPE_ICON)
                 .setOnClickListener(
-                        () ->
+                        () -> {
+                            if (mStep < MAX_STEPS_ALLOWED) {
                                 getScreenManager()
                                         .pushForResult(
                                                 new UserInteractionsDemoScreen(
                                                         mStep + 1, getCarContext()),
-                                                result -> mIsBackOperation = true))
+                                                result -> mIsBackOperation = true);
+                            } else {
+                                getScreenManager()
+                                        .pushForResult(
+                                                new TaskOverflowDemoScreen(
+                                                        getCarContext()),
+                                                result -> mIsBackOperation = true);
+                            }
+                        }
+                )
                 .build();
     }
 
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/TaskOverflowDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/TaskOverflowDemoScreen.java
new file mode 100644
index 0000000..7c44b5e
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/TaskOverflowDemoScreen.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.sample.showcase.common.screens.userinteractions;
+
+import static androidx.car.app.model.Action.BACK;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.constraints.ConstraintManager;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.GridTemplateMenuDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.ListTemplateDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.MessageTemplateDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.PaneTemplateDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.SearchTemplateDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.SignInTemplateDemoScreen;
+
+/** A screen demonstrating Task Overflow for the different templates */
+public class TaskOverflowDemoScreen extends Screen {
+
+    public TaskOverflowDemoScreen(@NonNull CarContext carContext) {
+        super(carContext);
+    }
+
+    private Row createRow(int index) {
+        switch (index) {
+            case 0:
+                return new Row.Builder()
+                        .setTitle("    ")
+                        .addText(getCarContext().getString(R.string.application_overflow_subtitle1))
+                        .addText(getCarContext().getString(R.string.application_overflow_subtitle2))
+
+                        .build();
+            case 1:
+                return buildRowForTemplate(new ListTemplateDemoScreen(getCarContext()),
+                        R.string.list_template_demo_title);
+            case 2:
+                return buildRowForTemplate(new GridTemplateMenuDemoScreen(getCarContext()),
+                        R.string.grid_template_menu_demo_title);
+            case 3:
+                return buildRowForTemplate(new MessageTemplateDemoScreen(getCarContext()),
+                        R.string.msg_template_demo_title);
+            case 4:
+                return buildRowForTemplate(new PaneTemplateDemoScreen(getCarContext()),
+                        R.string.pane_template_demo_title);
+            case 5:
+                return buildRowForTemplate(new SearchTemplateDemoScreen(getCarContext()),
+                        R.string.search_template_demo_title);
+            case 6:
+                return buildRowForTemplate(new SignInTemplateDemoScreen(getCarContext()),
+                        R.string.sign_in_template_demo_title);
+            default:
+                return new Row.Builder()
+                        .setTitle(
+                                getCarContext().getString(R.string.other_row_title_prefix) + (index
+                                        + 1))
+                        .addText(getCarContext().getString(R.string.other_row_text))
+                        .build();
+        }
+    }
+
+    private Row buildRowForTemplate(Screen screen, int title) {
+        return new Row.Builder()
+                .setTitle(getCarContext().getString(title))
+                .setOnClickListener(() -> getScreenManager().push(screen))
+                .setBrowsable(true)
+                .build();
+    }
+
+    @NonNull
+    @Override
+    public Template onGetTemplate() {
+        int listLimit = getCarContext().getCarService(ConstraintManager.class).getContentLimit(
+                ConstraintManager.CONTENT_LIMIT_TYPE_LIST);
+        if (listLimit >= 6) {
+            listLimit = 6;
+        }
+
+        ItemList.Builder listBuilder = new ItemList.Builder();
+        for (int i = 0; i < listLimit; i++) {
+            listBuilder.addItem(createRow(i));
+        }
+        return new ListTemplate.Builder()
+                .setSingleList(listBuilder.build())
+                .setTitle(getCarContext().getString(R.string.application_overflow_title))
+                .setHeaderAction(BACK)
+                .build();
+    }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
index 197eb29..9bbe73a 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
@@ -373,6 +373,7 @@
   <string name="task_limit_reached_msg">Task limit reached\nGoing forward will force stop the app</string>
   <string name="task_step_of_title">Task step %1$d of %2$d</string>
   <string name="task_step_of_text">Click to go forward</string>
+  <string name="task_content_allowed">Please visit the different templates and ensure the car is in driving mode</string>
 
   <!-- ToggleButtonDemoScreen -->
   <string name="toggle_button_demo_title">Toggle Button Demo</string>
@@ -411,6 +412,11 @@
   <string name="user_interactions_demo_title">User Interactions</string>
   <string name="request_permission_menu_demo_title">Request Permissions Demos</string>
 
+  <!-- Task Overflow Demo Screen -->
+  <string name="application_overflow_title">Application Overflow Validator</string>
+  <string name="application_overflow_subtitle1">Please test the following templates while changing</string>
+  <string name="application_overflow_subtitle2">the vehicle from parked to driving state</string>
+
   <!-- Manifest file permissions -->
   <string name="perm_group">Permission Group</string>
   <string name="perm_group_description">Permission Group for Showcase App</string>
diff --git a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
index 9908df5..8157779 100644
--- a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
+++ b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
@@ -39,6 +39,7 @@
     field @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
     field @androidx.car.app.annotations.RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER";
     field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
+    field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_MESSAGING_APP = "androidx.car.app.category.MESSAGING";
     field public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
     field @Deprecated public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
     field public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
@@ -843,6 +844,14 @@
 
 }
 
+package androidx.car.app.messaging {
+
+  @androidx.car.app.annotations.ExperimentalCarApi public class MessagingServiceConstants {
+    field public static final String ACTION_HANDLE_CAR_MESSAGING = "androidx.car.app.messaging.action.HANDLE_CAR_MESSAGING";
+  }
+
+}
+
 package androidx.car.app.messaging.model {
 
   @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public class CarMessage {
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 9908df5..8157779 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -39,6 +39,7 @@
     field @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
     field @androidx.car.app.annotations.RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER";
     field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
+    field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_MESSAGING_APP = "androidx.car.app.category.MESSAGING";
     field public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
     field @Deprecated public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
     field public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
@@ -843,6 +844,14 @@
 
 }
 
+package androidx.car.app.messaging {
+
+  @androidx.car.app.annotations.ExperimentalCarApi public class MessagingServiceConstants {
+    field public static final String ACTION_HANDLE_CAR_MESSAGING = "androidx.car.app.messaging.action.HANDLE_CAR_MESSAGING";
+  }
+
+}
+
 package androidx.car.app.messaging.model {
 
   @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public class CarMessage {
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index 4a8c423..3f4b442 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -136,6 +136,14 @@
     @ExperimentalCarApi
     public static final String CATEGORY_SETTINGS_APP = "androidx.car.app.category.SETTINGS";
 
+    /**
+     * Used to declare that this app is a messaging app in the manifest.
+     *
+     * <p> This app can be used to send and receive short-form chat messages (IM/SMS).
+     */
+    @ExperimentalCarApi
+    public static final String CATEGORY_MESSAGING_APP = "androidx.car.app.category.MESSAGING";
+
     private static final String AUTO_DRIVE = "AUTO_DRIVE";
 
     @NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/MessagingServiceConstants.java b/car/app/app/src/main/java/androidx/car/app/messaging/MessagingServiceConstants.java
new file mode 100644
index 0000000..ef46c87
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/MessagingServiceConstants.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.messaging;
+
+import androidx.car.app.annotations.ExperimentalCarApi;
+
+/** Constants related to messaging in Android Auto */
+@ExperimentalCarApi
+public class MessagingServiceConstants {
+    /**
+     * Used to declare Android Auto messaging support within an app's manifest
+     *
+     * <p> Specifically, this bit should be added to an {@link android.app.IntentService} as the
+     * {@code IntentFilter}'s action. When declared, apps will show up in Android Auto's app
+     * launcher with the "default" / "built-in" in-car messaging experience.
+     */
+    public static final String ACTION_HANDLE_CAR_MESSAGING =
+            "androidx.car.app.messaging.action.HANDLE_CAR_MESSAGING";
+
+    // Do not instantiate
+    private MessagingServiceConstants() {}
+}
diff --git a/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index 7832f49..1cec99e 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -16,7 +16,6 @@
 
 
 import androidx.build.KmpPlatformsKt
-import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
 import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig
@@ -27,6 +26,7 @@
     id("AndroidXPlugin")
     id("com.android.library")
     id("androidx.benchmark")
+    id("androidx.benchmark.darwin")
 }
 
 def macEnabled = KmpPlatformsKt.enableMac(project)
@@ -34,7 +34,7 @@
 androidXMultiplatform {
     android()
 
-    // XCFrameworkConfig must match module name
+    // XCFrameworkConfig must always be called AndroidXDarwinBenchmarks
     def xcf = new XCFrameworkConfig(project, "AndroidXDarwinBenchmarks")
     ios {
         binaries.framework {
@@ -111,18 +111,14 @@
     description = "AndroidX Collections Benchmarks (Android / iOS)"
 }
 
-if (macEnabled) {
-    apply plugin: "androidx.benchmark.darwin"
-    darwinBenchmark {
-        xcodeGenConfigFile = project.rootProject.file(
-                "benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml"
-        )
-        xcodeProjectName = "collection-benchmark-ios"
-        scheme = "testapp-ios"
-        // ios 13, 15.2
-        destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
-        xcFrameworkConfig = "AndroidXDarwinBenchmarks"
-    }
+darwinBenchmark {
+    xcodeGenConfigFile = project.rootProject.file(
+            "benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml"
+    )
+    xcodeProjectName = "collection-benchmark-ios"
+    scheme = "testapp-ios"
+    // ios 13, 15.2
+    destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
 }
 
 android {
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt
index c7872c7..debf3c6 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt
@@ -21,6 +21,7 @@
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.geometry.Offset
@@ -396,6 +397,7 @@
     finishedListener: ((T) -> Unit)? = null
 ): State<T> {
 
+    val toolingOverride = remember { mutableStateOf<State<T>?>(null) }
     val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }
     val listener by rememberUpdatedState(finishedListener)
     val animSpec: AnimationSpec<T> by rememberUpdatedState(
@@ -429,7 +431,7 @@
             }
         }
     }
-    return animatable.asState()
+    return toolingOverride.value ?: animatable.asState()
 }
 
 @Deprecated(
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 c9657c0..c1d0123 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
@@ -28,6 +28,7 @@
     ) = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
+            import androidx.compose.runtime.ReadOnlyComposable
             import androidx.compose.runtime.key
             import androidx.compose.runtime.NonRestartableComposable
 
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 fc62647..0f96e97 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
@@ -287,7 +287,7 @@
             fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
               sourceInformation(%composer, "C(Test)<A()>,<M3>,<A()>:Test.kt")
-              val tmp1_marker = %composer.currentMarker
+              val tmp0_marker = %composer.currentMarker
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -303,7 +303,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (condition) {
-                      %composer.endToMarker(tmp1_marker)
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
@@ -361,7 +361,7 @@
             fun Test(a: Boolean, b: Boolean, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
               sourceInformation(%composer, "C(Test)<A()>,<M3>,<M3>,<A()>:Test.kt")
-              val tmp1_marker = %composer.currentMarker
+              val tmp0_marker = %composer.currentMarker
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
@@ -380,7 +380,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (a) {
-                      %composer.endToMarker(tmp1_marker)
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
@@ -401,7 +401,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (b) {
-                      %composer.endToMarker(tmp1_marker)
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
@@ -722,7 +722,7 @@
             fun testInline_M1_W_Return_Func(condition: Boolean, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
               sourceInformation(%composer, "C(testInline_M1_W_Return_Func)<A()>,<M1>,<A()>:Test.kt")
-              val tmp1_marker = %composer.currentMarker
+              val tmp0_marker = %composer.currentMarker
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -742,7 +742,7 @@
                     while (true) {
                       A(%composer, 0)
                       if (condition) {
-                        %composer.endToMarker(tmp1_marker)
+                        %composer.endToMarker(tmp0_marker)
                         if (isTraceInProgress()) {
                           traceEventEnd()
                         }
@@ -882,7 +882,7 @@
             fun test_CM1_CCM1_RetFun(condition: Boolean, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
               sourceInformation(%composer, "C(test_CM1_CCM1_RetFun)<Text("...>,<M1>,<Text("...>:Test.kt")
-              val tmp1_marker = %composer.currentMarker
+              val tmp0_marker = %composer.currentMarker
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -906,7 +906,7 @@
                         sourceInformation(%composer, "C<Text("...>:Test.kt")
                         if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                           Text("In CCM1", %composer, 0b0110)
-                          %composer.endToMarker(tmp1_marker)
+                          %composer.endToMarker(tmp0_marker)
                           if (isTraceInProgress()) {
                             traceEventEnd()
                           }
@@ -998,6 +998,295 @@
         """
     )
 
+    @Test // regression 255350755
+    fun testEnsureEarlyExitInNonInline_NormalComposable() = controlFlow(
+        """
+            object obj {
+                val condition = true
+            }
+
+            @Composable
+            fun Test(condition: Boolean) {
+                if (condition) return
+                with (obj) {
+                    if (condition) return
+                }
+                A()
+            }
+        """,
+        """
+            @StabilityInferred(parameters = 0)
+            object obj {
+              val condition: Boolean = true
+              static val %stable: Int = 0
+            }
+            @Composable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)<A()>:Test.kt")
+              val tmp0_marker = %composer.currentMarker
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+              }
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                if (condition) {
+                  if (isTraceInProgress()) {
+                    traceEventEnd()
+                  }
+                  %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                    Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                  }
+                  return
+                }
+                with(obj) {
+                  if (condition) {
+                    %composer.endToMarker(tmp0_marker)
+                    if (isTraceInProgress()) {
+                      traceEventEnd()
+                    }
+                    %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                      Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                    }
+                    return
+                  }
+                }
+                A(%composer, 0)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+        """
+    )
+
+    @Test // regression 255350755
+    fun testEnsureEarlyExitInNonInline_ReadOnlyComposable() = controlFlow(
+        """
+            import androidx.compose.runtime.currentComposer
+
+            object obj {
+                val condition = false
+            }
+
+            @Composable
+            @ReadOnlyComposable
+            fun Calculate(condition: Boolean): Boolean {
+                if (condition) return false
+
+                with (obj) {
+                    if (condition) return false
+                    return currentComposer.inserting
+                }
+            }
+        """,
+        """
+            @StabilityInferred(parameters = 0)
+            object obj {
+              val condition: Boolean = false
+              static val %stable: Int = 0
+            }
+            @Composable
+            @ReadOnlyComposable
+            fun Calculate(condition: Boolean, %composer: Composer?, %changed: Int): Boolean {
+              sourceInformationMarkerStart(%composer, <>, "C(Calculate):Test.kt")
+              val tmp1_marker = %composer.currentMarker
+              if (isTraceInProgress()) {
+                traceEventStart(<>, %changed, -1, <>)
+              }
+              if (condition) {
+                val tmp0_return = false
+                sourceInformationMarkerEnd(%composer)
+                return tmp0_return
+              }
+              with(obj) {
+                if (condition) {
+                  val tmp0_return = false
+                  %composer.endToMarker(tmp1_marker)
+                  sourceInformationMarkerEnd(%composer)
+                  return tmp0_return
+                }
+                val tmp1_return = %composer.inserting
+                %composer.endToMarker(tmp1_marker)
+                sourceInformationMarkerEnd(%composer)
+                return tmp1_return
+              }
+              if (isTraceInProgress()) {
+                traceEventEnd()
+              }
+              sourceInformationMarkerEnd(%composer)
+            }
+        """
+    )
+
+    @Test // regression 255350755
+    fun testEnsureEarlyExitInInline_Labeled() = controlFlow(
+        """
+            @Composable
+            fun Test(condition: Boolean) {
+                IW iw@ {
+                    if (condition) return@iw
+                    A()
+                }
+            }
+        """,
+        """
+            @Composable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)<IW>:Test.kt")
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+              }
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                val tmp0_marker = %composer.currentMarker
+                IW({ %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<A()>:Test.kt")
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+                    if (condition) {
+                      %composer.endToMarker(tmp0_marker)
+                    }
+                    A(%composer, 0)
+                  } else {
+                    %composer.skipToGroupEnd()
+                  }
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+        """
+    )
+
+    @Test
+    fun testEnsureRuntimeTestWillCompile_CL() = ensureSetup {
+        classLoader(
+            """
+            import androidx.compose.runtime.Composable
+
+            @Composable
+            fun test_CM1_RetFun(condition: Boolean) {
+                Text("Root - before")
+                M1 {
+                    Text("M1 - before")
+                    if (condition) return
+                    Text("M1 - after")
+                }
+                Text("Root - after")
+            }
+            @Composable
+            inline fun InlineWrapper(content: @Composable () -> Unit) = content()
+
+            @Composable
+            inline fun M1(content: @Composable () -> Unit) = InlineWrapper { content() }
+
+            @Composable
+            fun Text(value: String) { }
+            """,
+            "Test.kt"
+        )
+    }
+
+    @Test // regression 255350755
+    fun testEnsureRuntimeTestWillCompile_CG() = verifyComposeIrTransform(
+        """
+            import androidx.compose.runtime.Composable
+
+            @Composable
+            fun test_CM1_RetFun(condition: Boolean) {
+                Text("Root - before")
+                M1 {
+                    Text("M1 - before")
+                    if (condition) return
+                    Text("M1 - after")
+                }
+                Text("Root - after")
+            }
+
+        """,
+        """
+            @Composable
+            fun test_CM1_RetFun(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(test_CM1_RetFun)<Text("...>,<M1>,<Text("...>:Test.kt")
+              val tmp0_marker = %composer.currentMarker
+              val %dirty = %changed
+              if (%changed and 0b1110 === 0) {
+                %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+              }
+              if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                Text("Root - before", %composer, 0b0110)
+                M1({ %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+                    Text("M1 - before", %composer, 0b0110)
+                    if (condition) {
+                      %composer.endToMarker(tmp0_marker)
+                      if (isTraceInProgress()) {
+                        traceEventEnd()
+                      }
+                      %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                        test_CM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                      }
+                      return
+                    }
+                    Text("M1 - after", %composer, 0b0110)
+                  } else {
+                    %composer.skipToGroupEnd()
+                  }
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                Text("Root - after", %composer, 0b0110)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                test_CM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.Composable
+            import androidx.compose.runtime.NonRestartableComposable
+
+            @Composable
+            inline fun InlineWrapper(content: @Composable () -> Unit) = content()
+
+            @Composable
+            inline fun M1(content: @Composable () -> Unit) = InlineWrapper { content() }
+
+            @Composable @NonRestartableComposable
+            fun Text(value: String) { }
+        """
+    )
+
     @Test
     fun testIfElseWithCallsInConditions(): Unit = controlFlow(
         """
@@ -5299,8 +5588,6 @@
     @Test
     fun testReadOnlyComposableWithEarlyReturn() = controlFlow(
         source = """
-            import androidx.compose.runtime.ReadOnlyComposable
-
             @ReadOnlyComposable
             @Composable
             fun getSomeValue(a: Int): Int {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
index 4159214..136d1d6 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
@@ -118,7 +118,7 @@
             fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
               sourceInformation(%composer, "C(Test)<A()>,<Wrappe...>,<A()>:Test.kt")
-              val tmp1_marker = %composer.currentMarker
+              val tmp0_marker = %composer.currentMarker
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -134,7 +134,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (!condition) {
-                      %composer.endToMarker(tmp1_marker)
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 980e7f7..b29e3df 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -110,7 +110,7 @@
          * The maven version string of this compiler. This string should be updated before/after every
          * release.
          */
-        const val compilerVersion: String = "1.3.3"
+        const val compilerVersion: String = "1.4.0-alpha01"
         private val minimumRuntimeVersion: String
             get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
     }
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 96d4611..9f41e35 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
@@ -1849,8 +1849,11 @@
         )
     }
 
-    fun irCurrentMarker() =
-        irMethodCall(irCurrentComposer(), currentMarkerProperty!!.getter!!)
+    fun irCurrentMarker(composerParameter: IrValueParameter) =
+        irMethodCall(
+            irCurrentComposer(composerParameter = composerParameter),
+            currentMarkerProperty!!.getter!!
+        )
 
     private fun irIsSkipping() =
         irMethodCall(irCurrentComposer(), isSkippingFunction.getter!!)
@@ -1971,18 +1974,17 @@
         }
     }
 
-    private fun nearestComposer(): IrValueParameter =
-        currentScope.nearestComposer
-            ?: error("Not in a composable function \n${printScopeStack()}")
+    private fun nearestComposer(): IrValueParameter = currentScope.myComposer
 
     private fun irCurrentComposer(
         startOffset: Int = UNDEFINED_OFFSET,
-        endOffset: Int = UNDEFINED_OFFSET
+        endOffset: Int = UNDEFINED_OFFSET,
+        composerParameter: IrValueParameter = nearestComposer()
     ): IrExpression {
         return IrGetValueImpl(
             startOffset,
             endOffset,
-            nearestComposer().symbol
+            composerParameter.symbol
         )
     }
 
@@ -2620,9 +2622,9 @@
                                 val marker = irGet(functionScope.allocateMarker())
                                 extraEndLocation(irEndToMarker(marker))
                             } else {
+                                val marker = functionScope.allocateMarker()
                                 functionScope.markReturn {
-                                    val marker = irGet(functionScope.allocateMarker())
-                                    extraEndLocation(irEndToMarker(marker))
+                                    extraEndLocation(irEndToMarker(irGet(marker)))
                                     extraEndLocation(it)
                                 }
                             }
@@ -3713,6 +3715,9 @@
         open val fileScope: FileScope? get() = parent?.fileScope
         open val nearestComposer: IrValueParameter? get() = parent?.nearestComposer
 
+        val myComposer: IrValueParameter get() = nearestComposer
+            ?: error("Not in a composable function")
+
         open class SourceLocation(val element: IrElement) {
             open val repeatable: Boolean
                 get() = false
@@ -3777,7 +3782,7 @@
                 return (if (isInlinedLambda && parent is Scope.CallScope) {
                     parent.allocateMarker()
                 } else transformer.irTemporary(
-                    transformer.irCurrentMarker(),
+                    transformer.irCurrentMarker(myComposer),
                     getNameForTemporary("marker")
                 ).also { markerPreamble.statements.add(it) }).also {
                     marker = it
@@ -4230,7 +4235,7 @@
 
             fun allocateMarker(): IrVariable = marker
                 ?: transformer.irTemporary(
-                    transformer.irCurrentMarker(),
+                    transformer.irCurrentMarker(myComposer),
                     getNameForTemporary("marker")
                 ).also { marker = it }
 
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
index de3293f..62ab2de 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
@@ -138,13 +138,18 @@
         // do it ourself here.
         if (
             ownerFn != null &&
-            ownerFn.origin == IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB
+            ownerFn.origin == IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB &&
+            ownerFn.hasComposableArguments()
         ) {
-            symbolRemapper.visitConstructor(ownerFn)
-            val newFn = super.visitConstructor(ownerFn).also {
-                it.patchDeclarationParents(ownerFn.parent)
+            if (symbolRemapper.getReferencedConstructor(ownerFn.symbol) == ownerFn.symbol) {
+                // Not remapped yet, so remap now.
+                // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
+                symbolRemapper.visitConstructor(ownerFn)
+                super.visitConstructor(ownerFn).also {
+                    it.patchDeclarationParents(ownerFn.parent)
+                }
             }
-            val newCallee = symbolRemapper.getReferencedConstructor(newFn.symbol)
+            val newCallee = symbolRemapper.getReferencedConstructor(ownerFn.symbol)
 
             return IrConstructorCallImpl(
                 expression.startOffset, expression.endOffset,
@@ -192,22 +197,29 @@
             expression.dispatchReceiver?.type?.isComposable() == true
         ) {
             val typeArguments = containingClass.defaultType.arguments
-            val newFnClass = context.function(typeArguments.size).owner
+            val realParams = typeArguments.size - 1
+            // with composer and changed
+            val newArgsSize = realParams + 1 + changedParamCount(realParams, 0)
+            val newFnClass = context.function(newArgsSize).owner
 
             var newFn = newFnClass
                 .functions
                 .first { it.name == ownerFn.name }
 
-            symbolRemapper.visitSimpleFunction(newFn)
-            newFn = super.visitSimpleFunction(newFn).also { fn ->
-                fn.overriddenSymbols = ownerFn.overriddenSymbols.map { it }
-                fn.dispatchReceiverParameter = ownerFn.dispatchReceiverParameter
-                fn.extensionReceiverParameter = ownerFn.extensionReceiverParameter
-                newFn.valueParameters.forEach { p ->
-                    fn.addValueParameter(p.name.identifier, p.type)
+            if (symbolRemapper.getReferencedSimpleFunction(newFn.symbol) == newFn.symbol) {
+                // Not remapped yet, so remap now.
+                // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
+                symbolRemapper.visitSimpleFunction(newFn)
+                newFn = super.visitSimpleFunction(newFn).also { fn ->
+                    fn.overriddenSymbols = ownerFn.overriddenSymbols.map { it }
+                    fn.dispatchReceiverParameter = ownerFn.dispatchReceiverParameter
+                    fn.extensionReceiverParameter = ownerFn.extensionReceiverParameter
+                    newFn.valueParameters.forEach { p ->
+                        fn.addValueParameter(p.name.identifier, p.type)
+                    }
+                    fn.patchDeclarationParents(newFnClass)
+                    assert(fn.body == null) { "expected body to be null" }
                 }
-                fn.patchDeclarationParents(newFnClass)
-                assert(fn.body == null) { "expected body to be null" }
             }
 
             val newCallee = symbolRemapper.getReferencedSimpleFunction(newFn.symbol)
@@ -234,20 +246,30 @@
                 val property = ownerFn.correspondingPropertySymbol!!.owner
                 // avoid java properties since they go through a different lowering and it is
                 // also impossible for them to have composable types
-                if (property.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB) {
-                    symbolRemapper.visitProperty(property)
-                    visitProperty(property).also {
-                        it.getter?.correspondingPropertySymbol = it.symbol
-                        it.setter?.correspondingPropertySymbol = it.symbol
-                        it.patchDeclarationParents(ownerFn.parent)
-                        it.copyAttributes(property)
+                if (property.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB &&
+                    property.getter?.returnType?.isComposable() == true
+                ) {
+                    if (symbolRemapper.getReferencedProperty(property.symbol) == property.symbol) {
+                        // Not remapped yet, so remap now.
+                        // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
+                        symbolRemapper.visitProperty(property)
+                        visitProperty(property).also {
+                            it.getter?.correspondingPropertySymbol = it.symbol
+                            it.setter?.correspondingPropertySymbol = it.symbol
+                            it.patchDeclarationParents(ownerFn.parent)
+                            it.copyAttributes(property)
+                        }
                     }
                 }
-            } else {
-                symbolRemapper.visitSimpleFunction(ownerFn)
-                visitSimpleFunction(ownerFn).also {
-                    it.correspondingPropertySymbol = null
-                    it.patchDeclarationParents(ownerFn.parent)
+            } else if (ownerFn.hasComposableArguments()) {
+                if (symbolRemapper.getReferencedSimpleFunction(ownerFn.symbol) == ownerFn.symbol) {
+                    // Not remapped yet, so remap now.
+                    // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
+                    symbolRemapper.visitSimpleFunction(ownerFn)
+                    visitSimpleFunction(ownerFn).also {
+                        it.correspondingPropertySymbol = null
+                        it.patchDeclarationParents(ownerFn.parent)
+                    }
                 }
             }
             val newCallee = symbolRemapper.getReferencedSimpleFunction(ownerFn.symbol)
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
index 523769d..c152873 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
@@ -74,7 +74,6 @@
 import org.jetbrains.kotlin.ir.util.copyTypeParametersFrom
 import org.jetbrains.kotlin.ir.util.defaultType
 import org.jetbrains.kotlin.ir.util.explicitParameters
-import org.jetbrains.kotlin.ir.util.fileOrNull
 import org.jetbrains.kotlin.ir.util.findAnnotation
 import org.jetbrains.kotlin.ir.util.functions
 import org.jetbrains.kotlin.ir.util.getInlineClassUnderlyingType
@@ -167,15 +166,6 @@
             else -> (symbol.owner).withComposerParamIfNeeded()
         }
 
-        // externally transformed functions are already remapped from decoys, so we only need to
-        // add the parameters to the call
-        if (!ownerFn.externallyTransformed()) {
-            if (!isComposableLambda && !transformedFunctionSet.contains(ownerFn))
-                return this
-            if (symbol.owner == ownerFn)
-                return this
-        }
-
         return IrCallImpl(
             startOffset,
             endOffset,
@@ -673,5 +663,7 @@
      * different module fragment with the same [ModuleDescriptor]
      */
     private fun IrFunction.externallyTransformed(): Boolean =
-        decoysEnabled && currentModule?.files?.contains(fileOrNull) != true
+        decoysEnabled && valueParameters.firstOrNull {
+            it.name == KtxNameConventions.COMPOSER_PARAMETER
+        } != null
 }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/WrapJsComposableLambdaLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/WrapJsComposableLambdaLowering.kt
index 1f74d01..2b3ec86 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/WrapJsComposableLambdaLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/WrapJsComposableLambdaLowering.kt
@@ -20,6 +20,8 @@
 import androidx.compose.compiler.plugins.kotlin.ComposeClassIds
 import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
 import androidx.compose.compiler.plugins.kotlin.lower.decoys.AbstractDecoysLowering
+import androidx.compose.compiler.plugins.kotlin.lower.decoys.CreateDecoysTransformer
+import androidx.compose.compiler.plugins.kotlin.lower.decoys.isDecoy
 import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
 import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
 import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
@@ -28,6 +30,7 @@
 import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
 import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
 import org.jetbrains.kotlin.ir.declarations.impl.IrFunctionImpl
 import org.jetbrains.kotlin.ir.expressions.IrCall
 import org.jetbrains.kotlin.ir.expressions.IrExpression
@@ -43,6 +46,7 @@
 import org.jetbrains.kotlin.ir.types.typeWith
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
 import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET
+import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
 import org.jetbrains.kotlin.ir.util.functions
 import org.jetbrains.kotlin.ir.util.isVararg
 import org.jetbrains.kotlin.ir.util.patchDeclarationParents
@@ -83,12 +87,31 @@
     signatureBuilder: IdSignatureSerializer
 ) : AbstractDecoysLowering(context, symbolRemapper, metrics, signatureBuilder) {
 
-    private val composableLambdaSymbol = symbolRemapper.getReferencedSimpleFunction(
-        getTopLevelFunction(ComposeCallableIds.composableLambda)
-    )
-    private val composableLambdaInstanceSymbol = symbolRemapper.getReferencedSimpleFunction(
-        getTopLevelFunction(ComposeCallableIds.composableLambdaInstance)
-    )
+    private val rememberFunSymbol by lazy {
+        symbolRemapper.getReferencedSimpleFunction(
+            getTopLevelFunctions(ComposeCallableIds.remember).map { it.owner }.first {
+                it.valueParameters.size == 2 && !it.valueParameters.first().isVararg
+            }.symbol
+        ).owner.let {
+            if (!it.isDecoy()) {
+                // If a module didn't have any explicit remember calls,
+                // so `fun remember` wasn't transformed yet, then we have to transform it now.
+                val createDecoysTransformer = CreateDecoysTransformer(
+                    context, symbolRemapper, signatureBuilder, metrics
+                )
+                createDecoysTransformer.visitSimpleFunction(it) as IrSimpleFunction
+                createDecoysTransformer.updateParents()
+                val composerParamTransformer = ComposerParamTransformer(
+                    context, symbolRemapper, true, metrics
+                )
+                composerParamTransformer.visitSimpleFunction(
+                    it.getComposableForDecoy().owner as IrSimpleFunction
+                ) as IrSimpleFunction
+            } else {
+                it.getComposableForDecoy().owner as IrSimpleFunction
+            }
+        }.symbol
+    }
 
     override fun lower(module: IrModuleFragment) {
         module.transformChildrenVoid(this)
@@ -97,11 +120,11 @@
 
     override fun visitCall(expression: IrCall): IrExpression {
         val original = super.visitCall(expression) as IrCall
-        return when (expression.symbol) {
-            composableLambdaSymbol -> {
+        return when (expression.symbol.owner.fqNameForIrSerialization) {
+            ComposeCallableIds.composableLambda.asSingleFqName() -> {
                 transformComposableLambdaCall(original)
             }
-            composableLambdaInstanceSymbol -> {
+            ComposeCallableIds.composableLambdaInstance.asSingleFqName() -> {
                 transformComposableLambdaInstanceCall(original)
             }
             else -> original
@@ -145,12 +168,6 @@
             lambda, irGet(composableLambdaVar)
         )
 
-        val rememberFunSymbol = symbolRemapper.getReferencedSimpleFunction(
-            getTopLevelFunctions(ComposeCallableIds.remember).first {
-                it.owner.valueParameters.size == 2 && !it.owner.valueParameters.first().isVararg
-            }
-        ).owner.getComposableForDecoy() as IrSimpleFunctionSymbol
-
         val calculationFunSymbol = IrSimpleFunctionSymbolImpl()
         val rememberBlock = createLambda0(
             returnType = lambda.type,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
index 97fca97..bab96bb 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
@@ -16,13 +16,24 @@
 
 package androidx.compose.compiler.plugins.kotlin.lower.decoys
 
+import androidx.compose.compiler.plugins.kotlin.ComposeFqNames
 import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
 import androidx.compose.compiler.plugins.kotlin.lower.AbstractComposeLowering
 import androidx.compose.compiler.plugins.kotlin.lower.includeFileNameInExceptionTrace
 import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
 import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
+import org.jetbrains.kotlin.ir.declarations.IrConstructor
+import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
 import org.jetbrains.kotlin.ir.declarations.IrFile
+import org.jetbrains.kotlin.ir.declarations.IrFunction
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
+import org.jetbrains.kotlin.ir.types.IrSimpleType
+import org.jetbrains.kotlin.ir.types.IrType
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.hasAnnotation
+import org.jetbrains.kotlin.ir.util.isEnumClass
+import org.jetbrains.kotlin.ir.util.isLocal
+import org.jetbrains.kotlin.ir.util.parentAsClass
 
 abstract class AbstractDecoysLowering(
     pluginContext: IrPluginContext,
@@ -47,4 +58,36 @@
             return file
         }
     }
+
+    protected fun IrFunction.shouldBeRemapped(): Boolean =
+        !isLocalFunction() &&
+            !isEnumConstructor() &&
+            (hasComposableAnnotation() || hasComposableParameter())
+
+    private fun IrFunction.isLocalFunction(): Boolean =
+        origin == IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA ||
+            (isLocal && (this is IrSimpleFunction && !overridesComposable()))
+
+    private fun IrSimpleFunction.overridesComposable() =
+        overriddenSymbols.any {
+            it.owner.isDecoy() || it.owner.shouldBeRemapped()
+        }
+
+    private fun IrFunction.hasComposableParameter() =
+        valueParameters.any { it.type.hasComposable() } ||
+            extensionReceiverParameter?.type?.hasComposable() == true
+
+    private fun IrFunction.isEnumConstructor() =
+        this is IrConstructor && parentAsClass.isEnumClass
+
+    private fun IrType.hasComposable(): Boolean {
+        if (hasAnnotation(ComposeFqNames.Composable)) {
+            return true
+        }
+
+        return when (this) {
+            is IrSimpleType -> arguments.any { (it as? IrType)?.hasComposable() == true }
+            else -> false
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
index e9df6af..ad16985 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.compiler.plugins.kotlin.lower.decoys
 
-import androidx.compose.compiler.plugins.kotlin.ComposeFqNames
 import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
 import androidx.compose.compiler.plugins.kotlin.lower.ModuleLoweringPass
 import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
@@ -32,7 +31,6 @@
 import org.jetbrains.kotlin.ir.builders.irReturn
 import org.jetbrains.kotlin.ir.declarations.IrConstructor
 import org.jetbrains.kotlin.ir.declarations.IrDeclarationContainer
-import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
 import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent
 import org.jetbrains.kotlin.ir.declarations.IrFunction
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
@@ -42,19 +40,13 @@
 import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
 import org.jetbrains.kotlin.ir.expressions.IrGetValue
 import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
-import org.jetbrains.kotlin.ir.types.IrSimpleType
-import org.jetbrains.kotlin.ir.types.IrType
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
 import org.jetbrains.kotlin.ir.util.addChild
 import org.jetbrains.kotlin.ir.util.constructors
 import org.jetbrains.kotlin.ir.util.copyTo
 import org.jetbrains.kotlin.ir.util.copyTypeParametersFrom
 import org.jetbrains.kotlin.ir.util.defaultType
-import org.jetbrains.kotlin.ir.util.hasAnnotation
 import org.jetbrains.kotlin.ir.util.hasDefaultValue
-import org.jetbrains.kotlin.ir.util.isEnumClass
-import org.jetbrains.kotlin.ir.util.isLocal
-import org.jetbrains.kotlin.ir.util.parentAsClass
 import org.jetbrains.kotlin.ir.util.patchDeclarationParents
 import org.jetbrains.kotlin.ir.util.remapTypeParameters
 import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
@@ -116,12 +108,15 @@
 
     override fun lower(module: IrModuleFragment) {
         module.transformChildrenVoid()
+        updateParents()
+        module.patchDeclarationParents()
+    }
 
+    fun updateParents() {
         originalFunctions.forEach { (f, parent) ->
             (parent as? IrDeclarationContainer)?.addChild(f)
         }
-
-        module.patchDeclarationParents()
+        originalFunctions.clear()
     }
 
     override fun visitSimpleFunction(declaration: IrSimpleFunction): IrStatement {
@@ -189,7 +184,7 @@
             newFunction.overriddenSymbols = (original as IrSimpleFunction).overriddenSymbols
             newFunction.correspondingPropertySymbol = null
         }
-        newFunction.origin = IrDeclarationOrigin.DEFINED
+        newFunction.origin = original.origin
 
         // here generic value parameters will be applied
         newFunction.copyTypeParametersFrom(original)
@@ -302,39 +297,7 @@
             }
     }
 
-    private fun IrFunction.shouldBeRemapped(): Boolean =
-        !isLocalFunction() &&
-            !isEnumConstructor() &&
-            (hasComposableAnnotation() || hasComposableParameter())
-
-    private fun IrFunction.isLocalFunction(): Boolean =
-        origin == IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA ||
-            (isLocal && (this is IrSimpleFunction && !overridesComposable()))
-
-    private fun IrSimpleFunction.overridesComposable() =
-        overriddenSymbols.any {
-            it.owner.isDecoy() || it.owner.shouldBeRemapped()
-        }
-
-    private fun IrFunction.hasComposableParameter() =
-        valueParameters.any { it.type.hasComposable() } ||
-            extensionReceiverParameter?.type?.hasComposable() == true
-
-    private fun IrFunction.isEnumConstructor() =
-        this is IrConstructor && parentAsClass.isEnumClass
-
-    private fun IrType.hasComposable(): Boolean {
-        if (hasAnnotation(ComposeFqNames.Composable)) {
-            return true
-        }
-
-        return when (this) {
-            is IrSimpleType -> arguments.any { (it as? IrType)?.hasComposable() == true }
-            else -> false
-        }
-    }
-
     companion object {
         private const val IMPLEMENTATION_FUNCTION_SUFFIX = "\$composable"
     }
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
index ba05ddc..fc763b8 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
@@ -17,14 +17,17 @@
 package androidx.compose.compiler.plugins.kotlin.lower.decoys
 
 import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
+import androidx.compose.compiler.plugins.kotlin.lower.ComposerParamTransformer
 import androidx.compose.compiler.plugins.kotlin.lower.ModuleLoweringPass
 import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
 import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
 import org.jetbrains.kotlin.ir.IrStatement
 import org.jetbrains.kotlin.ir.declarations.IrConstructor
+import org.jetbrains.kotlin.ir.declarations.IrFunction
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
 import org.jetbrains.kotlin.ir.declarations.IrValueParameter
+import org.jetbrains.kotlin.ir.declarations.lazy.IrLazyFunctionBase
 import org.jetbrains.kotlin.ir.expressions.IrCall
 import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
 import org.jetbrains.kotlin.ir.expressions.IrDelegatingConstructorCall
@@ -35,11 +38,14 @@
 import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
 import org.jetbrains.kotlin.ir.expressions.impl.IrDelegatingConstructorCallImpl
 import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionReferenceImpl
+import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
 import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
 import org.jetbrains.kotlin.ir.util.copyTypeAndValueArgumentsFrom
 import org.jetbrains.kotlin.ir.util.patchDeclarationParents
 import org.jetbrains.kotlin.ir.util.remapTypeParameters
+import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
+import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
 
 /**
  * Replaces all decoys references to their implementations created in [CreateDecoysTransformer].
@@ -55,6 +61,10 @@
     metrics = metrics,
     signatureBuilder = signatureBuilder
 ), ModuleLoweringPass {
+    private val decoysTransformer = CreateDecoysTransformer(
+        pluginContext, symbolRemapper, signatureBuilder, metrics
+    )
+    private val lazyDeclarationsCache = mutableMapOf<IrFunctionSymbol, IrFunction>()
 
     override fun lower(module: IrModuleFragment) {
         module.transformChildrenVoid()
@@ -117,7 +127,7 @@
     }
 
     override fun visitConstructorCall(expression: IrConstructorCall): IrExpression {
-        val callee = expression.symbol.owner
+        val callee = expression.symbol.decoyOwner
         if (!callee.isDecoy()) {
             return super.visitConstructorCall(expression)
         }
@@ -144,7 +154,7 @@
     override fun visitDelegatingConstructorCall(
         expression: IrDelegatingConstructorCall
     ): IrExpression {
-        val callee = expression.symbol.owner
+        val callee = expression.symbol.decoyOwner
         if (!callee.isDecoy()) {
             return super.visitDelegatingConstructorCall(expression)
         }
@@ -167,7 +177,7 @@
     }
 
     override fun visitCall(expression: IrCall): IrExpression {
-        val callee = expression.symbol.owner
+        val callee = expression.symbol.decoyOwner
         if (!callee.isDecoy()) {
             return super.visitCall(expression)
         }
@@ -191,7 +201,7 @@
     }
 
     override fun visitFunctionReference(expression: IrFunctionReference): IrExpression {
-        val callee = expression.symbol.owner
+        val callee = expression.symbol.decoyOwner
         if (!callee.isDecoy()) {
             return super.visitFunctionReference(expression)
         }
@@ -213,4 +223,43 @@
         }
         return super.visitFunctionReference(updatedReference)
     }
+
+    private val addComposerParameterInplace = object : IrElementTransformerVoid() {
+        private val сomposerParamTransformer = ComposerParamTransformer(
+            context, symbolRemapper, true, metrics
+        )
+        override fun visitSimpleFunction(declaration: IrSimpleFunction): IrStatement {
+            return сomposerParamTransformer.visitSimpleFunction(declaration)
+        }
+    }
+
+    /**
+     * Since kotlin 1.7.20, k/js started to use LazyIr (stubs).
+     * Decoys logic used to rely on full deserialized IR of the module dependencies.
+     *
+     * This extension adjusts LazyIr for decoys needs:
+     * generates decoy implementation declarations and adds composer parameter to ensure matching
+     * signatures.
+     *
+     * To consider: LazyIr allows to get rid of decoys logic. But it's going to be a breaking
+     * change for the compose users, since all published klibs will need to be recompiled in order
+     * to be compatible with new-no-decoys compiler plugin. So we'll need to choose
+     * a good moment for such a breaking change.
+     */
+    private val IrFunctionSymbol.decoyOwner: IrFunction
+        get() = if (owner is IrLazyFunctionBase && !owner.isDecoy()) {
+            lazyDeclarationsCache.getOrPut(this) {
+                val declaration = owner
+                if (declaration.shouldBeRemapped()) {
+                    when (declaration) {
+                        is IrSimpleFunction -> decoysTransformer.visitSimpleFunction(declaration)
+                        is IrConstructor -> decoysTransformer.visitConstructor(declaration)
+                        else -> decoysTransformer.visitFunction(declaration)
+                    }.also {
+                        decoysTransformer.updateParents()
+                        owner.parent.transformChildrenVoid(addComposerParameterInplace)
+                    } as IrFunction
+                } else owner
+            }
+        } else owner
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
index 894b35c..28cef82 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import java.util.concurrent.TimeUnit
 import org.junit.After
@@ -89,6 +90,7 @@
     }
 
     @OptIn(ExperimentalLayoutApi::class)
+    @FlakyTest(bugId = 256020254)
     @Test
     fun imeAnimationWhenHidingIme() {
         val imeAnimationSourceValues = mutableListOf<Int>()
@@ -133,4 +135,4 @@
             imeAnimationTargetValues.last() == imeAnimationSourceValues.first()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldTrickyUseCase.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/RejectTextChangeDemo.kt
similarity index 98%
rename from compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldTrickyUseCase.kt
rename to compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/RejectTextChangeDemo.kt
index 3a4e5d0..442fe98 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldTrickyUseCase.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/RejectTextChangeDemo.kt
@@ -29,7 +29,7 @@
 
 @Preview
 @Composable
-fun InputFieldTrickyUseCase() {
+fun RejectTextChangeDemo() {
     LazyColumn {
         item {
             TagLine(tag = "don't set if non number is added")
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 54610b2..2b5a89f 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -22,8 +22,7 @@
 val TextDemos = DemoCategory(
     "Text",
     listOf(
-        ComposableDemo("Hyphens") { TextDemoHyphens() },
-        ComposableDemo("Static text") { TextDemo() },
+        ComposableDemo("Text Accessibility") { TextAccessibilityDemo() },
         DemoCategory(
             "Text Canvas",
             listOf(
@@ -32,57 +31,89 @@
                 ComposableDemo("Stroke") { TextStrokeDemo() }
             )
         ),
-        ComposableDemo("Ellipsize") { EllipsizeDemo() },
-        ComposableDemo("Typeface") { TypefaceDemo() },
-        ComposableDemo("Variable Fonts") { VariableFontsDemo() },
-        ComposableDemo("FontFamily fallback") { FontFamilyDemo() },
-        ComposableDemo("All system font families") { SystemFontFamilyDemo() },
-        ComposableDemo("Text selection") { TextSelectionDemo() },
-        ComposableDemo("Text selection sample") { TextSelectionSample() },
-        ComposableDemo("Multi paragraph") { MultiParagraphDemo() },
-        ComposableDemo("IncludeFontPadding & Clip") { TextFontPaddingDemo() },
-        ComposableDemo("Layout Reuse") { TextReuseLayoutDemo() },
-        ComposableDemo("Line Height Behavior") { TextLineHeightDemo() },
-        ComposableDemo("Interactive text") { InteractiveTextDemo() },
-        ComposableDemo("Min/max lines") { BasicTextMinMaxLinesDemo() },
-        ComposableDemo("Ellipsize and letterspacing") { EllipsizeWithLetterSpacing() },
-        ComposableDemo("Line breaking") { TextLineBreakingDemo() },
         DemoCategory(
-            "Text Overflow",
+            "Text Layout",
             listOf(
-                ComposableDemo("TextOverflow demo") { TextOverflowDemo() },
-                ComposableDemo("Visible overflow in drawText") {
-                    TextOverflowVisibleInDrawText()
-                },
-                ComposableDemo("Visible overflow in Popup") { TextOverflowVisibleInPopupDemo() }
+                ComposableDemo("Static text") { TextDemo() },
+                DemoCategory(
+                    "Line breaking",
+                    listOf(
+                        ComposableDemo("Line breaking") { TextLineBreakingDemo() },
+                        ComposableDemo("Hyphens") { TextDemoHyphens() },
+                        ComposableDemo("Ellipsize") { EllipsizeDemo() },
+                        ComposableDemo("Ellipsize and letterspacing") {
+                            EllipsizeWithLetterSpacing()
+                        },
+                    )
+                ),
+                DemoCategory(
+                    "Text Overflow",
+                    listOf(
+                        ComposableDemo("TextOverflow demo") { TextOverflowDemo() },
+                        ComposableDemo("Visible overflow in drawText") {
+                            TextOverflowVisibleInDrawText()
+                        },
+                        ComposableDemo("Visible overflow in Popup") {
+                            TextOverflowVisibleInPopupDemo()
+                        },
+                        ComposableDemo("Min/max lines") { BasicTextMinMaxLinesDemo() },
+                    )
+                ),
+                ComposableDemo("IncludeFontPadding & Clip") { TextFontPaddingDemo() },
+                ComposableDemo("Line Height Behavior") { TextLineHeightDemo() },
+                ComposableDemo("Layout Reuse") { TextReuseLayoutDemo() },
+                ComposableDemo("Multi paragraph") { MultiParagraphDemo() },
+                ComposableDemo("Interactive text") { InteractiveTextDemo() },
             )
         ),
         DemoCategory(
-            "Input fields",
+            "Fonts",
+            listOf(
+                ComposableDemo("Typeface") { TypefaceDemo() },
+                ComposableDemo("Variable Fonts") { VariableFontsDemo() },
+                ComposableDemo("FontFamily fallback") { FontFamilyDemo() },
+                ComposableDemo("All system font families") { SystemFontFamilyDemo() },
+            )
+        ),
+        DemoCategory(
+            "Text Input",
             listOf(
                 ComposableDemo("Basic input fields") { InputFieldDemo() },
-                ComposableDemo("Keyboard Types") { KeyboardTypeDemo() },
-                ComposableDemo("Ime Action") { ImeActionDemo() },
-                ComposableDemo("Various input fields") { VariousInputFieldDemo() },
-                ComposableDemo("Tricky input field") { InputFieldTrickyUseCase() },
-                ComposableDemo("Focus transition") { TextFieldFocusTransition() },
-                ComposableDemo("Focus keyboard interaction") {
-                    TextFieldFocusKeyboardInteraction()
+                ComposableDemo("Capitalization/AutoCorrect") {
+                    CapitalizationAutoCorrectDemo()
                 },
-                ComposableDemo("Tail Following Text Field") { TailFollowingTextFieldDemo() },
-                ComposableDemo("Scrollable text fields") { ScrollableTextFieldDemo() },
-                ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
+                ComposableDemo("Cursor configuration") { TextFieldCursorBlinkingDemo() },
+                DemoCategory(
+                    "Focus",
+                    listOf(
+                        ComposableDemo("Focus transition") { TextFieldFocusTransition() },
+                        ComposableDemo("Focus keyboard interaction") {
+                            TextFieldFocusKeyboardInteraction()
+                        },
+                    )
+                ),
+                ComposableDemo("Full-screen field") { FullScreenTextFieldDemo() },
+                ComposableDemo("Ime Action") { ImeActionDemo() },
                 ComposableDemo("Ime SingleLine") { ImeSingleLineDemo() },
-                ComposableDemo("Capitalization/AutoCorrect") { CapitalizationAutoCorrectDemo() },
-                ComposableDemo("TextFieldValue") { TextFieldValueDemo() },
                 ComposableDemo("Inside Dialog") { onNavigateUp ->
                     DialogInputFieldDemo(onNavigateUp)
                 },
                 ComposableDemo("Inside scrollable") { TextFieldsInScrollableDemo() },
-                ComposableDemo("Cursor configuration") { TextFieldCursorBlinkingDemo() },
-                ComposableDemo("Full-screen field") { FullScreenTextFieldDemo() },
+                ComposableDemo("Keyboard Types") { KeyboardTypeDemo() },
+                ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
+                ComposableDemo("Reject Text Change") { RejectTextChangeDemo() },
+                ComposableDemo("Scrollable text fields") { ScrollableTextFieldDemo() },
+                ComposableDemo("Visual Transformation") { VisualTransformationDemo() },
+                ComposableDemo("TextFieldValue") { TextFieldValueDemo() },
+                ComposableDemo("Tail Following Text Field") { TailFollowingTextFieldDemo() },
             )
         ),
-        ComposableDemo("Text Accessibility") { TextAccessibilityDemo() }
+        DemoCategory(
+            "Selection",
+            listOf(
+                ComposableDemo("Text selection") { TextSelectionDemo() },
+                ComposableDemo("Text selection sample") { TextSelectionSample() },
+            )
+        )
     )
 )
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
index dd5c30b..880f509 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
@@ -86,7 +86,7 @@
         text = "Normal blink",
         selection = TextRange(3)
     )
-    BasicTextField(value = textFieldValue, >
+    BasicTextField(value = textFieldValue, modifier = demoTextFieldModifiers, >
 }
 
 @Composable
@@ -97,6 +97,7 @@
     )
     BasicTextField(
         value = textFieldValue,
+        modifier = demoTextFieldModifiers,
         >
         cursorBrush = SolidColor(Color.Red)
     )
@@ -130,7 +131,7 @@
         value = textFieldValue,
         >
         cursorBrush = SolidColor(color.value),
-        modifier = Modifier.onFocusChanged { shouldAnimate = it.isFocused }
+        modifier = demoTextFieldModifiers.onFocusChanged { shouldAnimate = it.isFocused }
     )
 }
 
@@ -143,6 +144,7 @@
 
     BasicTextField(
         value = textFieldValue,
+        modifier = demoTextFieldModifiers,
         >
         cursorBrush = Brush.verticalGradient(colors = Rainbow),
     )
@@ -168,7 +170,7 @@
     BasicTextField(
         value = textFieldValue,
         >
-        modifier = Modifier.onFocusChanged { animate = it.isFocused }
+        modifier = demoTextFieldModifiers.onFocusChanged { animate = it.isFocused }
     )
 }
 
@@ -190,6 +192,7 @@
     Column {
         BasicTextField(
             value = textFieldValue,
+            modifier = demoTextFieldModifiers,
             >
             textStyle = TextStyle.Default.copy(fontFamily = FontFamily.Monospace)
         )
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/VisualTransformationDemo.kt
similarity index 99%
rename from compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt
rename to compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/VisualTransformationDemo.kt
index d51f8f2..7889ff5 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/VisualTransformationDemo.kt
@@ -153,7 +153,7 @@
 
 @Preview
 @Composable
-fun VariousInputFieldDemo() {
+fun VisualTransformationDemo() {
     LazyColumn {
         item {
             TagLine(tag = "Capitalization")
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt
new file mode 100644
index 0000000..90fa1d7
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 android.os.Build
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.COMPILATION_MODES
+import androidx.testutils.measureStartup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * This benchmark is used to verify that compilation mode performance is consistent across runs,
+ * and that compilation state doesn't leak across benchmarks.
+ */
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+@RunWith(Parameterized::class)
+class CompilationConsistencyBenchmark(
+    @Suppress("UNUSED_PARAMETER") iteration: Int,
+    private val compilationMode: CompilationMode
+) {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    @Test
+    fun startup() = benchmarkRule.measureStartup(
+        compilationMode = compilationMode,
+        startupMode = StartupMode.COLD,
+        packageName = packageName
+    ) {
+        action = "androidx.compose.integration.macrobenchmark.target.LAZY_COLUMN_ACTIVITY"
+        putExtra("ITEM_COUNT", 5)
+    }
+
+    companion object {
+        val packageName = "androidx.compose.integration.macrobenchmark.target"
+
+        @Parameterized.Parameters(name = "iter={0},compilation={1}")
+        @JvmStatic
+        fun parameters(): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+            for (iter in 1..4) {
+                for (compilationMode in COMPILATION_MODES) {
+                    add(arrayOf(iter, compilationMode))
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
index 1147a76..73e69950 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
@@ -70,7 +70,7 @@
             )
             .swipeAnchors(
                 state = swipeableState,
-                possibleStates = possibleStates,
+                possibleValues = possibleStates,
                 calculateAnchor = calculateAnchor
             )
             .offset {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2AnchorTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2AnchorTest.kt
index c7f0a55..6fb31971 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2AnchorTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2AnchorTest.kt
@@ -56,14 +56,14 @@
         lateinit var state: SwipeableV2State<TestState>
 
         rule.setContent {
-            state = rememberSwipeableV2State(initialState = A)
+            state = rememberSwipeableV2State(initialValue = A)
             compositionCounter++
             Box(
                 Modifier
                     .height(200.dp)
                     .swipeAnchors(
                         state,
-                        possibleStates = setOf(A, B, C)
+                        possibleValues = setOf(A, B, C)
                     ) { state, layoutSize ->
                         when (state) {
                             A -> 0f
@@ -92,13 +92,13 @@
         val swipeableSize = 200.dp
 
         rule.setContent {
-            state = rememberSwipeableV2State(initialState = A)
+            state = rememberSwipeableV2State(initialValue = A)
             Box(
                 Modifier
                     .requiredHeight(swipeableSize)
                     .swipeAnchors(
                         state,
-                        possibleStates = setOf(A, B, C)
+                        possibleValues = setOf(A, B, C)
                     ) { state, layoutSize ->
                         when (state) {
                             A -> 0f
@@ -131,7 +131,7 @@
                     .size(size.dp) // Trigger remeasure when size changes
                     .swipeAnchors(
                         state,
-                        possibleStates = setOf(A, B, C),
+                        possibleValues = setOf(A, B, C),
                         calculateAnchor = { state, _ -> anchors[state] }
                     )
             )
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
index 551c9bc..a748549 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
@@ -167,36 +167,36 @@
         state.dispatchRawDelta(positionOfA + distance * (absThreshold * 0.9f))
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(A)
-        assertThat(state.targetState).isEqualTo(A)
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
 
         state.dispatchRawDelta(distance * 0.2f)
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(A)
-        assertThat(state.targetState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(B)
 
         runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
 
-        assertThat(state.currentState).isEqualTo(B)
-        assertThat(state.targetState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
 
         state.dispatchRawDelta(-distance * (absThreshold * 0.9f))
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(B)
-        assertThat(state.targetState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
 
         state.dispatchRawDelta(-distance * 0.2f)
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(B)
-        assertThat(state.targetState).isEqualTo(A)
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(A)
 
         runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
 
-        assertThat(state.currentState).isEqualTo(A)
-        assertThat(state.targetState).isEqualTo(A)
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
     }
 
     @Test
@@ -220,39 +220,39 @@
         state.dispatchRawDelta(initialOffset + (absThreshold * 0.9f))
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(A)
-        assertThat(state.targetState).isEqualTo(A)
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
 
         // Swipe towards B, close after threshold
         state.dispatchRawDelta(absThreshold * 0.2f)
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(A)
-        assertThat(state.targetState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(B)
 
         runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
 
-        assertThat(state.currentState).isEqualTo(B)
-        assertThat(state.targetState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
 
         // Swipe towards A, close before threshold
         state.dispatchRawDelta(-(absThreshold * 0.9f))
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(B)
-        assertThat(state.targetState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
 
         // Swipe towards A, close after threshold
         state.dispatchRawDelta(-(absThreshold * 0.2f))
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(B)
-        assertThat(state.targetState).isEqualTo(A)
+        assertThat(state.currentValue).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(A)
 
         runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
 
-        assertThat(state.currentState).isEqualTo(A)
-        assertThat(state.targetState).isEqualTo(A)
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
     }
 
     @Test
@@ -273,7 +273,7 @@
             }
             state.dispatchRawDelta(60f)
             state.settle(velocityPx)
-            assertThat(state.currentState).isEqualTo(B)
+            assertThat(state.currentValue).isEqualTo(B)
         }
 
     @Test
@@ -293,7 +293,7 @@
             )
             state.dispatchRawDelta(60f)
             state.settle(velocityPx / 2)
-            assertThat(state.currentState).isEqualTo(A)
+            assertThat(state.currentValue).isEqualTo(A)
         }
 
     @Test
@@ -315,7 +315,7 @@
             }
 
         rule.waitForIdle()
-        assertThat(state.currentState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(B)
     }
 
     @Test
@@ -338,7 +338,7 @@
             }
 
         rule.waitForIdle()
-        assertThat(state.currentState).isEqualTo(A)
+        assertThat(state.currentValue).isEqualTo(A)
     }
 
     private fun SwipeableTestState(
@@ -348,7 +348,7 @@
         velocityThreshold: Dp = 125.dp,
         anchors: Map<TestState, Float>? = null
     ) = SwipeableV2State(
-        initialState = initialState,
+        initialValue = initialState,
         positionalThreshold = positionalThreshold,
         velocityThreshold = velocityThreshold,
         density = density
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
index d7433e2..10da79f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
@@ -58,7 +58,7 @@
     fun swipeable_state_canSkipStateByFling() {
         lateinit var state: SwipeableV2State<TestState>
         rule.setContent {
-            state = rememberSwipeableV2State(initialState = A)
+            state = rememberSwipeableV2State(initialValue = A)
             SwipeableBox(
                 swipeableState = state,
                 orientation = Orientation.Vertical,
@@ -71,14 +71,14 @@
 
         rule.waitForIdle()
 
-        assertThat(state.currentState).isEqualTo(C)
+        assertThat(state.currentValue).isEqualTo(C)
     }
 
     @Test
     fun swipeable_targetState_updatedOnSwipe() {
         lateinit var state: SwipeableV2State<TestState>
         rule.setContent {
-            state = rememberSwipeableV2State(initialState = A)
+            state = rememberSwipeableV2State(initialValue = A)
             SwipeableBox(
                 swipeableState = state,
                 orientation = Orientation.Vertical,
@@ -89,17 +89,17 @@
         rule.onNodeWithTag(swipeableTestTag)
             .performTouchInput { swipeDown(endY = bottom * 0.45f) }
         rule.waitForIdle()
-        assertThat(state.targetState).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
 
         rule.onNodeWithTag(swipeableTestTag)
             .performTouchInput { swipeDown(endY = bottom * 0.9f) }
         rule.waitForIdle()
-        assertThat(state.targetState).isEqualTo(C)
+        assertThat(state.targetValue).isEqualTo(C)
 
         rule.onNodeWithTag(swipeableTestTag)
             .performTouchInput { swipeUp(endY = top * 1.1f) }
         rule.waitForIdle()
-        assertThat(state.targetState).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
     }
 
     @Test
@@ -110,7 +110,7 @@
         lateinit var scope: CoroutineScope
         rule.setContent {
             state = rememberSwipeableV2State(
-                initialState = A,
+                initialValue = A,
                 animationSpec = tween(animationDuration, easing = LinearEasing)
             )
             scope = rememberCoroutineScope()
@@ -122,24 +122,24 @@
         }
 
         scope.launch {
-            state.animateTo(targetState = B)
+            state.animateTo(targetValue = B)
         }
         rule.mainClock.advanceTimeBy((animationDuration * 0.6).toLong())
 
         assertWithMessage("Current state")
-            .that(state.currentState)
+            .that(state.currentValue)
             .isEqualTo(A)
         assertWithMessage("Target state")
-            .that(state.targetState)
+            .that(state.targetValue)
             .isEqualTo(B)
 
         rule.mainClock.advanceTimeBy((animationDuration * 0.4).toLong())
 
         assertWithMessage("Current state")
-            .that(state.currentState)
+            .that(state.currentValue)
             .isEqualTo(B)
         assertWithMessage("Target state")
-            .that(state.targetState)
+            .that(state.targetValue)
             .isEqualTo(B)
     }
 
@@ -147,7 +147,7 @@
     fun swipeable_progress_matchesSwipePosition() {
         lateinit var state: SwipeableV2State<TestState>
         rule.setContent {
-            state = rememberSwipeableV2State(initialState = A)
+            state = rememberSwipeableV2State(initialValue = A)
             WithTouchSlop(touchSlop = 0f) {
                 SwipeableBox(
                     swipeableState = state,
@@ -164,7 +164,7 @@
         rule.onNodeWithTag(swipeableTestTag)
             .performTouchInput { swipeDown(endY = almostAnchorB) }
 
-        assertThat(state.targetState).isEqualTo(B)
+        assertThat(state.targetValue).isEqualTo(B)
         assertThat(state.progress).isEqualTo(expectedProgress)
 
         val almostAnchorA = anchorA + ((anchorB - anchorA) * 0.1f)
@@ -173,7 +173,7 @@
         rule.onNodeWithTag(swipeableTestTag)
             .performTouchInput { swipeUp(startY = anchorB, endY = almostAnchorA) }
 
-        assertThat(state.targetState).isEqualTo(A)
+        assertThat(state.targetValue).isEqualTo(A)
         assertThat(state.progress).isEqualTo(expectedProgress)
     }
 
@@ -181,7 +181,7 @@
     fun swipeable_snapTo_updatesImmediately() = runBlocking {
         lateinit var state: SwipeableV2State<TestState>
         rule.setContent {
-            state = rememberSwipeableV2State(initialState = A)
+            state = rememberSwipeableV2State(initialValue = A)
             SwipeableBox(
                 swipeableState = state,
                 orientation = Orientation.Vertical
@@ -189,7 +189,7 @@
         }
 
         state.snapTo(C)
-        assertThat(state.currentState)
+        assertThat(state.currentValue)
             .isEqualTo(C)
     }
 
@@ -210,26 +210,26 @@
 
         restorationTester.emulateSavedInstanceStateRestore()
 
-        assertThat(state.currentState).isEqualTo(initialState)
+        assertThat(state.currentValue).isEqualTo(initialState)
         assertThat(state.animationSpec).isEqualTo(animationSpec)
 
         scope.launch {
             state.animateTo(B)
         }
         rule.waitForIdle()
-        assertThat(state.currentState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(B)
 
         restorationTester.emulateSavedInstanceStateRestore()
-        assertThat(state.currentState).isEqualTo(B)
+        assertThat(state.currentValue).isEqualTo(B)
     }
 
     @Test
     fun swipeable_targetState_accessedInInitialComposition() {
         lateinit var targetState: TestState
         rule.setContent {
-            val state = rememberSwipeableV2State(initialState = B)
-            LaunchedEffect(state.targetState) {
-                targetState = state.targetState
+            val state = rememberSwipeableV2State(initialValue = B)
+            LaunchedEffect(state.targetValue) {
+                targetState = state.targetValue
             }
             SwipeableBox(state)
         }
@@ -241,7 +241,7 @@
     fun swipeable_progress_accessedInInitialComposition() {
         var progress = Float.NaN
         rule.setContent {
-            val state = rememberSwipeableV2State(initialState = B)
+            val state = rememberSwipeableV2State(initialValue = B)
             LaunchedEffect(state.progress) {
                 progress = state.progress
             }
@@ -258,7 +258,7 @@
         lateinit var state: SwipeableV2State<TestState>
         var offset: Float? = null
         rule.setContent {
-            state = rememberSwipeableV2State(initialState = B)
+            state = rememberSwipeableV2State(initialValue = B)
             SwipeableBox(state)
             exception = runCatching { offset = state.requireOffset() }.exceptionOrNull()
         }
@@ -275,7 +275,7 @@
     fun swipeable_requireOffset_accessedInEffect_doesntThrow() {
         var exception: Throwable? = null
         rule.setContent {
-            val state = rememberSwipeableV2State(initialState = B)
+            val state = rememberSwipeableV2State(initialValue = B)
             LaunchedEffect(Unit) {
                 exception = runCatching { state.requireOffset() }.exceptionOrNull()
             }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
index b21f1bc..a9df38a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
@@ -44,12 +44,12 @@
 import kotlinx.coroutines.launch
 
 /**
- * Enable swipe gestures between a set of predefined states.
+ * Enable swipe gestures between a set of predefined values.
  *
  * When a swipe is detected, the offset of the [SwipeableV2State] will be updated with the swipe
  * delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
  * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableV2State] will also be updated to the state corresponding to
+ * reached, the value of the [SwipeableV2State] will also be updated to the value corresponding to
  * the new anchor.
  *
  * Swiping is constrained between the minimum and maximum anchors.
@@ -84,24 +84,24 @@
  * the state with them.
  *
  * @param state The associated [SwipeableV2State]
- * @param possibleStates All possible states the [SwipeableV2State] could be in.
+ * @param possibleValues All possible values the [SwipeableV2State] could be in.
  * @param anchorsChanged A callback to be invoked when the anchors have changed, `null` by default.
  * Components with custom reconciliation logic should implement this callback, i.e. to re-target an
  * in-progress animation.
  * @param calculateAnchor This method will be invoked to calculate the position of all
- * [possibleStates], given this node's layout size. Return the anchor's offset from the initial
- * anchor, or `null` to indicate that a state does not exist.
+ * [possibleValues], given this node's layout size. Return the anchor's offset from the initial
+ * anchor, or `null` to indicate that a value does not have an anchor.
  */
 @ExperimentalMaterialApi
 internal fun <T> Modifier.swipeAnchors(
     state: SwipeableV2State<T>,
-    possibleStates: Set<T>,
+    possibleValues: Set<T>,
     anchorsChanged: ((oldAnchors: Map<T, Float>, newAnchors: Map<T, Float>) -> Unit)? = null,
-    calculateAnchor: (state: T, layoutSize: IntSize) -> Float?,
+    calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
 ) = onSizeChanged { layoutSize ->
     val previousAnchors = state.anchors
     val newAnchors = mutableMapOf<T, Float>()
-    possibleStates.forEach {
+    possibleValues.forEach {
         val anchorValue = calculateAnchor(it, layoutSize)
         if (anchorValue != null) {
             newAnchors[it] = anchorValue
@@ -122,10 +122,10 @@
  * to change the state either immediately or by starting an animation. To create and remember a
  * [SwipeableV2State] use [rememberSwipeableV2State].
  *
- * @param initialState The initial value of the state.
+ * @param initialValue The initial value of the state.
  * @param density The density used to convert thresholds from px to dp.
  * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
  * @param positionalThreshold The positional threshold to be used when calculating the target state
  * while a swipe is in progress and when settling after the swipe ends. This is the distance from
  * the start of a transition. It will be, depending on the direction of the interaction, added or
@@ -138,31 +138,31 @@
 @Stable
 @ExperimentalMaterialApi
 internal class SwipeableV2State<T>(
-    initialState: T,
+    initialValue: T,
     internal val density: Density,
     internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
-    internal val confirmStateChange: (newValue: T) -> Boolean = { true },
+    internal val confirmValueChange: (newValue: T) -> Boolean = { true },
     internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
         SwipeableV2Defaults.PositionalThreshold,
     internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
 ) {
 
     /**
-     * The current state of the [SwipeableV2State].
+     * The current value of the [SwipeableV2State].
      */
-    var currentState: T by mutableStateOf(initialState)
+    var currentValue: T by mutableStateOf(initialValue)
         private set
 
     /**
-     * The target state. This is the closest state to the current offset (taking into account
+     * The target value. This is the closest value to the current offset (taking into account
      * positional thresholds). If no interactions like animations or drags are in progress, this
-     * will be the current state.
+     * will be the current value.
      */
-    val targetState: T by derivedStateOf {
+    val targetValue: T by derivedStateOf {
         val currentOffset = offset
         if (currentOffset != null) {
-            computeTarget(currentOffset, currentState, velocity = 0f)
-        } else currentState
+            computeTarget(currentOffset, currentValue, velocity = 0f)
+        } else currentValue
     }
 
     /**
@@ -199,12 +199,13 @@
         private set
 
     /**
-     * The fraction of the progress going from currentState to targetState, within [0f..1f] bounds.
+     * The fraction of the progress going from [currentValue] to [targetValue], within [0f..1f]
+     * bounds.
      */
     /*@FloatRange(from = 0f, to = 1f)*/
     val progress: Float by derivedStateOf {
-        val a = anchors[currentState] ?: 0f
-        val b = anchors[targetState] ?: 0f
+        val a = anchors[currentValue] ?: 0f
+        val b = anchors[targetValue] ?: 0f
         val distance = abs(b - a)
         if (distance > 1e-6f) {
             val progress = (this.requireOffset() - a) / (b - a)
@@ -239,45 +240,45 @@
         val previousAnchorsEmpty = anchors.isEmpty()
         anchors = newAnchors
         if (previousAnchorsEmpty) {
-            dragPosition = anchors.requireAnchor(this.currentState)
+            dragPosition = anchors.requireAnchor(this.currentValue)
         }
     }
 
     /**
-     * Whether the [state] has an anchor associated with it.
+     * Whether the [value] has an anchor associated with it.
      */
-    fun hasAnchorForState(state: T): Boolean = anchors.containsKey(state)
+    fun hasAnchorForValue(value: T): Boolean = anchors.containsKey(value)
 
     /**
-     * Snap to a [targetState] without any animation.
+     * Snap to a [targetValue] without any animation.
      *
      * @throws CancellationException if the interaction interrupted by another interaction like a
      * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
      *
-     * @param targetState The target state of the animation
+     * @param targetValue The target value of the animation
      */
-    suspend fun snapTo(targetState: T) {
-        val targetOffset = anchors.requireAnchor(targetState)
+    suspend fun snapTo(targetValue: T) {
+        val targetOffset = anchors.requireAnchor(targetValue)
         draggableState.drag {
             dragBy(targetOffset - requireOffset())
         }
-        this.currentState = targetState
+        this.currentValue = targetValue
     }
 
     /**
-     * Animate to a [targetState].
+     * Animate to a [targetValue].
      *
      * @throws CancellationException if the interaction interrupted by another interaction like a
      * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
      *
-     * @param targetState The target state of the animation
+     * @param targetValue The target value of the animation
      * @param velocity The velocity the animation should start with, [lastVelocity] by default
      */
     suspend fun animateTo(
-        targetState: T,
+        targetValue: T,
         velocity: Float = lastVelocity,
     ) {
-        val targetOffset = anchors.requireAnchor(targetState)
+        val targetOffset = anchors.requireAnchor(targetValue)
         try {
             draggableState.drag {
                 isAnimationRunning = true
@@ -302,7 +303,7 @@
                 .entries
                 .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
                 ?.key
-            this.currentState = endState ?: currentState
+            this.currentValue = endState ?: currentValue
         }
     }
 
@@ -310,17 +311,17 @@
      * Find the closest anchor taking into account the velocity and settle at it with an animation.
      */
     suspend fun settle(velocity: Float) {
-        val previousState = this.currentState
-        val targetState = computeTarget(
+        val previousValue = this.currentValue
+        val targetValue = computeTarget(
             offset = requireOffset(),
-            currentState = previousState,
+            currentValue = previousValue,
             velocity = velocity
         )
-        if (confirmStateChange(targetState)) {
-            animateTo(targetState, velocity)
+        if (confirmValueChange(targetValue)) {
+            animateTo(targetValue, velocity)
         } else {
             // If the user vetoed the state change, rollback to the previous state.
-            animateTo(previousState, velocity)
+            animateTo(previousValue, velocity)
         }
     }
 
@@ -342,32 +343,32 @@
 
     private fun computeTarget(
         offset: Float,
-        currentState: T,
+        currentValue: T,
         velocity: Float
     ): T {
         val currentAnchors = anchors
-        val currentAnchor = currentAnchors.requireAnchor(currentState)
+        val currentAnchor = currentAnchors.requireAnchor(currentValue)
         return if (currentAnchor <= offset) {
             // Swiping from lower to upper (positive).
             if (velocity >= velocityThresholdPx) {
-                currentAnchors.closestState(offset, true)
+                currentAnchors.closestAnchor(offset, true)
             } else {
-                val upper = currentAnchors.closestState(offset, true)
+                val upper = currentAnchors.closestAnchor(offset, true)
                 val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
                 val relativeThreshold = abs(positionalThreshold(density, distance))
                 val absoluteThreshold = abs(currentAnchor + relativeThreshold)
-                if (offset < absoluteThreshold) currentState else upper
+                if (offset < absoluteThreshold) currentValue else upper
             }
         } else {
             // Swiping from upper to lower (negative).
             if (velocity <= -velocityThresholdPx) {
-                currentAnchors.closestState(offset, false)
+                currentAnchors.closestAnchor(offset, false)
             } else {
-                val lower = currentAnchors.closestState(offset, false)
+                val lower = currentAnchors.closestAnchor(offset, false)
                 val distance = abs(currentAnchor - currentAnchors.getValue(lower))
                 val relativeThreshold = abs(positionalThreshold(density, distance))
                 val absoluteThreshold = abs(currentAnchor - relativeThreshold)
-                if (offset > absoluteThreshold) currentState else lower
+                if (offset > absoluteThreshold) currentValue else lower
             }
         }
     }
@@ -379,17 +380,17 @@
         @ExperimentalMaterialApi
         fun <T : Any> Saver(
             animationSpec: AnimationSpec<Float>,
-            confirmStateChange: (T) -> Boolean,
+            confirmValueChange: (T) -> Boolean,
             positionalThreshold: Density.(distance: Float) -> Float,
             velocityThreshold: Dp,
             density: Density
         ) = Saver<SwipeableV2State<T>, T>(
-            save = { it.currentState },
+            save = { it.currentValue },
             restore = {
                 SwipeableV2State(
-                    initialState = it,
+                    initialValue = it,
                     animationSpec = animationSpec,
-                    confirmStateChange = confirmStateChange,
+                    confirmValueChange = confirmValueChange,
                     positionalThreshold = positionalThreshold,
                     velocityThreshold = velocityThreshold,
                     density = density
@@ -402,32 +403,32 @@
 /**
  * Create and remember a [SwipeableV2State].
  *
- * @param initialState The initial state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param initialValue The initial value.
+ * @param animationSpec The default animation that will be used to animate to a new value.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending value change.
  */
 @Composable
 @ExperimentalMaterialApi
 internal fun <T : Any> rememberSwipeableV2State(
-    initialState: T,
+    initialValue: T,
     animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
-    confirmStateChange: (newValue: T) -> Boolean = { true }
+    confirmValueChange: (newValue: T) -> Boolean = { true }
 ): SwipeableV2State<T> {
     val density = LocalDensity.current
     return rememberSaveable(
-        initialState, animationSpec, confirmStateChange, density,
+        initialValue, animationSpec, confirmValueChange, density,
         saver = SwipeableV2State.Saver(
             animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange,
+            confirmValueChange = confirmValueChange,
             positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
             velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
             density = density
         ),
     ) {
         SwipeableV2State(
-            initialState = initialState,
+            initialValue = initialValue,
             animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange,
+            confirmValueChange = confirmValueChange,
             positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
             velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
             density = density
@@ -484,11 +485,11 @@
         fixedPositionalThreshold(56.dp)
 }
 
-private fun <T> Map<T, Float>.closestState(
+private fun <T> Map<T, Float>.closestAnchor(
     offset: Float = 0f,
     searchUpwards: Boolean = false
 ): T {
-    require(isNotEmpty()) { "The anchors were empty when trying to find the closest state" }
+    require(isNotEmpty()) { "The anchors were empty when trying to find the closest anchor" }
     return minBy { (_, anchor) ->
         val delta = if (searchUpwards) anchor - offset else offset - anchor
         if (delta < 0) Float.POSITIVE_INFINITY else delta
@@ -497,6 +498,6 @@
 
 private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
 private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(state: T) = requireNotNull(this[state]) {
-    "Required anchor $state was not found in anchors. Current anchors: ${this.toMap()}"
+private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
+    "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
 }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchBarTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchBarTokens.kt
new file mode 100644
index 0000000..6cc751a
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchBarTokens.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// VERSION: v0_126
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SearchBarTokens {
+    val AvatarShape = ShapeKeyTokens.CornerFull
+    val AvatarSize = 30.0.dp
+    val ContainerColor = ColorSchemeKeyTokens.Surface
+    val ContainerElevation = ElevationTokens.Level3
+    val ContainerHeight = 56.0.dp
+    val ContainerShape = ShapeKeyTokens.CornerFull
+    val ContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+    val HoverSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val InputTextColor = ColorSchemeKeyTokens.OnSurface
+    val InputTextFont = TypographyKeyTokens.BodyLarge
+    val LeadingIconColor = ColorSchemeKeyTokens.OnSurface
+    val PressedSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val SupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val SupportingTextFont = TypographyKeyTokens.BodyLarge
+    val TrailingIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchViewTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchViewTokens.kt
new file mode 100644
index 0000000..ae626cf
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchViewTokens.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// VERSION: v0_126
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SearchViewTokens {
+    val ContainerColor = ColorSchemeKeyTokens.Surface
+    val ContainerElevation = ElevationTokens.Level3
+    val ContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+    val DividerColor = ColorSchemeKeyTokens.Outline
+    val DockedContainerShape = ShapeKeyTokens.CornerExtraLarge
+    val DockedHeaderContainerHeight = 56.0.dp
+    val FullScreenContainerShape = ShapeKeyTokens.CornerNone
+    val FullScreenHeaderContainerHeight = 72.0.dp
+    val HeaderInputTextColor = ColorSchemeKeyTokens.OnSurface
+    val HeaderInputTextFont = TypographyKeyTokens.BodyLarge
+    val HeaderLeadingIconColor = ColorSchemeKeyTokens.OnSurface
+    val HeaderSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val HeaderSupportingTextFont = TypographyKeyTokens.BodyLarge
+    val HeaderTrailingIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+}
\ No newline at end of file
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 48e2c23..e637a6a 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
@@ -147,59 +147,6 @@
     }
 
     /**
-     * A [Job] used as a parent of any effects created by this [Recomposer]'s compositions.
-     * Its cleanup is used to advance to [State.ShuttingDown] or [State.ShutDown].
-     */
-    private val effectJob = Job(effectCoroutineContext[Job]).apply {
-        invokeOnCompletion { throwable ->
-            // Since the running recompose job is operating in a disjoint job if present,
-            // kick it out and make sure no new ones start if we have one.
-            val cancellation = CancellationException("Recomposer effect job completed", throwable)
-
-            var continuationToResume: CancellableContinuation<Unit>? = null
-            synchronized(stateLock) {
-                val runnerJob = runnerJob
-                if (runnerJob != null) {
-                    _state.value = State.ShuttingDown
-                    // If the recomposer is closed we will let the runnerJob return from
-                    // runRecomposeAndApplyChanges normally and consider ourselves shut down
-                    // immediately.
-                    if (!isClosed) {
-                        // This is the job hosting frameContinuation; no need to resume it otherwise
-                        runnerJob.cancel(cancellation)
-                    } else if (workContinuation != null) {
-                        continuationToResume = workContinuation
-                    }
-                    workContinuation = null
-                    runnerJob.invokeOnCompletion { runnerJobCause ->
-                        synchronized(stateLock) {
-                            closeCause = throwable?.apply {
-                                runnerJobCause
-                                    ?.takeIf { it !is CancellationException }
-                                    ?.let { addSuppressed(it) }
-                            }
-                            _state.value = State.ShutDown
-                        }
-                    }
-                } else {
-                    closeCause = cancellation
-                    _state.value = State.ShutDown
-                }
-            }
-            continuationToResume?.resume(Unit)
-        }
-    }
-
-    /**
-     * The [effectCoroutineContext] is derived from the parameter of the same name.
-     */
-    internal override val effectCoroutineContext: CoroutineContext =
-        effectCoroutineContext + broadcastFrameClock + effectJob
-
-    internal override val recomposeCoroutineContext: CoroutineContext
-        get() = EmptyCoroutineContext
-
-    /**
      * Valid operational states of a [Recomposer].
      */
     enum class State {
@@ -271,6 +218,63 @@
     private val _state = MutableStateFlow(State.Inactive)
 
     /**
+     * A [Job] used as a parent of any effects created by this [Recomposer]'s compositions.
+     * Its cleanup is used to advance to [State.ShuttingDown] or [State.ShutDown].
+     *
+     * Initialized after other state above, since it is possible for [Job.invokeOnCompletion]
+     * to run synchronously during construction if the [Recomposer] is constructed with
+     * a completed or cancelled [Job].
+     */
+    private val effectJob = Job(effectCoroutineContext[Job]).apply {
+        invokeOnCompletion { throwable ->
+            // Since the running recompose job is operating in a disjoint job if present,
+            // kick it out and make sure no new ones start if we have one.
+            val cancellation = CancellationException("Recomposer effect job completed", throwable)
+
+            var continuationToResume: CancellableContinuation<Unit>? = null
+            synchronized(stateLock) {
+                val runnerJob = runnerJob
+                if (runnerJob != null) {
+                    _state.value = State.ShuttingDown
+                    // If the recomposer is closed we will let the runnerJob return from
+                    // runRecomposeAndApplyChanges normally and consider ourselves shut down
+                    // immediately.
+                    if (!isClosed) {
+                        // This is the job hosting frameContinuation; no need to resume it otherwise
+                        runnerJob.cancel(cancellation)
+                    } else if (workContinuation != null) {
+                        continuationToResume = workContinuation
+                    }
+                    workContinuation = null
+                    runnerJob.invokeOnCompletion { runnerJobCause ->
+                        synchronized(stateLock) {
+                            closeCause = throwable?.apply {
+                                runnerJobCause
+                                    ?.takeIf { it !is CancellationException }
+                                    ?.let { addSuppressed(it) }
+                            }
+                            _state.value = State.ShutDown
+                        }
+                    }
+                } else {
+                    closeCause = cancellation
+                    _state.value = State.ShutDown
+                }
+            }
+            continuationToResume?.resume(Unit)
+        }
+    }
+
+    /**
+     * The [effectCoroutineContext] is derived from the parameter of the same name.
+     */
+    internal override val effectCoroutineContext: CoroutineContext =
+        effectCoroutineContext + broadcastFrameClock + effectJob
+
+    internal override val recomposeCoroutineContext: CoroutineContext
+        get() = EmptyCoroutineContext
+
+    /**
      * Determine the new value of [_state]. Call only while locked on [stateLock].
      * If it returns a continuation, that continuation should be resumed after releasing the lock.
      */
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
index 0a2bbe1..65078df 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
@@ -304,6 +304,11 @@
 
         assertEquals(1, recompostions)
     }
+
+    @Test
+    fun constructRecomposerWithCancelledJob() {
+        Recomposer(Job().apply { cancel() })
+    }
 }
 
 class UnitApplier : Applier<Unit> {
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 72e371e..99ae536 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -73,6 +73,7 @@
         androidTestImplementation(libs.mockitoCore)
         androidTestImplementation(libs.dexmakerMockito)
         androidTestImplementation(libs.mockitoKotlin)
+        androidTestImplementation(libs.kotlinTest)
 
         samples(project(":compose:ui:ui-test:ui-test-samples"))
     }
@@ -133,6 +134,7 @@
                 implementation(libs.mockitoCore)
                 implementation(libs.mockitoKotlin)
                 implementation(libs.dexmakerMockito)
+                implementation(libs.kotlinTest)
             }
 
             desktopMain.dependencies {
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt
new file mode 100644
index 0000000..8730103
--- /dev/null
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test
+
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.util.TestCounter
+import androidx.test.filters.SmallTest
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+class PhaseOrderingTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun singlePass() {
+        val counter = TestCounter()
+        rule.setContent {
+            // This should never recompose.
+            counter.expect(1)
+
+            LaunchedEffect(Unit) {
+                counter.expect(2)
+                withFrameNanos {
+                    counter.expect(6)
+                    launch {
+                        // TODO(b/254115946) No continuations resumed during a frame should be
+                        //  dispatched until after the frame callbacks finish running.
+                        counter.expect(7)
+                    }
+                    counter.expect(8)
+                }
+                counter.expect(9)
+            }
+
+            Layout(
+                content = {},
+                modifier = Modifier.drawBehind {
+                    counter.expect(5)
+                }
+            ) { _, _ ->
+                counter.expect(3)
+                layout(1, 1) {
+                    counter.expect(4)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun frameCallbackRestartsLayout() {
+        val counter = TestCounter()
+        var firstPass = true
+
+        rule.setContent {
+            LaunchedEffect(Unit) {
+                counter.expect(1)
+                withFrameNanos {
+                    // This won't run until the 2nd frame, so the first layout happens before it.
+                    counter.expect(4)
+                    firstPass = false
+                }
+                // TODO(b/222093277) This should happen *after* the next layout pass.
+                counter.expect(5)
+            }
+
+            Layout(content = {}) { _, _ ->
+                // TODO(b/222093277) This should happen *before* the next layout pass.
+                counter.expect(if (firstPass) 2 else 6)
+                layout(1, 1) {
+                    counter.expect(3)
+                }
+            }
+        }
+    }
+}
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
index 60bf60c..22f51cb 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
@@ -17,57 +17,307 @@
 package androidx.compose.ui.test
 
 import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.test.util.TestCounter
 import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.currentTime
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
 import org.junit.Assert.assertEquals
-import org.junit.Assert.fail
 import org.junit.Test
 
-@ExperimentalCoroutinesApi
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 class TestMonotonicFrameClockTest {
+
+    // These delay tests aren't just testing that the test dispatchers correctly skip delays
+    // (which is code not under our control and thus not worth testing), but rather that the
+    // TestMonotonicFrameClock machinery doesn't break the delay mechanism and end up using wall
+    // clock time for delays.
     @Test
-    fun testMonotonicFrameClockRunsFrame() = runTest(UnconfinedTestDispatcher()) {
-        val frameDelayNanos = 16_000_000L
-        withContext(TestMonotonicFrameClock(this, frameDelayNanos)) {
-            val startTime = currentTime
-            val expectedFrameTime = startTime + frameDelayNanos / 1_000_000
-            val counter = TestCounter()
+    fun delaysAreSkipped_unconfinedDispatcher() = runTest(UnconfinedTestDispatcher()) {
+        val delayMillis = 999_999_999L
+        withTestClockContext {
+            val virtualStartTimeMillis = currentTime
+            val realStartTimeNanos = System.nanoTime()
+            delay(delayMillis)
+            val realDurationMillis = (System.nanoTime() - realStartTimeNanos) / 1_000_000
+            val virtualDurationMillis = currentTime - virtualStartTimeMillis
+
+            assertThat(virtualDurationMillis).isEqualTo(delayMillis)
+            // This could theoretically fail if the test host is running _really_ slow, but 500ms
+            // should be more than enough.
+            assertThat(realDurationMillis).isLessThan(500)
+        }
+    }
+
+    @Test
+    fun delaysAreSkipped_standardDispatcher() = runTest(StandardTestDispatcher()) {
+        val delayMillis = 999_999_999L
+        withTestClockContext {
+            val virtualStartTimeMillis = currentTime
+            val realStartTimeNanos = System.nanoTime()
+            delay(delayMillis)
+            val realDurationMillis = (System.nanoTime() - realStartTimeNanos) / 1_000_000
+            val virtualDurationMillis = currentTime - virtualStartTimeMillis
+
+            assertThat(virtualDurationMillis).isEqualTo(delayMillis)
+            // This could theoretically fail if the test host is running _really_ slow, but 500ms
+            // should be more than enough.
+            assertThat(realDurationMillis).isLessThan(500)
+        }
+    }
+
+    @Test
+    fun testMonotonicFrameClockRunsFrame_unconfinedDispatcher() =
+        runTest(UnconfinedTestDispatcher()) {
+            withTestClockContext {
+                val startTime = currentTime
+                val expectedFrameTime = startTime + FrameDelayNanos / 1_000_000
+                val counter = TestCounter()
+                launch {
+                    counter.expect(1, "in coroutine 1")
+                    withFrameNanos {
+                        counter.expect(4, "in frame callback 1")
+                        assertEquals("frame time 1", expectedFrameTime, currentTime)
+                    }
+                    counter.expect(6, "after resuming frame callback 1")
+                }
+                launch {
+                    counter.expect(2, "in coroutine 2")
+                    withFrameNanos {
+                        counter.expect(5, "in frame callback 2")
+                        assertEquals("frame time 2", expectedFrameTime, currentTime)
+                    }
+                    counter.expect(7, "after resuming frame callback 2")
+                }
+                counter.expect(3)
+                advanceUntilIdleWorkaround()
+                counter.expect(8, "final result")
+            }
+        }
+
+    @Test
+    fun testMonotonicFrameClockRunsFrame_standardDispatcher() =
+        runTest(StandardTestDispatcher()) {
+            withTestClockContext {
+                val startTime = currentTime
+                val expectedFrameTime = startTime + FrameDelayNanos / 1_000_000
+                val counter = TestCounter()
+                launch {
+                    counter.expect(2, "in coroutine 1")
+                    withFrameNanos {
+                        counter.expect(4, "in frame callback 1")
+                        assertEquals("frame time 1", expectedFrameTime, currentTime)
+                    }
+                    counter.expect(6, "after resuming frame callback 1")
+                }
+                launch {
+                    counter.expect(3, "in coroutine 2")
+                    withFrameNanos {
+                        counter.expect(5, "in frame callback 2")
+                        assertEquals("frame time 2", expectedFrameTime, currentTime)
+                    }
+                    counter.expect(7, "after resuming frame callback 2")
+                }
+                counter.expect(1)
+                advanceUntilIdleWorkaround()
+                counter.expect(8, "final result")
+            }
+        }
+
+    @Test
+    fun testMonotonicFrameClockDispatcherDefersResumesInsideFrame_unconfinedDispatcher() =
+        runTest(UnconfinedTestDispatcher()) {
+            withTestClockContext {
+                val counter = TestCounter()
+                var continuation: Continuation<Unit>? = null
+
+                launch {
+                    counter.expect(1, "in external coroutine")
+                    suspendCancellableCoroutine { continuation = it }
+                    counter.expect(9, "after resuming external coroutine")
+                }
+
+                launch {
+                    counter.expect(2)
+                    withFrameNanos {
+                        counter.expect(5, "in frame callback 1")
+                        // Coroutines launched inside withFrameNanos shouldn't be dispatched until
+                        // after all frame callbacks are complete.
+                        launch {
+                            counter.expect(6, "in \"effect\" coroutine")
+                        }
+                        counter.expect(7, "after launching \"effect\" coroutine")
+                    }
+                    counter.expect(11, "after resuming frame callback 1")
+                }
+
+                launch {
+                    counter.expect(3)
+                    withFrameNanos {
+                        counter.expect(8, "in frame callback 2")
+                        // Coroutines that were already suspended and are resumed inside
+                        // withFrameNanos shouldn't be dispatched until after all frame callbacks
+                        // are complete either.
+                        continuation!!.resume(Unit)
+                        counter.expect(10, "after resuming external continuation")
+                    }
+                    counter.expect(12, "after resuming frame callback 2")
+                }
+
+                counter.expect(4)
+                advanceUntilIdleWorkaround()
+                counter.expect(13, "final result")
+            }
+        }
+
+    @Test
+    fun testMonotonicFrameClockDispatcherDefersResumesInsideFrame_standardDispatcher() =
+        runTest(StandardTestDispatcher()) {
+            withTestClockContext {
+                val counter = TestCounter()
+                var continuation: Continuation<Unit>? = null
+
+                launch {
+                    counter.expect(2, "in external coroutine")
+                    suspendCancellableCoroutine { continuation = it }
+                    counter.expect(10, "after resuming external coroutine")
+                }
+
+                launch {
+                    counter.expect(3)
+                    withFrameNanos {
+                        counter.expect(5, "in frame callback 1")
+                        // Coroutines launched inside withFrameNanos shouldn't be dispatched until
+                        // after all frame callbacks are complete.
+                        launch {
+                            counter.expect(9, "in \"effect\" coroutine")
+                        }
+                        counter.expect(6, "after launching \"effect\" coroutine")
+                    }
+                    counter.expect(11, "after resuming frame callback 1")
+                }
+
+                launch {
+                    counter.expect(4)
+                    withFrameNanos {
+                        counter.expect(7, "in frame callback 2")
+                        // Coroutines that were already suspended and are resumed inside
+                        // withFrameNanos shouldn't be dispatched until after all frame callbacks
+                        // are complete either.
+                        continuation!!.resume(Unit)
+                        counter.expect(8, "after resuming external continuation")
+                    }
+                    counter.expect(12, "after resuming frame callback 2")
+                }
+
+                counter.expect(1)
+                advanceUntilIdleWorkaround()
+                counter.expect(13, "final result")
+            }
+        }
+
+    @Test
+    fun withFrameNanosThrows_unconfinedDispatcher() = runTest(UnconfinedTestDispatcher()) {
+        val message = "the frame threw an error"
+        var error: RuntimeException? = null
+        var firstCoroutineContinued = false
+        var secondFrameCallbackRan = false
+        var secondCoroutineContinued = false
+
+        withTestClockContext {
+            launch {
+                try {
+                    withFrameNanos {
+                        throw RuntimeException(message)
+                    }
+                } catch (e: RuntimeException) {
+                    error = e
+                }
+                firstCoroutineContinued = true
+            }
+
             launch {
                 withFrameNanos {
-                    counter.expect(2, "in frame callback 1")
-                    assertEquals("frame time 1", expectedFrameTime, currentTime)
+                    // If one frame callback throws, other frame callbacks should still run.
+                    secondFrameCallbackRan = true
                 }
-                counter.expect(4, "after resuming frame callback 1")
+                secondCoroutineContinued = true
             }
+
+            advanceUntilIdleWorkaround()
+            assertThat(error).hasMessageThat().isEqualTo(message)
+            assertThat(firstCoroutineContinued).isTrue()
+            assertThat(secondFrameCallbackRan).isTrue()
+            assertThat(secondCoroutineContinued).isTrue()
+        }
+    }
+
+    @Test
+    fun withFrameNanosThrows_standardDispatcher() = runTest(StandardTestDispatcher()) {
+        val message = "the frame threw an error"
+        var error: RuntimeException? = null
+        var firstCoroutineContinued = false
+        var secondFrameCallbackRan = false
+        var secondCoroutineContinued = false
+
+        withTestClockContext {
+            launch {
+                try {
+                    withFrameNanos {
+                        throw RuntimeException(message)
+                    }
+                } catch (e: RuntimeException) {
+                    error = e
+                }
+                firstCoroutineContinued = true
+            }
+
             launch {
                 withFrameNanos {
-                    counter.expect(3, "in frame callback 2")
-                    assertEquals("frame time 2", expectedFrameTime, currentTime)
+                    // If one frame callback throws, other frame callbacks should still run.
+                    secondFrameCallbackRan = true
                 }
-                counter.expect(5, "after resuming frame callback 2")
+                secondCoroutineContinued = true
             }
-            counter.expect(1)
+
+            advanceUntilIdleWorkaround()
+            assertThat(error).hasMessageThat().isEqualTo(message)
+            assertThat(firstCoroutineContinued).isTrue()
+            assertThat(secondFrameCallbackRan).isTrue()
+            assertThat(secondCoroutineContinued).isTrue()
+        }
+    }
+
+    private suspend fun TestScope.withTestClockContext(block: suspend CoroutineScope.() -> Unit) {
+        withContext(TestMonotonicFrameClock(this, FrameDelayNanos), block)
+    }
+
+    /** Workaround for https://github.com/Kotlin/kotlinx.coroutines/issues/3493. */
+    private suspend fun TestScope.advanceUntilIdleWorkaround() {
+        // Yielding is required to dispatch launches.
+        // Advancing is required to execute withFrameNanos callbacks.
+        // There need to be enough repetitions to execute all of each of those in the tests.
+        repeat(10) {
+            yield()
             advanceUntilIdle()
-            counter.expect(6, "final result")
         }
     }
-}
 
-private class TestCounter {
-    private var count = 0
-
-    fun expect(checkpoint: Int, message: String = "(no message)") {
-        val expected = count + 1
-        if (checkpoint != expected) {
-            fail("out of order event $checkpoint, expected $expected, $message")
-        }
-        count = expected
+    private companion object {
+        const val FrameDelayNanos = 16_000_000L
     }
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt
new file mode 100644
index 0000000..9fac117
--- /dev/null
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.util
+
+import org.junit.Assert.fail
+
+/**
+ * Asserts that [expect] calls occur in a specific order. Useful for coroutine dispatching tests.
+ */
+internal class TestCounter {
+    private var count = 0
+
+    fun expect(checkpoint: Int, message: String = "(no message)") {
+        val expected = count + 1
+        if (checkpoint != expected) {
+            fail("out of order event $checkpoint, expected $expected, $message")
+        }
+        count = expected
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/api/current.txt b/compose/ui/ui-tooling/api/current.txt
index 73ffd26..8b2d4ee 100644
--- a/compose/ui/ui-tooling/api/current.txt
+++ b/compose/ui/ui-tooling/api/current.txt
@@ -8,6 +8,9 @@
     method @Deprecated @androidx.compose.runtime.Composable public static void InInspectionModeOnly(kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class PreviewLogger_androidKt {
+  }
+
   public final class PreviewUtilsKt {
   }
 
@@ -21,6 +24,13 @@
   public final class AnimationSearchKt {
   }
 
+  public final class ToolingState<T> implements androidx.compose.runtime.State<T> {
+    ctor public ToolingState(T? default);
+    method public T! getValue();
+    method public void setValue(T!);
+    property public T! value;
+  }
+
   public final class TransitionComposeAnimationKt {
   }
 
diff --git a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
index a778a63..cfd82b6 100644
--- a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
@@ -13,6 +13,9 @@
     method @Deprecated @androidx.compose.runtime.Composable public static void InInspectionModeOnly(kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class PreviewLogger_androidKt {
+  }
+
   public final class PreviewUtilsKt {
   }
 
@@ -26,6 +29,13 @@
   public final class AnimationSearchKt {
   }
 
+  public final class ToolingState<T> implements androidx.compose.runtime.State<T> {
+    ctor public ToolingState(T? default);
+    method public T! getValue();
+    method public void setValue(T!);
+    property public T! value;
+  }
+
   public final class TransitionComposeAnimationKt {
   }
 
diff --git a/compose/ui/ui-tooling/api/restricted_current.txt b/compose/ui/ui-tooling/api/restricted_current.txt
index 73ffd26..8b2d4ee 100644
--- a/compose/ui/ui-tooling/api/restricted_current.txt
+++ b/compose/ui/ui-tooling/api/restricted_current.txt
@@ -8,6 +8,9 @@
     method @Deprecated @androidx.compose.runtime.Composable public static void InInspectionModeOnly(kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
+  public final class PreviewLogger_androidKt {
+  }
+
   public final class PreviewUtilsKt {
   }
 
@@ -21,6 +24,13 @@
   public final class AnimationSearchKt {
   }
 
+  public final class ToolingState<T> implements androidx.compose.runtime.State<T> {
+    ctor public ToolingState(T? default);
+    method public T! getValue();
+    method public void setValue(T!);
+    property public T! value;
+  }
+
   public final class TransitionComposeAnimationKt {
   }
 
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt
new file mode 100644
index 0000000..e1233ef
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.currentComposer
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import java.lang.reflect.InvocationTargetException
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+
+@Suppress("DEPRECATION")
+@OptIn(ExperimentalComposeUiApi::class)
+class ComposeInvokerTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun workingComposable() {
+        rule.setContent {
+            ComposableInvoker.invokeComposable(
+                "androidx.compose.ui.tooling.MyTestComposables",
+                "MyWorkingComposable",
+                currentComposer
+            )
+        }
+    }
+
+    @Test
+    fun composableClassNotFound() {
+        try {
+            rule.setContent {
+                ComposableInvoker.invokeComposable(
+                    "androidx.compose.ui.tooling.ClassDoesntExist",
+                    "MyWorkingComposable",
+                    currentComposer
+                )
+            }
+            fail("ClassNotFoundException expected to be thrown")
+        } catch (e: ClassNotFoundException) {
+            // ClassNotFoundException expected to be thrown when Composable class is not found.
+        }
+    }
+
+    @Test
+    fun composableMethodNotFound() {
+        try {
+            rule.setContent {
+                ComposableInvoker.invokeComposable(
+                    "androidx.compose.ui.tooling.MyTestComposables",
+                    "MethodDoesntExist",
+                    currentComposer
+                )
+            }
+            fail("NoSuchMethodException expected to be thrown")
+        } catch (e: NoSuchMethodException) {
+            // NoSuchMethodException expected to be thrown when Composable method is not found.
+        }
+    }
+
+    @Test
+    fun composableMethodThrowsException() {
+        try {
+            rule.setContent {
+                ComposableInvoker.invokeComposable(
+                    "androidx.compose.ui.tooling.MyTestComposables",
+                    "MyThrowExceptionComposable",
+                    currentComposer
+                )
+            }
+            fail("InvocationTargetException expected to be thrown")
+        } catch (e: InvocationTargetException) {
+            // InvocationTargetException expected to be thrown when Composable throws an exception.
+        }
+    }
+}
+
+class MyTestComposables {
+
+    @Composable
+    fun MyWorkingComposable() {}
+
+    @Composable
+    fun MyThrowExceptionComposable() {
+        throw Exception("An Exception")
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
index c73edb2..adea0bf 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
@@ -20,6 +20,7 @@
 import android.os.Build
 import android.os.Bundle
 import androidx.compose.animation.core.InternalAnimationApi
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation
 import androidx.compose.ui.tooling.animation.PreviewAnimationClock
 import androidx.compose.ui.tooling.animation.UnsupportedComposeAnimation
 import androidx.compose.ui.tooling.data.UiToolingDataApi
@@ -173,11 +174,29 @@
     fun animateXAsStateIsSubscribed() {
         checkAnimationsAreSubscribed(
             "AnimateAsStatePreview",
-            listOf("DpAnimation", "IntAnimation")
+            animateXAsState = listOf("DpAnimation", "IntAnimation")
         )
     }
 
     @Test
+    fun animateXAsStateIsNotSubscribed() {
+        AnimateXAsStateComposeAnimation.testOverrideAvailability(false)
+        checkAnimationsAreSubscribed(
+            "AllAnimations",
+            unsupported = listOf(
+                "AnimatedContent",
+                "animateContentSize",
+                "TargetBasedAnimation",
+                "DecayAnimation",
+                "InfiniteTransition"
+            ),
+            transitions = listOf("checkBoxAnim", "Crossfade"),
+            animateXAsState = emptyList()
+        )
+        AnimateXAsStateComposeAnimation.testOverrideAvailability(true)
+    }
+
+    @Test
     fun animateContentSizeIsSubscribed() {
         checkAnimationsAreSubscribed("AnimateContentSizePreview", listOf("animateContentSize"))
     }
@@ -208,7 +227,8 @@
         checkAnimationsAreSubscribed(
             "AllAnimations",
             emptyList(),
-            listOf("checkBoxAnim", "Crossfade")
+            listOf("checkBoxAnim", "Crossfade"),
+            animateXAsState = listOf("DpAnimation", "IntAnimation")
         )
         UnsupportedComposeAnimation.testOverrideAvailability(true)
     }
@@ -224,8 +244,9 @@
 
     private fun checkAnimationsAreSubscribed(
         preview: String,
-        unsupported: List<String>,
-        transitions: List<String> = emptyList()
+        unsupported: List<String> = emptyList(),
+        transitions: List<String> = emptyList(),
+        animateXAsState: List<String> = emptyList()
     ) {
         val clock = PreviewAnimationClock()
 
@@ -251,6 +272,8 @@
         activityTestRule.runOnUiThread {
             assertEquals(unsupported, clock.trackedUnsupportedAnimations.map { it.label })
             assertEquals(transitions, clock.transitionClocks.values.map { it.animation.label })
+            assertEquals(animateXAsState,
+                clock.animateXAsStateClocks.values.map { it.animation.label })
             assertEquals(0, clock.animatedVisibilityClocks.size)
         }
     }
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
index 1c3d41b..71c9d1d 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
@@ -220,6 +220,36 @@
 
 @Preview
 @Composable
+fun AnimateAsStateWithLabelsPreview() {
+    var showMenu by remember { mutableStateOf(true) }
+    var message by remember { mutableStateOf("Hello") }
+
+    val size: Dp by animateDpAsState(
+        targetValue = if (showMenu) 0.dp else 10.dp,
+        animationSpec = spring(Spring.DampingRatioHighBouncy, Spring.StiffnessHigh),
+        label = "CustomDpLabel"
+    )
+    val offset by animateIntAsState(
+        targetValue = if (showMenu) 2 else 1,
+        label = "CustomIntLabel"
+    )
+
+    Box(
+        Modifier
+            .padding(size)
+            .offset(offset.dp)
+            .pointerInput(Unit) {
+                detectTapGestures {
+                    showMenu = !showMenu
+                    message += "!"
+                }
+            }) {
+        Text(text = message)
+    }
+}
+
+@Preview
+@Composable
 fun CrossFadePreview() {
     var currentPage by remember { mutableStateOf("A") }
     Row {
@@ -243,6 +273,29 @@
 
 @Preview
 @Composable
+fun CrossFadeWithLabelPreview() {
+    var currentPage by remember { mutableStateOf("A") }
+    Row {
+        Button(>
+            currentPage = when (currentPage) {
+                "A" -> "B"
+                "B" -> "A"
+                else -> "A"
+            }
+        }) {
+            Text("Switch Page")
+        }
+        Crossfade(targetState = currentPage, label = "CrossfadeWithLabel") { screen ->
+            when (screen) {
+                "A" -> Text("Page A")
+                "B" -> Text("Page B")
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
 fun AnimateContentSizePreview() {
     var message by remember { mutableStateOf("Hello") }
     Row {
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt
new file mode 100644
index 0000000..459a6aa
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntAsState
+import androidx.compose.animation.core.animateIntOffsetAsState
+import androidx.compose.animation.core.animateIntSizeAsState
+import androidx.compose.animation.core.animateOffsetAsState
+import androidx.compose.animation.core.animateRectAsState
+import androidx.compose.animation.core.animateSizeAsState
+import androidx.compose.animation.core.animateValueAsState
+import androidx.compose.runtime.State
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.Utils.searchForAnimation
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AnimateXAsStateComposeAnimationTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun dpAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var dpState: State<Dp>? = null
+        rule.searchForAnimation(search) {
+            dpState = animateDpAsState(targetValue = 10.dp)
+        }
+        val composeAnimation = checkDefaultState(search, "DpAnimation", 10.dp)
+        rule.runOnUiThread {
+            composeAnimation.setState(20.dp)
+        }
+        rule.waitForIdle()
+        assertEquals(20.dp, dpState!!.value)
+    }
+
+    @Test
+    fun floatAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Float>? = null
+        rule.searchForAnimation(search) {
+            state = animateFloatAsState(targetValue = 10f)
+        }
+        val composeAnimation = checkDefaultState(search, "FloatAnimation", 10f)
+        rule.runOnUiThread {
+            composeAnimation.setState(20f)
+        }
+        rule.waitForIdle()
+        assertEquals(20f, state!!.value)
+    }
+
+    @Test
+    fun intAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Int>? = null
+        rule.searchForAnimation(search) {
+            state = animateIntAsState(targetValue = 10)
+        }
+        val composeAnimation = checkDefaultState(search, "IntAnimation", 10)
+        rule.runOnUiThread {
+            composeAnimation.setState(20)
+        }
+        rule.waitForIdle()
+        assertEquals(20, state!!.value)
+    }
+
+    @Test
+    fun intSizeAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<IntSize>? = null
+        rule.searchForAnimation(search) {
+            state = animateIntSizeAsState(targetValue = IntSize(10, 20))
+        }
+        checkDefaultState(search, "IntSizeAnimation", IntSize(10, 20))
+        val composeAnimation = search.animations.first().parse()!!
+        rule.runOnUiThread {
+            composeAnimation.setState(IntSize(30, 40))
+        }
+        rule.waitForIdle()
+        assertEquals(IntSize(30, 40), state!!.value)
+    }
+
+    @Test
+    fun intOffsetAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<IntOffset>? = null
+        rule.searchForAnimation(search) {
+            state = animateIntOffsetAsState(targetValue = IntOffset(10, 20))
+        }
+        val composeAnimation = checkDefaultState(search, "IntOffsetAnimation", IntOffset(10, 20))
+        rule.runOnUiThread {
+            composeAnimation.setState(IntOffset(30, 40))
+        }
+        rule.waitForIdle()
+        assertEquals(IntOffset(30, 40), state!!.value)
+    }
+
+    @Test
+    fun offsetAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Offset>? = null
+        rule.searchForAnimation(search) {
+            state = animateOffsetAsState(targetValue = Offset(10f, 20f))
+        }
+        val composeAnimation = checkDefaultState(search, "OffsetAnimation", Offset(10f, 20f))
+        rule.runOnUiThread {
+            composeAnimation.setState(Offset(30f, 40f))
+        }
+        rule.waitForIdle()
+        assertEquals(Offset(30f, 40f), state!!.value)
+    }
+
+    @Test
+    fun sizeAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Size>? = null
+        rule.searchForAnimation(search) {
+            state = animateSizeAsState(targetValue = Size(10f, 20f))
+        }
+        val composeAnimation = checkDefaultState(search, "SizeAnimation", Size(10f, 20f))
+        composeAnimation.setState(Size(30f, 40f))
+        rule.waitForIdle()
+        assertEquals(Size(30f, 40f), state!!.value)
+    }
+
+    @Test
+    fun rectAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Rect>? = null
+        rule.searchForAnimation(search) {
+            state = animateRectAsState(targetValue = Rect(10f, 20f, 30f, 40f))
+        }
+        val composeAnimation = checkDefaultState(search, "RectAnimation", Rect(10f, 20f, 30f, 40f))
+        composeAnimation.setState(Rect(50f, 60f, 70f, 80f))
+        rule.waitForIdle()
+        assertEquals(Rect(50f, 60f, 70f, 80f), state!!.value)
+    }
+
+    @Test
+    fun colorAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Color>? = null
+        rule.searchForAnimation(search) {
+            state = animateColorAsState(targetValue = Color(0.1f, 0.2f, 0.3f, 0.4f))
+        }
+        val composeAnimation =
+            checkDefaultState(search, "ColorAnimation", Color(0.1f, 0.2f, 0.3f, 0.4f))
+        composeAnimation.setState(Color(0.3f, 0.4f, 0.5f, 0.6f))
+        rule.waitForIdle()
+        assertEquals(Color(0.3f, 0.4f, 0.5f, 0.6f), state!!.value)
+    }
+
+    @Test
+    fun customFloatAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch {}
+        var state: State<Float>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(10f, Float.VectorConverter)
+        }
+        val composeAnimation = checkDefaultState(search, "ValueAnimation", 10f)
+        composeAnimation.setState(30f)
+        rule.waitForIdle()
+        assertEquals(30f, state!!.value)
+    }
+
+    @Test
+    fun nullableFloatAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Float?>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(1f, Utils.nullableFloatConverter)
+        }
+        val composeAnimation = checkDefaultState(search, "ValueAnimation", 1f)
+        composeAnimation.setState(30f)
+        rule.waitForIdle()
+        assertEquals(30f, state!!.value)
+    }
+
+    @Test
+    fun nullableFloatAnimationWithNullState() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        rule.searchForAnimation(search) {
+            animateValueAsState(null, Utils.nullableFloatConverter)
+        }
+        assertEquals(1, search.animations.size)
+        val composeAnimation = search.animations.first().parse()
+        assertNull(composeAnimation)
+    }
+
+    @Test
+    fun stringAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<String>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState("11.0", Utils.stringConverter)
+        }
+        val composeAnimation = checkDefaultState(search, "ValueAnimation", "11.0")
+        composeAnimation.setState("56.0")
+        rule.waitForIdle()
+        assertEquals("56.0", state!!.value)
+    }
+
+    @Test
+    fun enumAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Utils.EnumState>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(Utils.EnumState.One, Utils.enumConverter)
+        }
+        val composeAnimation = checkDefaultState(search, "ValueAnimation", Utils.EnumState.One, 3)
+        composeAnimation.setState(Utils.EnumState.Three)
+        rule.waitForIdle()
+        assertEquals(Utils.EnumState.Three, state!!.value)
+    }
+
+    @Test
+    fun nullableEnumAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Utils.EnumState?>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(Utils.EnumState.One, Utils.nullableEnumConverter)
+        }
+        val composeAnimation = checkDefaultState(search, "ValueAnimation", Utils.EnumState.One, 3)
+        composeAnimation.setState(Utils.EnumState.Three)
+        rule.waitForIdle()
+        assertEquals(Utils.EnumState.Three, state!!.value)
+    }
+
+    private fun checkDefaultState(
+        search: AnimationSearch.AnimateXAsStateSearch,
+        label: String,
+        defaultValue: Any,
+        numberOfStates: Int = 1
+    ): AnimateXAsStateComposeAnimation<*, *> {
+        assertEquals(1, search.animations.size)
+        val composeAnimation = search.animations.first().parse()!!
+        composeAnimation.animationObject.let {
+            assertNotNull(it)
+            assertEquals(defaultValue, it.value)
+        }
+        composeAnimation.animationSpec.let {
+            assertNotNull(it)
+            assertTrue(it is SpringSpec<*>)
+        }
+        assertEquals(label, composeAnimation.label)
+        assertEquals(numberOfStates, composeAnimation.states.size)
+        assertEquals(defaultValue, composeAnimation.states.first())
+        assertNotNull(composeAnimation.toolingState)
+        return composeAnimation
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
index bffb7df..2376088 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
@@ -16,13 +16,16 @@
 
 package androidx.compose.ui.tooling.animation
 
+import androidx.compose.animation.core.SpringSpec
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.tooling.AnimateAsStatePreview
+import androidx.compose.ui.tooling.AnimateAsStateWithLabelsPreview
 import androidx.compose.ui.tooling.AnimateContentSizePreview
 import androidx.compose.ui.tooling.AnimatedContentExtensionPreview
 import androidx.compose.ui.tooling.AnimatedContentPreview
 import androidx.compose.ui.tooling.AnimatedVisibilityPreview
 import androidx.compose.ui.tooling.CrossFadePreview
+import androidx.compose.ui.tooling.CrossFadeWithLabelPreview
 import androidx.compose.ui.tooling.DecayAnimationPreview
 import androidx.compose.ui.tooling.InfiniteTransitionPreview
 import androidx.compose.ui.tooling.TargetBasedAnimationPreview
@@ -32,6 +35,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import org.junit.Assert
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
@@ -82,10 +86,46 @@
         val search = AnimationSearch.AnimateXAsStateSearch { callbacks++ }
         rule.searchForAnimation(search) { AnimateAsStatePreview() }
         assertEquals(2, search.animations.size)
+        search.animations.first().let {
+            Assert.assertTrue(it.animationSpec is SpringSpec)
+            Assert.assertNotNull(it.toolingState)
+            Assert.assertNotNull(it.animatable)
+            assertEquals("IntAnimation", it.animatable.label)
+        }
+        search.animations.last().let {
+            Assert.assertTrue(it.animationSpec is SpringSpec)
+            Assert.assertNotNull(it.toolingState)
+            Assert.assertNotNull(it.animatable)
+            assertEquals("DpAnimation", it.animatable.label)
+        }
         search.track()
         assertEquals(2, callbacks)
-        assertEquals(0.dp, search.animations.last().targetValue)
-        assertEquals(2, search.animations.first().targetValue)
+        assertEquals(0.dp, search.animations.last().animatable.targetValue)
+        assertEquals(2, search.animations.first().animatable.targetValue)
+    }
+
+    @Test
+    fun animatedXAsStateWithLabelsSearchIsFound() {
+        var callbacks = 0
+        val search = AnimationSearch.AnimateXAsStateSearch { callbacks++ }
+        rule.searchForAnimation(search) { AnimateAsStateWithLabelsPreview() }
+        assertEquals(2, search.animations.size)
+        search.animations.first().let {
+            Assert.assertTrue(it.animationSpec is SpringSpec)
+            Assert.assertNotNull(it.toolingState)
+            Assert.assertNotNull(it.animatable)
+            assertEquals("CustomIntLabel", it.animatable.label)
+        }
+        search.animations.last().let {
+            Assert.assertTrue(it.animationSpec is SpringSpec)
+            Assert.assertNotNull(it.toolingState)
+            Assert.assertNotNull(it.animatable)
+            assertEquals("CustomDpLabel", it.animatable.label)
+        }
+        search.track()
+        assertEquals(2, callbacks)
+        assertEquals(0.dp, search.animations.last().animatable.targetValue)
+        assertEquals(2, search.animations.first().animatable.targetValue)
     }
 
     @Test
@@ -137,6 +177,19 @@
         search.track()
         assertEquals(1, callbacks)
         assertEquals("A", search.animations.first().targetState)
+        assertEquals("Crossfade", search.animations.first().label)
+    }
+
+    @Test
+    fun crossFadeWithLabelIsFoundAsTransition() {
+        var callbacks = 0
+        val search = AnimationSearch.TransitionSearch { callbacks++ }
+        rule.searchForAnimation(search) { CrossFadeWithLabelPreview() }
+        assertEquals(1, search.animations.size)
+        search.track()
+        assertEquals(1, callbacks)
+        assertEquals("A", search.animations.first().targetState)
+        assertEquals("CrossfadeWithLabel", search.animations.first().label)
     }
 
     @Test
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
index 52991f8..6015f60 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
@@ -18,7 +18,10 @@
 
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.AnimationVector1D
 import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.TwoWayConverter
 import androidx.compose.animation.core.updateTransition
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -35,6 +38,40 @@
 
 object Utils {
 
+    enum class EnumState { One, Two, Three }
+
+    val nullableFloatConverter = TwoWayConverter<Float?, AnimationVector1D>({
+        AnimationVector1D(it ?: 0f)
+    }, { it.value })
+
+    val stringConverter = TwoWayConverter<String, AnimationVector1D>(
+        { AnimationVector1D(it.toFloat()) }, { it.value.toString() })
+
+    val enumConverter = object : TwoWayConverter<EnumState, AnimationVector> {
+        override val convertFromVector: (AnimationVector) -> EnumState
+            get() = { EnumState.One }
+
+        override val convertToVector: (EnumState) -> AnimationVector
+            get() = { AnimationVector(1f) }
+    }
+
+    val nullableEnumConverter = object :
+        TwoWayConverter<EnumState?, AnimationVector> {
+        override val convertFromVector: (AnimationVector) -> EnumState?
+            get() = { EnumState.One }
+
+        override val convertToVector: (EnumState?) -> AnimationVector
+            get() = { AnimationVector(1f) }
+    }
+
+    val booleanConverter = object : TwoWayConverter<Boolean, AnimationVector1D> {
+        override val convertFromVector: (AnimationVector1D) -> Boolean
+            get() = { it.value == 1f }
+
+        override val convertToVector: (Boolean) -> AnimationVector1D
+            get() = { AnimationVector(if (it) 1f else 0f) }
+    }
+
     @OptIn(UiToolingDataApi::class)
     internal fun ComposeContentTestRule.searchForAnimation(
         search: AnimationSearch.Search<*>,
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
new file mode 100644
index 0000000..e65ad4f
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
@@ -0,0 +1,674 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation.clock
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntAsState
+import androidx.compose.animation.core.animateIntOffsetAsState
+import androidx.compose.animation.core.animateIntSizeAsState
+import androidx.compose.animation.core.animateOffsetAsState
+import androidx.compose.animation.core.animateRectAsState
+import androidx.compose.animation.core.animateSizeAsState
+import androidx.compose.animation.core.animateValueAsState
+import androidx.compose.animation.tooling.ComposeAnimatedProperty
+import androidx.compose.animation.tooling.ComposeAnimation
+import androidx.compose.runtime.State
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.AnimationSearch
+import androidx.compose.ui.tooling.animation.Utils
+import androidx.compose.ui.tooling.animation.Utils.searchForAnimation
+import androidx.compose.ui.tooling.animation.states.ComposeAnimationState
+import androidx.compose.ui.tooling.animation.states.TargetState
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AnimateXAsStateClockTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun dpAnimationClock() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Dp>? = null
+        rule.searchForAnimation(search) {
+            state = animateDpAsState(
+                targetValue = 10.dp, animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "DpAnimation",
+            initialValue = 10.dp,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(1.dp, 2.dp) }
+        checkUpdatedState(clock, label = "DpAnimation",
+            newInitialValue = 1.dp, newTargetValue = 2.dp,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(3.dp), listOf(4.dp)) }
+        checkUpdatedState(clock, label = "DpAnimation",
+            newInitialValue = 3.dp, newTargetValue = 4.dp,
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(111.dp, 111)
+            clock.setStateParameters(111.dp, null)
+            clock.setStateParameters(listOf(111.dp), listOf(111L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(111.dp), emptyList<Dp>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "DpAnimation",
+            newInitialValue = 3.dp, newTargetValue = 4.dp,
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun floatAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Float>? = null
+        rule.searchForAnimation(search) {
+            state = animateFloatAsState(
+                targetValue = 10f, animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "FloatAnimation",
+            initialValue = 10f,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(1f, 2f) }
+        checkUpdatedState(clock, label = "FloatAnimation",
+            newInitialValue = 1f, newTargetValue = 2f,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(3f), listOf(4f)) }
+        checkUpdatedState(clock, label = "FloatAnimation",
+            newInitialValue = 3f, newTargetValue = 4f,
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(111f, 111)
+            clock.setStateParameters(111f, null)
+            clock.setStateParameters(listOf(111f), listOf(111L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(111f), emptyList<Dp>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "FloatAnimation",
+            newInitialValue = 3f, newTargetValue = 4f,
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun intSizeAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<IntSize>? = null
+        rule.searchForAnimation(search) {
+            state = animateIntSizeAsState(
+                targetValue = IntSize(10, 20),
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "IntSizeAnimation",
+            initialValue = IntSize(10, 20),
+            composeState = { state!!.value })
+        rule.runOnUiThread {
+            clock.setStateParameters(IntSize(3, 4), IntSize(4, 5))
+        }
+        checkUpdatedState(clock, label = "IntSizeAnimation",
+            newInitialValue = IntSize(3, 4), newTargetValue = IntSize(4, 5),
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(5, 6), listOf(7, 8)) }
+        checkUpdatedState(clock, label = "IntSizeAnimation",
+            newInitialValue = IntSize(5, 6), newTargetValue = IntSize(7, 8),
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(IntSize(111, 111), 111)
+            clock.setStateParameters(IntSize(111, 111), null)
+            clock.setStateParameters(10, 10)
+            clock.setStateParameters(listOf(IntSize(111, 111)), listOf(111L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(IntSize(111, 11)), emptyList<IntOffset>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "IntSizeAnimation",
+            newInitialValue = IntSize(5, 6), newTargetValue = IntSize(7, 8),
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun intAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Int>? = null
+        rule.searchForAnimation(search) {
+            state = animateIntAsState(
+                targetValue = 10, animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "IntAnimation",
+            initialValue = 10,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(1, 2) }
+        checkUpdatedState(clock, label = "IntAnimation",
+            newInitialValue = 1, newTargetValue = 2,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(3), listOf(4)) }
+        checkUpdatedState(clock, label = "IntAnimation",
+            newInitialValue = 3, newTargetValue = 4,
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(111, 111f)
+            clock.setStateParameters(111, null)
+            clock.setStateParameters(111f, 111f)
+            clock.setStateParameters(listOf(111), listOf(111L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(111), emptyList<Int>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "IntAnimation",
+            newInitialValue = 3, newTargetValue = 4,
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun intOffsetAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<IntOffset>? = null
+        rule.searchForAnimation(search) {
+            state = animateIntOffsetAsState(
+                targetValue = IntOffset(10, 20),
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "IntOffsetAnimation",
+            initialValue = IntOffset(10, 20),
+            composeState = { state!!.value })
+        rule.runOnUiThread {
+            clock.setStateParameters(IntOffset(1, 2), IntOffset(3, 4))
+        }
+        checkUpdatedState(clock, label = "IntOffsetAnimation",
+            newInitialValue = IntOffset(1, 2), newTargetValue = IntOffset(3, 4),
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(3, 4), listOf(4, 5)) }
+        checkUpdatedState(clock, label = "IntOffsetAnimation",
+            newInitialValue = IntOffset(3, 4), newTargetValue = IntOffset(4, 5),
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(IntOffset(111, 111), 111)
+            clock.setStateParameters(IntOffset(111, 111), null)
+            clock.setStateParameters(10, 10)
+            clock.setStateParameters(listOf(IntOffset(111, 111)), listOf(111L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(IntOffset(111, 11)), emptyList<IntOffset>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "IntOffsetAnimation",
+            newInitialValue = IntOffset(3, 4), newTargetValue = IntOffset(4, 5),
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun offsetAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Offset>? = null
+        rule.searchForAnimation(search) {
+            state = animateOffsetAsState(
+                targetValue = Offset(10f, 20f),
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "OffsetAnimation",
+            initialValue = Offset(10f, 20f),
+            composeState = { state!!.value })
+        rule.runOnUiThread {
+            clock.setStateParameters(Offset(1f, 2f), Offset(3f, 4f))
+        }
+        checkUpdatedState(clock, label = "OffsetAnimation",
+            newInitialValue = Offset(1f, 2f), newTargetValue = Offset(3f, 4f),
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(3f, 4f), listOf(4f, 5f)) }
+        checkUpdatedState(clock, label = "OffsetAnimation",
+            newInitialValue = Offset(3f, 4f), newTargetValue = Offset(4f, 5f),
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(Offset(111f, 111f), 111)
+            clock.setStateParameters(Offset(111f, 111f), null)
+            clock.setStateParameters(10, 10)
+            clock.setStateParameters(listOf(Offset(111f, 111f)), listOf(111L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(Offset(111f, 111f)), emptyList<Offset>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "OffsetAnimation",
+            newInitialValue = Offset(3f, 4f), newTargetValue = Offset(4f, 5f),
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun sizeAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Size>? = null
+        rule.searchForAnimation(search) {
+            state = animateSizeAsState(
+                targetValue = Size(10f, 20f),
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "SizeAnimation",
+            initialValue = Size(10f, 20f),
+            composeState = { state!!.value })
+        rule.runOnUiThread {
+            clock.setStateParameters(Size(1f, 2f), Size(3f, 4f))
+        }
+        checkUpdatedState(clock, label = "SizeAnimation",
+            newInitialValue = Size(1f, 2f), newTargetValue = Size(3f, 4f),
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(3f, 4f), listOf(4f, 5f)) }
+        checkUpdatedState(clock, label = "SizeAnimation",
+            newInitialValue = Size(3f, 4f), newTargetValue = Size(4f, 5f),
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(Size(111f, 111f), 111)
+            clock.setStateParameters(Size(111f, 111f), null)
+            clock.setStateParameters(10, 10)
+            clock.setStateParameters(listOf(Size(111f, 111f)), listOf(111L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(Size(111f, 111f)), emptyList<Size>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "SizeAnimation",
+            newInitialValue = Size(3f, 4f), newTargetValue = Size(4f, 5f),
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun rectAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Rect>? = null
+        rule.searchForAnimation(search) {
+            state = animateRectAsState(
+                targetValue = Rect(10f, 20f, 30f, 40f),
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "RectAnimation",
+            initialValue = Rect(10f, 20f, 30f, 40f),
+            composeState = { state!!.value })
+        rule.runOnUiThread {
+            clock.setStateParameters(
+                Rect(1f, 2f, 30f, 40f),
+                Rect(3f, 4f, 30f, 40f)
+            )
+        }
+        checkUpdatedState(clock, label = "RectAnimation",
+            newInitialValue = Rect(1f, 2f, 30f, 40f),
+            newTargetValue = Rect(3f, 4f, 30f, 40f),
+            composeState = { state!!.value })
+        rule.runOnUiThread {
+            clock.setStateParameters(
+                listOf(3f, 4f, 30f, 40f),
+                listOf(4f, 5f, 30f, 40f)
+            )
+        }
+        checkUpdatedState(clock, label = "RectAnimation",
+            newInitialValue = Rect(3f, 4f, 30f, 40f),
+            newTargetValue = Rect(4f, 5f, 30f, 40f),
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(Rect(42f, 42f, 42f, 42f), 42f)
+            clock.setStateParameters(Rect(42f, 42f, 42f, 42f), null)
+            clock.setStateParameters(42f, 42f)
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(Rect(42f, 42f, 42f, 42f)), listOf(42f))
+            clock.setStateParameters(listOf(Rect(42f, 42f, 42f, 42f)), emptyList<Rect>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "RectAnimation",
+            newInitialValue = Rect(3f, 4f, 30f, 40f),
+            newTargetValue = Rect(4f, 5f, 30f, 40f),
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun colorAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Color>? = null
+        rule.searchForAnimation(search) {
+            state = animateColorAsState(
+                targetValue = Color.Black,
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "ColorAnimation",
+            initialValue = Color.Black,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(Color.Gray, Color.Red) }
+        checkUpdatedState(clock, label = "ColorAnimation",
+            newInitialValue = Color.Gray, newTargetValue = Color.Red,
+            composeState = { state!!.value })
+        rule.runOnUiThread {
+            clock.setStateParameters(
+                listOf(Color.Blue.red, Color.Blue.green, Color.Blue.blue, Color.Blue.alpha),
+                listOf(Color.Yellow.red, Color.Yellow.green, Color.Yellow.blue, Color.Yellow.alpha)
+            )
+        }
+        checkUpdatedState(clock, label = "ColorAnimation",
+            newInitialValue = Color.Blue, newTargetValue = Color.Yellow,
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(Color.Red, 1)
+            clock.setStateParameters(Color.Red, null)
+            clock.setStateParameters(10, 10)
+            clock.setStateParameters(listOf(Color.Red), listOf(0.1f, 0.2f, 0.3f))
+            clock.setStateParameters(listOf(Color.Red), emptyList<Color>())
+            clock.setStateParameters(listOf(null), listOf(null))
+            // Invalid arguments for color.
+            clock.setStateParameters(listOf(10f, 10f, 10f, 10f), listOf(10f, 10f, 10f, 10f))
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "ColorAnimation",
+            newInitialValue = Color.Blue, newTargetValue = Color.Yellow,
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun customFloatAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Float>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(
+                targetValue = 10f,
+                Float.VectorConverter,
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "ValueAnimation",
+            initialValue = 10f,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(1f, 2f) }
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = 1f, newTargetValue = 2f,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(3f), listOf(4f)) }
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = 3f, newTargetValue = 4f,
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(30f, 40)
+            clock.setStateParameters(30f, null)
+            clock.setStateParameters(30L, 40L)
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(30f), listOf(40L))
+            clock.setStateParameters(emptyList<Float>(), emptyList<Float>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = 3f, newTargetValue = 4f,
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun nullableFloatAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Float?>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(
+                targetValue = 10f,
+                Utils.nullableFloatConverter,
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "ValueAnimation",
+            initialValue = 10f,
+            composeState = { state!!.value!! })
+        rule.runOnUiThread { clock.setStateParameters(1f, 2f) }
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = 1f, newTargetValue = 2f,
+            composeState = { state!!.value!! })
+        rule.runOnUiThread { clock.setStateParameters(listOf(3f), listOf(4f)) }
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = 3f, newTargetValue = 4f,
+            composeState = { state!!.value!! })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(30f, 40)
+            clock.setStateParameters(30f, null)
+            clock.setStateParameters(30L, 40L)
+            clock.setStateParameters(listOf(30f), listOf(40L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(emptyList<Float>(), emptyList<Float>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = 3f, newTargetValue = 4f,
+            composeState = { state!!.value!! })
+    }
+
+    @Test
+    fun stringAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<String>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(
+                targetValue = "10.0",
+                Utils.stringConverter,
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "ValueAnimation",
+            initialValue = "10.0",
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters("20.0", "30.0") }
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = "20.0", newTargetValue = "30.0",
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf("40.0"), listOf("50.0")) }
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = "40.0", newTargetValue = "50.0",
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(30f, 40)
+            clock.setStateParameters(30f, null)
+            clock.setStateParameters(30f, 40f)
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(30f), listOf(40L))
+            clock.setStateParameters(emptyList<String>(), emptyList<Int>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = "40.0", newTargetValue = "50.0",
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun enumAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Utils.EnumState>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(
+                targetValue = Utils.EnumState.One,
+                Utils.enumConverter,
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "ValueAnimation",
+            initialValue = Utils.EnumState.One,
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun booleanAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Boolean>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(
+                targetValue = false,
+                Utils.booleanConverter,
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "ValueAnimation",
+            initialValue = false,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(false, true) }
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = false, newTargetValue = true,
+            composeState = { state!!.value })
+        rule.runOnUiThread { clock.setStateParameters(listOf(true), listOf(false)) }
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = true, newTargetValue = false,
+            composeState = { state!!.value })
+        // Invalid parameters are ignored.
+        rule.runOnUiThread {
+            clock.setStateParameters(true, 111f)
+            clock.setStateParameters(true, null)
+            clock.setStateParameters(42f, 42f)
+            clock.setStateParameters(listOf(true), listOf(111L))
+            clock.setStateParameters(listOf(null), listOf(null))
+            clock.setStateParameters(listOf(true), emptyList<Boolean>())
+        }
+        // State hasn't changed.
+        checkUpdatedState(clock, label = "ValueAnimation",
+            newInitialValue = true, newTargetValue = false,
+            composeState = { state!!.value })
+    }
+
+    @Test
+    fun nullableEnumAnimation() {
+        val search = AnimationSearch.AnimateXAsStateSearch { }
+        var state: State<Utils.EnumState?>? = null
+        rule.searchForAnimation(search) {
+            state = animateValueAsState(
+                targetValue = Utils.EnumState.One,
+                Utils.nullableEnumConverter,
+                animationSpec = TweenSpec(durationMillis = 100)
+            )
+        }
+        val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+        checkInitialState(clock, label = "ValueAnimation",
+            initialValue = Utils.EnumState.One,
+            composeState = { state!!.value!! })
+    }
+
+    private fun <T : ComposeAnimation, TState : ComposeAnimationState, V : Any>
+        checkInitialState(
+        clock: ComposeAnimationClock<T, TState>,
+        label: String,
+        initialValue: V,
+        composeState: () -> V
+    ) {
+        // Check default state.
+        assertEquals(initialValue, composeState())
+        assertEquals(TargetState(initialValue, initialValue), clock.state)
+        assertEquals(100L, clock.getMaxDuration())
+        assertEquals(100L, clock.getMaxDurationPerIteration())
+        assertEquals(
+            listOf(ComposeAnimatedProperty(label, initialValue)),
+            clock.getAnimatedProperties()
+        )
+        val transitions = clock.getTransitions(100)
+        assertEquals(1, transitions.size)
+        transitions.first().let {
+            assertEquals(label, it.label)
+            assertEquals(0L, it.startTimeMillis)
+            assertEquals(100L, it.endTimeMillis)
+            assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
+            assertEquals(mapOf(0L to initialValue, 100L to initialValue), it.values)
+        }
+    }
+
+    private fun <T : ComposeAnimation, TState : ComposeAnimationState, V : Any>
+        checkUpdatedState(
+        clock: ComposeAnimationClock<T, TState>,
+        label: String,
+        newInitialValue: V,
+        newTargetValue: V,
+        composeState: () -> V
+    ) {
+        rule.waitForIdle()
+        // Check new state.
+        rule.runOnUiThread {
+            clock.setClockTime(0)
+            assertEquals(newInitialValue, composeState())
+            assertEquals(TargetState(newInitialValue, newTargetValue), clock.state)
+            assertEquals(100L, clock.getMaxDuration())
+            assertEquals(100L, clock.getMaxDurationPerIteration())
+            assertEquals(
+                listOf(ComposeAnimatedProperty(label, newInitialValue)),
+                clock.getAnimatedProperties()
+            )
+            val newTransitions = clock.getTransitions(100)
+            assertEquals(1, newTransitions.size)
+            newTransitions.first().let {
+                assertEquals(label, it.label)
+                assertEquals(0L, it.startTimeMillis)
+                assertEquals(100L, it.endTimeMillis)
+                assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
+                assertEquals(mapOf(0L to newInitialValue, 100L to newTargetValue), it.values)
+            }
+        }
+        // Jump to the end of the animation.
+        rule.runOnUiThread {
+            clock.setClockTime(millisToNanos(300L))
+        }
+        // Check state at the end of the animation.
+        rule.waitForIdle()
+        rule.runOnUiThread {
+            assertEquals(newTargetValue, composeState())
+            val properties = clock.getAnimatedProperties()
+            assertEquals(listOf(ComposeAnimatedProperty(label, newTargetValue)), properties)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
index 14b8d3d..4f473ff 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
@@ -48,8 +48,9 @@
 import androidx.compose.ui.platform.LocalFontLoader
 import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.text.font.createFontFamilyResolver
-import androidx.compose.ui.tooling.animation.PreviewAnimationClock
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation
 import androidx.compose.ui.tooling.animation.AnimationSearch
+import androidx.compose.ui.tooling.animation.PreviewAnimationClock
 import androidx.compose.ui.tooling.animation.UnsupportedComposeAnimation
 import androidx.compose.ui.tooling.data.Group
 import androidx.compose.ui.tooling.data.SourceLocation
@@ -314,23 +315,29 @@
             clock.trackAnimatedVisibility(it, ::requestLayout)
         }
         // All supported animations.
-        val supportedSearch = setOf(
+        fun supportedSearch() = setOf(
             transitionSearch,
             animatedVisibilitySearch,
         )
 
-        // All unsupported animations, if API is available.
-        val extraSearch = if (UnsupportedComposeAnimation.apiAvailable) setOf(
+        fun unsupportedSearch() = if (UnsupportedComposeAnimation.apiAvailable) setOf(
             animatedContentSearch,
-            AnimationSearch.AnimateXAsStateSearch { clock.trackAnimateXAsState(it) },
             AnimationSearch.AnimateContentSizeSearch { clock.trackAnimateContentSize(it) },
             AnimationSearch.TargetBasedSearch { clock.trackTargetBasedAnimations(it) },
             AnimationSearch.DecaySearch { clock.trackDecayAnimations(it) },
             AnimationSearch.InfiniteTransitionSearch { clock.trackInfiniteTransition(it) }
         ) else emptyList()
 
+        fun animateXAsStateSearch() =
+            if (AnimateXAsStateComposeAnimation.apiAvailable)
+                setOf(AnimationSearch.AnimateXAsStateSearch { clock.trackAnimateXAsState(it) })
+            else emptyList()
+
+        // All unsupported animations, if API is available.
+        val extraSearch = unsupportedSearch() + animateXAsStateSearch()
+
         // Animations to track in PreviewAnimationClock.
-        val setToTrack = supportedSearch + extraSearch
+        val setToTrack = supportedSearch() + extraSearch
 
         // Animations to search. animatedContentSearch is included even if it's not going to be
         // tracked as it should be excluded from transitionSearch.
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.android.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.android.kt
new file mode 100644
index 0000000..981a832
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.android.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling
+
+import android.util.Log
+
+private const val Tag = "PreviewLogger"
+
+internal actual class PreviewLogger {
+
+    actual companion object {
+        internal actual fun logWarning(message: String, throwable: Throwable?) {
+            Log.w(Tag, message, throwable)
+        }
+
+        internal actual fun logError(message: String, throwable: Throwable?) {
+            Log.e(Tag, message, throwable)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.kt
index 43a9ec6..6398dee 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.tooling
 
-import android.util.Log
 import androidx.compose.ui.tooling.data.Group
 import androidx.compose.ui.tooling.data.UiToolingDataApi
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@@ -29,7 +28,7 @@
         @Suppress("UNCHECKED_CAST")
         return Class.forName(this) as? Class<out PreviewParameterProvider<*>>
     } catch (e: ClassNotFoundException) {
-        Log.e("PreviewProvider", "Unable to find provider '$this'", e)
+        PreviewLogger.logError("Unable to find PreviewProvider '$this'", e)
         return null
     }
 }
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimation.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimation.kt
new file mode 100644
index 0000000..a1e339b
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimation.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.tooling.ComposeAnimation
+import androidx.compose.animation.tooling.ComposeAnimationType
+import org.jetbrains.annotations.TestOnly
+
+/**
+ * [ComposeAnimation] of type [ComposeAnimationType.ANIMATE_X_AS_STATE].
+ */
+internal class AnimateXAsStateComposeAnimation<T, V : AnimationVector>
+private constructor(
+    val toolingState: ToolingState<T>,
+    val animationSpec: AnimationSpec<T>,
+    override val animationObject: Animatable<T, V>,
+) : ComposeAnimation {
+    override val type = ComposeAnimationType.ANIMATE_X_AS_STATE
+
+    override val states: Set<Any> = (animationObject.value as Any).let {
+        it.javaClass.enumConstants?.toSet() ?: setOf(it)
+    }
+
+    override val label: String = animationObject.label
+
+    @Suppress("UNCHECKED_CAST")
+    fun setState(value: Any) {
+        toolingState.value = value as T
+    }
+
+    companion object {
+
+        /**
+         * [ComposeAnimationType] from ANIMATABLE to UNSUPPORTED are not available in previous
+         * versions of the library. To avoid creating non-existing enum,
+         * [UnsupportedComposeAnimation] should only be instantiated if [ComposeAnimationType] API
+         * for UNSUPPORTED enum is available.
+         */
+        var apiAvailable = enumValues<ComposeAnimationType>().any { it.name == "UNSUPPORTED" }
+            private set
+
+        internal fun <T, V : AnimationVector> AnimationSearch
+        .AnimateXAsStateSearchInfo<T, V>.parse():
+            AnimateXAsStateComposeAnimation<*, *>? {
+            if (!apiAvailable) return null
+            // Tooling can't control nullable Animatable with value set to null.
+            if (animatable.value == null) return null
+            return AnimateXAsStateComposeAnimation(
+                toolingState, animationSpec, animatable
+            )
+        }
+
+        /** This method is for testing only. */
+        @TestOnly
+        fun testOverrideAvailability(override: Boolean) {
+            apiAvailable = override
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
index f439810..790ecc0 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
@@ -17,10 +17,15 @@
 package androidx.compose.ui.tooling.animation
 
 import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationVector
 import androidx.compose.animation.core.DecayAnimation
 import androidx.compose.animation.core.InfiniteTransition
 import androidx.compose.animation.core.TargetBasedAnimation
 import androidx.compose.animation.core.Transition
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.ui.tooling.data.CallGroup
 import androidx.compose.ui.tooling.data.Group
 import androidx.compose.ui.tooling.data.UiToolingDataApi
 import androidx.compose.ui.tooling.firstOrNull
@@ -32,6 +37,7 @@
 private const val ANIMATED_VISIBILITY = "AnimatedVisibility"
 private const val ANIMATE_VALUE_AS_STATE = "animateValueAsState"
 private const val REMEMBER = "remember"
+private const val REMEMBER_UPDATED_STATE = "rememberUpdatedState"
 private const val SIZE_ANIMATION_MODIFIER = "androidx.compose.animation.SizeAnimationModifier"
 
 /** Find first data with type [T] within all remember calls. */
@@ -93,10 +99,21 @@
     class InfiniteTransitionSearch(trackAnimation: (InfiniteTransition) -> Unit) :
         RememberSearch<InfiniteTransition>(InfiniteTransition::class, trackAnimation)
 
+    data class AnimateXAsStateSearchInfo<T, V : AnimationVector>(
+        val animatable: Animatable<T, V>,
+        val animationSpec: AnimationSpec<T>,
+        val toolingState: ToolingState<T>
+    )
+
     /** Search for animateXAsState() and animateValueAsState() animations. */
-    class AnimateXAsStateSearch(trackAnimation: (Animatable<*, *>) -> Unit) :
-        Search<Animatable<*, *>>(trackAnimation) {
+    class AnimateXAsStateSearch(trackAnimation: (AnimateXAsStateSearchInfo<*, *>) -> Unit) :
+        Search<AnimateXAsStateSearchInfo<*, *>>(trackAnimation) {
         override fun addAnimations(groupsWithLocation: Collection<Group>) {
+            animations.addAll(findAnimations<Any?>(groupsWithLocation))
+        }
+
+        private fun <T> findAnimations(groupsWithLocation: Collection<Group>):
+            List<AnimateXAsStateSearchInfo<T, AnimationVector>> {
             // How "animateXAsState" calls organized:
             // Group with name "animateXAsState", for example animateDpAsState, animateIntAsState
             //    children
@@ -107,12 +124,37 @@
             // To distinguish Animatable within "animateXAsState" calls from other Animatables,
             // first "animateValueAsState" calls are found.
             //  Find Animatable within "animateValueAsState" call.
-            animations.addAll(
-                groupsWithLocation.filter { call -> call.name == ANIMATE_VALUE_AS_STATE }
-                    .mapNotNull { animateValue ->
-                        animateValue.children.findRememberedData<Animatable<*, *>>().firstOrNull()
-                    }.toSet()
-            )
+            val groups = groupsWithLocation.filter { group -> group.name == ANIMATE_VALUE_AS_STATE }
+                .filterIsInstance<CallGroup>()
+            return groups.mapNotNull {
+                val animatable = findAnimatable<T>(it)
+                val spec = findAnimationSpec<T>(it)
+                val toolingOverride =
+                    it.children.findRememberedData<MutableState<State<T>?>>().firstOrNull()
+                if (animatable != null && spec != null && toolingOverride != null) {
+                    if (toolingOverride.value == null) {
+                        toolingOverride.value = ToolingState(animatable.value)
+                    }
+                    AnimateXAsStateSearchInfo(
+                        animatable,
+                        spec,
+                        toolingOverride.value as ToolingState<T>
+                    )
+                } else null
+            }
+        }
+
+        @Suppress("UNCHECKED_CAST")
+        private fun <T> findAnimationSpec(group: CallGroup): AnimationSpec<T>? {
+            return group.children.filter { it.name == REMEMBER_UPDATED_STATE }
+                .flatMap { it.data }
+                .filterIsInstance<State<T>>().map { it.value }
+                .filterIsInstance<AnimationSpec<T>>().firstOrNull()
+        }
+
+        private fun <T> findAnimatable(group: CallGroup): Animatable<T, AnimationVector>? {
+            return group.children.findRememberedData<Animatable<T, AnimationVector>>()
+                .firstOrNull()
         }
     }
 
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
index a24f04c..e065db8 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
@@ -18,7 +18,6 @@
 
 import android.util.Log
 import androidx.annotation.VisibleForTesting
-import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.DecayAnimation
 import androidx.compose.animation.core.InfiniteTransition
 import androidx.compose.animation.core.TargetBasedAnimation
@@ -26,6 +25,8 @@
 import androidx.compose.animation.tooling.ComposeAnimatedProperty
 import androidx.compose.animation.tooling.ComposeAnimation
 import androidx.compose.animation.tooling.TransitionInfo
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.clock.AnimateXAsStateClock
 import androidx.compose.ui.tooling.animation.clock.AnimatedVisibilityClock
 import androidx.compose.ui.tooling.animation.clock.ComposeAnimationClock
 import androidx.compose.ui.tooling.animation.clock.TransitionClock
@@ -72,12 +73,19 @@
     internal val animatedVisibilityClocks =
         mutableMapOf<AnimatedVisibilityComposeAnimation, AnimatedVisibilityClock>()
 
+    /** Map of subscribed [AnimateXAsStateComposeAnimation]s and corresponding [AnimateXAsStateClock]s. */
+    @VisibleForTesting
+    internal val animateXAsStateClocks =
+        mutableMapOf<AnimateXAsStateComposeAnimation<*, *>, AnimateXAsStateClock<*, *>>()
+
     /** All subscribed animations clocks. */
     private val allClocks: List<ComposeAnimationClock<*, *>>
-        get() = transitionClocks.values + animatedVisibilityClocks.values
+        get() = transitionClocks.values +
+            animatedVisibilityClocks.values + animateXAsStateClocks.values
 
     private fun findClock(animation: ComposeAnimation): ComposeAnimationClock<*, *>? {
         return transitionClocks[animation] ?: animatedVisibilityClocks[animation]
+        ?: animateXAsStateClocks[animation]
     }
 
     fun trackTransition(animation: Transition<*>) {
@@ -106,8 +114,13 @@
         }
     }
 
-    fun trackAnimateXAsState(animation: Animatable<*, *>) {
-        trackUnsupported(animation, animation.label)
+    fun trackAnimateXAsState(animation: AnimationSearch.AnimateXAsStateSearchInfo<*, *>) {
+        trackAnimation(animation.animatable) {
+            animation.parse()?.let {
+                animateXAsStateClocks[it] = AnimateXAsStateClock(it)
+                notifySubscribe(it)
+            }
+        }
     }
 
     fun trackAnimateContentSize(animation: Any) {
@@ -185,7 +198,7 @@
      * Expected to be called via reflection from Android Studio.
      */
     fun updateFromAndToStates(composeAnimation: ComposeAnimation, fromState: Any, toState: Any) {
-        transitionClocks[composeAnimation]?.setStateParameters(fromState, toState)
+        findClock(composeAnimation)?.setStateParameters(fromState, toState)
     }
 
     /**
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/ToolingState.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/ToolingState.kt
new file mode 100644
index 0000000..c785e65
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/ToolingState.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+/**
+ * Tooling can override [mutableStateOf] in [Composable] with [ToolingState].
+ * [Composable] should declare a state, which could be private:
+ *
+ *      val toolingOverride = remember { mutableStateOf<State<T>?>(null) }
+ *
+ * Tooling overrides `toolingOverride` with
+ *
+ *      toolingOverride.value = ToolingState(default)
+ *
+ * @param default default value
+ */
+class ToolingState<T>(default: T) : State<T> {
+    override var value by mutableStateOf(default)
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
new file mode 100644
index 0000000..d80cf01
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation.clock
+
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.TargetBasedAnimation
+import androidx.compose.animation.tooling.ComposeAnimatedProperty
+import androidx.compose.animation.tooling.TransitionInfo
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation
+import androidx.compose.ui.tooling.animation.states.TargetState
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * [ComposeAnimationClock] for [AnimateXAsStateComposeAnimation].
+ */
+internal class AnimateXAsStateClock<T, V : AnimationVector>(
+    override val animation: AnimateXAsStateComposeAnimation<T, V>
+) :
+    ComposeAnimationClock<AnimateXAsStateComposeAnimation<T, V>, TargetState<T>> {
+
+    override var state = TargetState(
+        animation.animationObject.value,
+        animation.animationObject.value
+    )
+        set(value) {
+            field = value
+            currAnimation = getCurrentAnimation()
+            setClockTime(0)
+        }
+
+    private var currentValue: T = animation.toolingState.value
+        private set(value) {
+            field = value
+            animation.toolingState.value = value
+        }
+
+    private var currAnimation: TargetBasedAnimation<T, V> = getCurrentAnimation()
+
+    @Suppress("UNCHECKED_CAST")
+    override fun setStateParameters(par1: Any, par2: Any?) {
+
+        fun parametersAreValid(par1: Any?, par2: Any?): Boolean {
+            return currentValue != null &&
+                par1 != null && par2 != null && par1::class == par2::class
+        }
+
+        fun parametersHasTheSameType(value: Any, par1: Any, par2: Any): Boolean {
+            return value::class == par1::class && value::class == par2::class
+        }
+
+        if (!parametersAreValid(par1, par2)) return
+
+        if (parametersHasTheSameType(currentValue!!, par1, par2!!)) {
+            state = TargetState(par1 as T, par2 as T)
+            return
+        }
+
+        if (par1 is List<*> && par2 is List<*>) {
+            try {
+                state = when (currentValue) {
+                    is IntSize -> TargetState(
+                        IntSize(par1[0] as Int, par1[1] as Int),
+                        IntSize(par2[0] as Int, par2[1] as Int)
+                    )
+
+                    is IntOffset -> TargetState(
+                        IntOffset(par1[0] as Int, par1[1] as Int),
+                        IntOffset(par2[0] as Int, par2[1] as Int)
+                    )
+
+                    is Size -> TargetState(
+                        Size(par1[0] as Float, par1[1] as Float),
+                        Size(par2[0] as Float, par2[1] as Float)
+                    )
+
+                    is Offset -> TargetState(
+                        Offset(par1[0] as Float, par1[1] as Float),
+                        Offset(par2[0] as Float, par2[1] as Float)
+                    )
+
+                    is Rect ->
+                        TargetState(
+                            Rect(
+                                par1[0] as Float,
+                                par1[1] as Float,
+                                par1[2] as Float,
+                                par1[3] as Float
+                            ),
+                            Rect(
+                                par2[0] as Float,
+                                par2[1] as Float,
+                                par2[2] as Float,
+                                par2[3] as Float
+                            ),
+                        )
+                    is Color -> TargetState(
+                        Color(
+                            par1[0] as Float,
+                            par1[1] as Float,
+                            par1[2] as Float,
+                            par1[3] as Float
+                        ),
+                        Color(
+                            par2[0] as Float,
+                            par2[1] as Float,
+                            par2[2] as Float,
+                            par2[3] as Float
+                        ),
+                    )
+                    else -> {
+                        if (parametersAreValid(par1[0], par2[0]) &&
+                            parametersHasTheSameType(currentValue!!, par1[0]!!, par2[0]!!)
+                        ) TargetState(par1[0], par2[0])
+                        else return
+                    }
+                } as TargetState<T>
+            } catch (_: IndexOutOfBoundsException) {
+                return
+            } catch (_: ClassCastException) {
+                return
+            } catch (_: IllegalArgumentException) {
+                return
+            } catch (_: NullPointerException) {
+                return
+            }
+        }
+    }
+
+    override fun getAnimatedProperties(): List<ComposeAnimatedProperty> {
+        return listOf(ComposeAnimatedProperty(animation.label, currentValue as Any))
+    }
+
+    override fun getMaxDurationPerIteration(): Long {
+        return nanosToMillis(currAnimation.durationNanos)
+    }
+
+    override fun getMaxDuration(): Long {
+        return nanosToMillis(currAnimation.durationNanos)
+    }
+
+    override fun getTransitions(stepMillis: Long): List<TransitionInfo> {
+        return listOf(
+            currAnimation.createTransitionInfo(
+                animation.label, animation.animationSpec, stepMillis
+            )
+        )
+    }
+
+    private var clockTimeNanos = 0L
+        set(value) {
+            field = value
+            currentValue = currAnimation.getValueFromNanos(value)
+        }
+
+    override fun setClockTime(animationTimeNanos: Long) {
+        clockTimeNanos = animationTimeNanos
+    }
+
+    private fun getCurrentAnimation(): TargetBasedAnimation<T, V> {
+        return TargetBasedAnimation(
+            animationSpec = animation.animationSpec,
+            initialValue = state.initial,
+            targetValue = state.target,
+            typeConverter = animation.animationObject.typeConverter,
+            initialVelocity = animation.animationObject.velocity
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/PreviewLogger.desktop.kt b/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/PreviewLogger.desktop.kt
new file mode 100644
index 0000000..7b9ca9f
--- /dev/null
+++ b/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/PreviewLogger.desktop.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling
+
+private const val Tag = "PreviewLogger"
+
+internal actual class PreviewLogger {
+
+    actual companion object {
+        internal actual fun logWarning(message: String, throwable: Throwable?) {
+            println("$Tag: $message.\n$throwable")
+        }
+
+        internal actual fun logError(message: String, throwable: Throwable?) {
+            System.err.println("$Tag: $message.\n$throwable")
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
index 28d41ec..04b7bfd 100644
--- a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
+++ b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
@@ -200,7 +200,10 @@
                 method.invokeComposableMethod(instance, composer, *args)
             }
         } catch (e: ReflectiveOperationException) {
-            throw ClassNotFoundException("Composable Method '$className.$methodName' not found", e)
+            PreviewLogger.logWarning(
+                "Failed to invoke Composable Method '$className.$methodName'"
+            )
+            throw e
         }
     }
 }
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.jvm.kt
similarity index 67%
rename from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
rename to compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.jvm.kt
index 79122bf..c26ea17 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
+++ b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.jvm.kt
@@ -14,10 +14,13 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler.generator
+package androidx.compose.ui.tooling
 
-import com.squareup.kotlinpoet.FileSpec
-import java.io.OutputStream
+internal expect class PreviewLogger {
 
-/** Convenience method to write [FileSpec]s to KSP-generated [OutputStream]s. */
-internal fun OutputStream.write(spec: FileSpec) = bufferedWriter().use(spec::writeTo)
+    companion object {
+        internal fun logWarning(message: String, throwable: Throwable? = null)
+
+        internal fun logError(message: String, throwable: Throwable? = null)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 91506e6..7dd6735 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2425,6 +2425,7 @@
   }
 
   public final class DelegatableNodeKt {
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
   }
 
   @androidx.compose.ui.ExperimentalComposeUiApi public abstract class DelegatingNode extends androidx.compose.ui.Modifier.Node {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index ad1c03f..8bed753 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
@@ -31,6 +32,7 @@
 import androidx.compose.testutils.assertPixels
 import androidx.compose.ui.AtLeastSize
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Color
@@ -97,26 +99,29 @@
             var rectColor by remember { mutableStateOf(Color.Blue) }
             AtLeastSize(
                 size = size,
-                modifier = Modifier.testTag(testTag).drawWithCache {
-                    val drawSize = this.size
-                    val path = Path().apply {
-                        lineTo(drawSize.width / 2f, 0f)
-                        lineTo(drawSize.width / 2f, drawSize.height)
-                        lineTo(0f, drawSize.height)
-                        close()
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        val drawSize = this.size
+                        val path = Path().apply {
+                            lineTo(drawSize.width / 2f, 0f)
+                            lineTo(drawSize.width / 2f, drawSize.height)
+                            lineTo(0f, drawSize.height)
+                            close()
+                        }
+                        cacheBuildCount++
+                        onDrawBehind {
+                            drawRect(rectColor)
+                            drawPath(path, Color.Red)
+                        }
                     }
-                    cacheBuildCount++
-                    onDrawBehind {
-                        drawRect(rectColor)
-                        drawPath(path, Color.Red)
+                    .clickable {
+                        if (rectColor == Color.Blue) {
+                            rectColor = Color.Green
+                        } else {
+                            rectColor = Color.Blue
+                        }
                     }
-                }.clickable {
-                    if (rectColor == Color.Blue) {
-                        rectColor = Color.Green
-                    } else {
-                        rectColor = Color.Blue
-                    }
-                }
             ) { }
         }
 
@@ -156,6 +161,34 @@
         }
     }
 
+    @Test
+    fun invalidationForDrawWithCache() {
+        var size by mutableStateOf(10f)
+        var drawCount = 0
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                Box(Modifier
+                    .graphicsLayer { }
+                    .size(50.dp)
+                    .drawWithCache {
+                        val rectSize = Size(size, size)
+                        onDrawBehind {
+                            drawRect(Color.Blue, Offset.Zero, rectSize)
+                            drawCount++
+                        }
+                    }
+                    .graphicsLayer { }
+                )
+            }
+        }
+        rule.waitForIdle()
+        assertThat(drawCount).isEqualTo(1)
+
+        size = 15f
+        rule.waitForIdle()
+        assertThat(drawCount).isEqualTo(2)
+    }
+
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun testCacheInvalidatedAfterStateChange() {
@@ -169,22 +202,25 @@
             var pathFillBounds by remember { mutableStateOf(false) }
             AtLeastSize(
                 size = size,
-                modifier = Modifier.testTag(testTag).drawWithCache {
-                    val pathSize = if (pathFillBounds) this.size else this.size / 2f
-                    val path = Path().apply {
-                        lineTo(pathSize.width, 0f)
-                        lineTo(pathSize.width, pathSize.height)
-                        lineTo(0f, pathSize.height)
-                        close()
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        val pathSize = if (pathFillBounds) this.size else this.size / 2f
+                        val path = Path().apply {
+                            lineTo(pathSize.width, 0f)
+                            lineTo(pathSize.width, pathSize.height)
+                            lineTo(0f, pathSize.height)
+                            close()
+                        }
+                        cacheBuildCount++
+                        onDrawBehind {
+                            drawRect(Color.Red)
+                            drawPath(path, Color.Blue)
+                        }
                     }
-                    cacheBuildCount++
-                    onDrawBehind {
-                        drawRect(Color.Red)
-                        drawPath(path, Color.Blue)
+                    .clickable {
+                        pathFillBounds = !pathFillBounds
                     }
-                }.clickable {
-                    pathFillBounds = !pathFillBounds
-                }
             ) { }
         }
 
@@ -269,25 +305,28 @@
             var size by remember { mutableStateOf(startSize) }
             AtLeastSize(
                 size = size,
-                modifier = Modifier.testTag(testTag).drawWithCache {
-                    val drawSize = this.size
-                    val path = Path().apply {
-                        lineTo(drawSize.width, 0f)
-                        lineTo(drawSize.height, drawSize.height)
-                        lineTo(0f, drawSize.height)
-                        close()
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        val drawSize = this.size
+                        val path = Path().apply {
+                            lineTo(drawSize.width, 0f)
+                            lineTo(drawSize.height, drawSize.height)
+                            lineTo(0f, drawSize.height)
+                            close()
+                        }
+                        cacheBuildCount++
+                        onDrawBehind {
+                            drawPath(path, Color.Red)
+                        }
                     }
-                    cacheBuildCount++
-                    onDrawBehind {
-                        drawPath(path, Color.Red)
+                    .clickable {
+                        if (size == startSize) {
+                            size = endSize
+                        } else {
+                            size = startSize
+                        }
                     }
-                }.clickable {
-                    if (size == startSize) {
-                        size = endSize
-                    } else {
-                        size = startSize
-                    }
-                }
             ) { }
         }
 
@@ -356,7 +395,9 @@
             val color = remember { mutableStateOf(Color.Red) }
             AtLeastSize(
                 size = startSize,
-                modifier = Modifier.testTag(testTag).drawPathHelperModifier(color.value)
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawPathHelperModifier(color.value)
                     .clickable {
                         if (color.value == Color.Red) {
                             color.value = Color.Blue
@@ -403,7 +444,8 @@
             Column {
                 AtLeastSize(
                     size = 50,
-                    modifier = Modifier.testTag(boxTag)
+                    modifier = Modifier
+                        .testTag(boxTag)
                         .graphicsLayer()
                         .drawWithCache {
                             // State read of flag
@@ -417,7 +459,8 @@
                 )
 
                 Box(
-                    Modifier.testTag(clickTag)
+                    Modifier
+                        .testTag(clickTag)
                         .size(20.dp)
                         .clickable {
                             flag.value = !flag.value
@@ -473,11 +516,14 @@
         rule.setContent {
             AtLeastSize(
                 size = testSize,
-                modifier = Modifier.testTag(testTag).drawWithCache {
-                    onDrawBehind {
-                        drawRect(Color.Red, size = Size(size.width / 2, size.height))
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        onDrawBehind {
+                            drawRect(Color.Red, size = Size(size.width / 2, size.height))
+                        }
                     }
-                }.background(Color.Blue)
+                    .background(Color.Blue)
             )
         }
 
@@ -502,12 +548,15 @@
         rule.setContent {
             AtLeastSize(
                 size = testSize,
-                modifier = Modifier.testTag(testTag).drawWithCache {
-                    onDrawWithContent {
-                        drawContent()
-                        drawRect(Color.Red, size = Size(size.width / 2, size.height))
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        onDrawWithContent {
+                            drawContent()
+                            drawRect(Color.Red, size = Size(size.width / 2, size.height))
+                        }
                     }
-                }.background(Color.Blue)
+                    .background(Color.Blue)
             )
         }
 
@@ -536,12 +585,15 @@
         rule.setContent {
             AtLeastSize(
                 size = testSize,
-                modifier = Modifier.testTag(testTag).drawWithCache {
-                    onDrawWithContent {
-                        drawContent()
-                        drawRect(Color.Green, blendMode = BlendMode.Plus)
+                modifier = Modifier
+                    .testTag(testTag)
+                    .drawWithCache {
+                        onDrawWithContent {
+                            drawContent()
+                            drawRect(Color.Green, blendMode = BlendMode.Plus)
+                        }
                     }
-                }.background(Color.Blue)
+                    .background(Color.Blue)
             )
         }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
new file mode 100644
index 0000000..b7279e2
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.ui.node
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.DrawModifier
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.graphicsLayer
+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.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+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
+
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class InvalidateSubtreeTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun invalidateSubtreeNoLayers() {
+        lateinit var invalidate: () -> Unit
+        val counter1 = LayoutAndDrawCounter()
+        val counter2 = LayoutAndDrawCounter()
+        val counter3 = LayoutAndDrawCounter()
+        val captureInvalidate = modifierElementOf(
+            create = {
+                val obj = object : Modifier.Node() {}
+                invalidate = { obj.invalidateSubtree() }
+                obj
+            }
+        )
+        rule.setContent {
+            Box(counter1) {
+                Box(counter2 then captureInvalidate) {
+                    Box(counter3.size(10.dp))
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertThat(counter1.drawCount).isEqualTo(1)
+        assertThat(counter1.measureCount).isEqualTo(1)
+        assertThat(counter1.placeCount).isEqualTo(1)
+        assertThat(counter2.drawCount).isEqualTo(1)
+        assertThat(counter2.measureCount).isEqualTo(1)
+        assertThat(counter2.placeCount).isEqualTo(1)
+        assertThat(counter3.drawCount).isEqualTo(1)
+        assertThat(counter3.measureCount).isEqualTo(1)
+        assertThat(counter3.placeCount).isEqualTo(1)
+
+        rule.runOnUiThread {
+            invalidate()
+        }
+        rule.waitForIdle()
+
+        // There isn't a layer that can be invalidated, so we draw this twice also
+        assertThat(counter1.drawCount).isEqualTo(2)
+        assertThat(counter1.measureCount).isEqualTo(1)
+        assertThat(counter1.placeCount).isEqualTo(1)
+        assertThat(counter2.drawCount).isEqualTo(2)
+        assertThat(counter2.measureCount).isEqualTo(2)
+        assertThat(counter2.placeCount).isEqualTo(2)
+        assertThat(counter3.drawCount).isEqualTo(2)
+        assertThat(counter3.measureCount).isEqualTo(2)
+        assertThat(counter3.placeCount).isEqualTo(2)
+    }
+
+    @Test
+    fun invalidateSubtreeWithLayers() {
+        lateinit var invalidate: () -> Unit
+        val counter1 = LayoutAndDrawCounter()
+        val counter2 = LayoutAndDrawCounter()
+        val counter3 = LayoutAndDrawCounter()
+        val counter4 = LayoutAndDrawCounter()
+        val captureInvalidate = modifierElementOf(
+            create = {
+                val obj = object : Modifier.Node() {}
+                invalidate = { obj.invalidateSubtree() }
+                obj
+            }
+        )
+        rule.setContent {
+            Box(Modifier.graphicsLayer {} then counter1.graphicsLayer { }) {
+                Box(Modifier.graphicsLayer { } then
+                    counter2 then captureInvalidate.graphicsLayer { } then counter3
+                ) {
+                    Box(counter4.size(10.dp))
+                }
+            }
+        }
+        rule.waitForIdle()
+        assertThat(counter1.drawCount).isEqualTo(1)
+        assertThat(counter2.drawCount).isEqualTo(1)
+        assertThat(counter3.drawCount).isEqualTo(1)
+        assertThat(counter4.drawCount).isEqualTo(1)
+
+        rule.runOnUiThread {
+            invalidate()
+        }
+        rule.waitForIdle()
+
+        assertThat(counter1.drawCount).isEqualTo(1)
+        assertThat(counter2.drawCount).isEqualTo(2)
+        assertThat(counter3.drawCount).isEqualTo(2)
+        assertThat(counter4.drawCount).isEqualTo(2)
+    }
+
+    private class LayoutAndDrawCounter : LayoutModifier, DrawModifier {
+        var measureCount = 0
+        var placeCount = 0
+        var drawCount = 0
+        override fun MeasureScope.measure(
+            measurable: Measurable,
+            constraints: Constraints
+        ): MeasureResult {
+            measureCount++
+            val p = measurable.measure(constraints)
+            return layout(p.width, p.height) {
+                placeCount++
+                p.place(0, 0)
+            }
+        }
+
+        override fun ContentDrawScope.draw() {
+            drawCount++
+            drawContent()
+        }
+    }
+}
\ No newline at end of file
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 0c989fd..5575793 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
@@ -28,6 +28,7 @@
 import androidx.core.view.doOnLayout
 import androidx.test.ext.junit.rules.activityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
@@ -92,6 +93,7 @@
      * [MonotonicFrameClock.withFrameNanos] will resume in time to make the current frame if called
      * from the situation described above, and that subsequent calls will wait until the next frame.
      */
+    @FlakyTest(bugId = 255972660)
     @Test
     fun runsBeforeFrameDispatchedByInput() = runBlocking {
         val ranInputJobOnFrame = CompletableDeferred<Int>()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index 6fe7f05..da9ee0f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -70,6 +70,7 @@
 import androidx.test.espresso.assertion.ViewAssertions.matches
 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SmallTest
 import org.hamcrest.CoreMatchers.instanceOf
@@ -92,6 +93,7 @@
     @get:Rule
     val rule = createAndroidComposeRule<ComponentActivity>()
 
+    @FlakyTest(bugId = 256017578)
     @Test
     fun composeViewComposedContent() {
         rule.activityRule.scenario.onActivity { activity ->
@@ -878,4 +880,4 @@
     ): Boolean {
         return super.addViewInLayout(child, index, params, preventRequestLayout)
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 4f87447..b6a4e25 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -141,7 +141,7 @@
  *
  * The dialog is visible as long as it is part of the composition hierarchy.
  * In order to let the user dismiss the Dialog, the implementation of [onDismissRequest] should
- * contain a way to remove to remove the dialog from the composition hierarchy.
+ * contain a way to remove the dialog from the composition hierarchy.
  *
  * Example usage:
  *
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 9c135bd..215dd4d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -306,3 +306,17 @@
 
 @ExperimentalComposeUiApi
 internal fun DelegatableNode.requireOwner(): Owner = requireLayoutNode().owner!!
+
+/**
+ * Invalidates the subtree of this layout, including layout, drawing, parent data, etc.
+ *
+ * Calling this method can be a relatively expensive operation as it will cause the
+ * entire subtree to relayout and redraw instead of just parts that
+ * are otherwise invalidated. Its use should be limited to structural changes.
+ */
+@ExperimentalComposeUiApi
+fun DelegatableNode.invalidateSubtree() {
+    if (node.isAttached) {
+        requireLayoutNode().invalidateSubtree()
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
index d123010..69c46bb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
@@ -35,4 +35,8 @@
 }
 
 @ExperimentalComposeUiApi
-internal fun DrawModifierNode.requestDraw() = requireLayoutNode().invalidateLayer()
+internal fun DrawModifierNode.requestDraw() {
+    if (node.isAttached) {
+        requireCoordinator(Nodes.Any).invalidateLayer()
+    }
+}
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 a04cfee..85db8a6 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
@@ -1178,6 +1178,22 @@
      */
     internal fun markLookaheadLayoutPending() = layoutDelegate.markLookaheadLayoutPending()
 
+    @OptIn(ExperimentalComposeUiApi::class)
+    fun invalidateSubtree(isRootOfInvalidation: Boolean = true) {
+        if (isRootOfInvalidation) {
+            parent?.invalidateLayer()
+            // Invalidate semantics. We can do this once because there isn't a node-by-node
+            // invalidation mechanism.
+            requireOwner().onSemanticsChange()
+        }
+        requestRemeasure()
+        nodes.headToTail(Nodes.Layout) {
+            it.requireCoordinator(Nodes.Layout).layer?.invalidate()
+        }
+        // TODO: invalidate parent data
+        _children.forEach { it.invalidateSubtree(false) }
+    }
+
     /**
      * Marks the layoutNode dirty for another lookahead measure pass.
      */
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index fc6a1b5f..9bd9dc8 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -21,9 +21,6 @@
   @androidx.constraintlayout.compose.ExperimentalMotionApi public abstract class BaseKeyFrameScope {
     method protected final <E extends androidx.constraintlayout.compose.NamedPropertyOrValue> kotlin.properties.ObservableProperty<E> addNameOnPropertyChange(E? initialValue, optional String? nameOverride);
     method protected final <T> kotlin.properties.ObservableProperty<T> addOnPropertyChange(T? initialValue, optional String? nameOverride);
-    method public final void addToContainer(androidx.constraintlayout.core.parser.CLContainer container);
-    method protected final java.util.Map<java.lang.String,java.lang.Object> getUserAttributes();
-    property protected final java.util.Map<java.lang.String,java.lang.Object> userAttributes;
   }
 
   @androidx.constraintlayout.compose.ExperimentalMotionApi public class BaseKeyFramesScope {
@@ -649,7 +646,15 @@
     method public androidx.constraintlayout.compose.ConstraintSetRef constraintSet(optional String? name, optional androidx.constraintlayout.compose.ConstraintSetRef? extendConstraintSet, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> constraintSetContent);
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
     method public void customColor(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
+    method public void customColor(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
+    method public void customDistance(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
+    method public void customDistance(androidx.constraintlayout.compose.KeyAttributeScope, String name, float value);
     method public void customFloat(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
+    method public void customFloat(androidx.constraintlayout.compose.KeyAttributeScope, String name, float value);
+    method public void customFontSize(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
+    method public void customFontSize(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
+    method public void customInt(androidx.constraintlayout.compose.ConstrainScope, String name, int value);
+    method public void customInt(androidx.constraintlayout.compose.KeyAttributeScope, String name, int value);
     method public void defaultTransition(androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, optional kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
     method public void transition(optional String? name, androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
   }
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt b/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
new file mode 100644
index 0000000..fc0af6f
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMotionApi::class)
+
+package androidx.constraintlayout.compose.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.Dimension
+import androidx.constraintlayout.compose.ExperimentalMotionApi
+import androidx.constraintlayout.compose.MotionLayout
+import androidx.constraintlayout.compose.MotionLayoutFlag
+import androidx.constraintlayout.compose.MotionScene
+import androidx.constraintlayout.compose.OnSwipe
+import androidx.constraintlayout.compose.SwipeDirection
+import androidx.constraintlayout.compose.SwipeSide
+
+@Preview
+@Composable
+fun CustomColorInKeyAttributesDemo() {
+    val boxId = "box"
+    val textId = "text"
+    MotionLayout(
+        motionScene = MotionScene {
+            val box = createRefFor(boxId)
+            val text = createRefFor(textId)
+            defaultTransition(
+                from = constraintSet {
+                    constrain(text) {
+                        top.linkTo(box.bottom, 10.dp)
+                        start.linkTo(box.start)
+                    }
+                    constrain(box) {
+                        width = Dimension.value(50.dp)
+                        height = Dimension.value(50.dp)
+
+                        centerVerticallyTo(parent)
+                        start.linkTo(parent.start)
+
+                        customColor("background", Color.Red)
+                    }
+                },
+                to = constraintSet {
+                    constrain(text) {
+                        top.linkTo(box.bottom, 10.dp)
+                        end.linkTo(box.end)
+                    }
+                    constrain(box) {
+                        width = Dimension.value(50.dp)
+                        height = Dimension.value(50.dp)
+
+                        centerVerticallyTo(parent)
+                        end.linkTo(parent.end)
+
+                        customColor("background", Color.Blue)
+                    }
+                }
+            ) {
+                >
+                    anchor = box,
+                    side = SwipeSide.Middle,
+                    direction = SwipeDirection.End
+                )
+                keyAttributes(box) {
+                    frame(33) {
+                        customColor("background", Color.Yellow)
+                    }
+                    frame(66) {
+                        customColor("background", Color.Green)
+                    }
+                }
+            }
+        },
+        progress = 0f,
+        motionLayoutFlags = setOf(MotionLayoutFlag.FullMeasure), // Needed to update text composable
+        modifier = Modifier.fillMaxSize()
+    ) {
+        val background = motionColor(boxId, "background")
+        Box(modifier = Modifier.layoutId(boxId).background(background))
+        Text(
+            modifier = Modifier.layoutId(textId),
+            text = "Color: ${background.toArgb().toUInt().toString(16)}"
+        )
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
index b6e1f7d..980a920 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.LocalTextStyle
@@ -29,23 +30,27 @@
 import androidx.compose.material.icons.filled.Person
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.size
+import androidx.compose.ui.unit.sp
 import androidx.constraintlayout.compose.test.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -54,6 +59,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalMotionApi::class)
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 internal class MotionLayoutTest {
@@ -93,6 +99,78 @@
         assertEquals(35.dp.value, usernameSize.width.value, absoluteTolerance = 0.5f)
         assertEquals(17.dp.value, usernameSize.height.value, absoluteTolerance = 0.5f)
     }
+
+    @Test
+    fun testCustomKeyFrameAttributes() {
+        val progress: MutableState<Float> = mutableStateOf(0f)
+        rule.setContent {
+            MotionLayout(
+                motionScene = MotionScene {
+                    val element = createRefFor("element")
+                    defaultTransition(
+                        from = constraintSet {
+                            constrain(element) {
+                                customColor("color", Color.White)
+                                customDistance("distance", 0.dp)
+                                customFontSize("fontSize", 0.sp)
+                                customInt("int", 0)
+                            }
+                        },
+                        to = constraintSet {
+                            constrain(element) {
+                                customColor("color", Color.Black)
+                                customDistance("distance", 10.dp)
+                                customFontSize("fontSize", 20.sp)
+                                customInt("int", 30)
+                            }
+                        }
+                    ) {
+                        keyAttributes(element) {
+                            frame(50) {
+                                customColor("color", Color.Red)
+                                customDistance("distance", 20.dp)
+                                customFontSize("fontSize", 30.sp)
+                                customInt("int", 40)
+                            }
+                        }
+                    }
+                },
+                progress = progress.value,
+                modifier = Modifier.size(200.dp)
+            ) {
+                val props by motionProperties(id = "element")
+                Column(Modifier.layoutId("element")) {
+                    Text(
+                        text = "Color: #${props.color("color").toArgb().toUInt().toString(16)}"
+                    )
+                    Text(
+                        text = "Distance: ${props.distance("distance")}"
+                    )
+                    Text(
+                        text = "FontSize: ${props.fontSize("fontSize")}"
+                    )
+                    Text(
+                        text = "Int: ${props.int("int")}"
+                    )
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        progress.value = 0.25f
+        rule.waitForIdle()
+        rule.onNodeWithText("Color: #ffffbaba").assertExists()
+        rule.onNodeWithText("Distance: 10.0.dp").assertExists()
+        rule.onNodeWithText("FontSize: 15.0.sp").assertExists()
+        rule.onNodeWithText("Int: 20").assertExists()
+
+        progress.value = 0.75f
+        rule.waitForIdle()
+        rule.onNodeWithText("Color: #ffba0000").assertExists()
+        rule.onNodeWithText("Distance: 15.0.dp").assertExists()
+        rule.onNodeWithText("FontSize: 25.0.sp").assertExists()
+        rule.onNodeWithText("Int: 35").assertExists()
+    }
 }
 
 @OptIn(ExperimentalMotionApi::class)
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
index b5b9dc5..7af1fcc 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
@@ -21,6 +21,8 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.constraintlayout.core.state.CorePixelDp
 
@@ -252,6 +254,69 @@
             state.constraints(id).addCustomColor(name, value.toArgb())
         }
     }
+
+    /**
+     * Declare a custom Int [value] addressed by [name].
+     */
+    fun ConstrainScope.customInt(name: String, value: Int) {
+        tasks.add { state ->
+            state.constraints(id).addCustomFloat(name, value.toFloat())
+        }
+    }
+
+    /**
+     * Declare a custom Dp [value] addressed by [name].
+     */
+    fun ConstrainScope.customDistance(name: String, value: Dp) {
+        tasks.add { state ->
+            state.constraints(id).addCustomFloat(name, value.value)
+        }
+    }
+
+    /**
+     * Declare a custom TextUnit [value] addressed by [name].
+     */
+    fun ConstrainScope.customFontSize(name: String, value: TextUnit) {
+        tasks.add { state ->
+            state.constraints(id).addCustomFloat(name, value.value)
+        }
+    }
+
+    /**
+     * Sets the custom Float [value] at the frame of the current [KeyAttributeScope].
+     */
+    fun KeyAttributeScope.customFloat(name: String, value: Float) {
+        customPropertiesValue[name] = value
+    }
+
+    /**
+     * Sets the custom Color [value] at the frame of the current [KeyAttributeScope].
+     */
+    fun KeyAttributeScope.customColor(name: String, value: Color) {
+        // Colors must be in the following format: "#AARRGGBB"
+        customPropertiesValue[name] = "#${value.toArgb().toUInt().toString(16)}"
+    }
+
+    /**
+     * Sets the custom Int [value] at the frame of the current [KeyAttributeScope].
+     */
+    fun KeyAttributeScope.customInt(name: String, value: Int) {
+        customPropertiesValue[name] = value
+    }
+
+    /**
+     * Sets the custom Dp [value] at the frame of the current [KeyAttributeScope].
+     */
+    fun KeyAttributeScope.customDistance(name: String, value: Dp) {
+        customPropertiesValue[name] = value.value
+    }
+
+    /**
+     * Sets the custom TextUnit [value] at the frame of the current [KeyAttributeScope].
+     */
+    fun KeyAttributeScope.customFontSize(name: String, value: TextUnit) {
+        customPropertiesValue[name] = value.value
+    }
 }
 
 data class ConstraintSetRef internal constructor(
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
index 34b560a..b74f5e4 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
@@ -264,18 +264,48 @@
 
 @ExperimentalMotionApi
 abstract class BaseKeyFrameScope internal constructor() {
-    protected val userAttributes = mutableMapOf<String, Any>()
+    /**
+     * PropertyName-Value map for the properties of each type of key frame.
+     *
+     * The values are for a singular unspecified frame.
+     */
+    private val keyFramePropertiesValue = mutableMapOf<String, Any>()
 
+    /**
+     * PropertyName-Value map for user-defined values.
+     *
+     * Typically used on KeyAttributes only.
+     */
+    internal val customPropertiesValue = mutableMapOf<String, Any>()
+
+    /**
+     * When changed, updates the value of type [T] on the [keyFramePropertiesValue] map.
+     *
+     * Where the Key is the property's name unless [nameOverride] is not null.
+     */
     protected fun <T> addOnPropertyChange(initialValue: T, nameOverride: String? = null) =
         object : ObservableProperty<T>(initialValue) {
             override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
                 val name = nameOverride ?: property.name
                 if (newValue != null) {
-                    userAttributes[name] = newValue
+                    keyFramePropertiesValue[name] = newValue
                 }
             }
         }
 
+    /**
+     * Property delegate that updates the [keyFramePropertiesValue] map on value changes.
+     *
+     * Where the Key is the property's name unless [nameOverride] is not null.
+     *
+     * The value is the String given by [NamedPropertyOrValue.name].
+     *
+     * &nbsp;
+     *
+     * Use when declaring properties that have a named value.
+     *
+     * E.g.: `var curveFit: CurveFit? by addNameOnPropertyChange(null)`
+     */
     protected fun <E : NamedPropertyOrValue?> addNameOnPropertyChange(
         initialValue: E,
         nameOverride: String? = null
@@ -284,14 +314,35 @@
             override fun afterChange(property: KProperty<*>, oldValue: E, newValue: E) {
                 val name = nameOverride ?: property.name
                 if (newValue != null) {
-                    userAttributes[name] = newValue.name
+                    keyFramePropertiesValue[name] = newValue.name
                 }
             }
         }
 
-    fun addToContainer(container: CLContainer) {
-        userAttributes.forEach { (name, value) ->
-            val array = container.getArrayOrCreate(name)
+    /**
+     * Adds the property maps to the given container.
+     *
+     * Where every value is treated as part of array.
+     */
+    internal fun addToContainer(container: CLContainer) {
+        container.putValuesAsArrayElements(keyFramePropertiesValue)
+        val customPropsObject = container.getObjectOrNull("custom") ?: run {
+            val custom = CLObject(charArrayOf())
+            container.put("custom", custom)
+            custom
+        }
+        customPropsObject.putValuesAsArrayElements(customPropertiesValue)
+    }
+
+    /**
+     * Adds the values from [propertiesSource] to the [CLContainer].
+     *
+     * Each value will be added as a new element of their corresponding array (given by the Key,
+     * which is the name of the affected property).
+     */
+    private fun CLContainer.putValuesAsArrayElements(propertiesSource: Map<String, Any>) {
+        propertiesSource.forEach { (name, value) ->
+            val array = this.getArrayOrCreate(name)
             when (value) {
                 is String -> {
                     val stringChars = value.toCharArray()
diff --git a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorInflaterTest.java b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorInflaterTest.java
index ca14558..b93a66a 100644
--- a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorInflaterTest.java
+++ b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorInflaterTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
@@ -31,9 +32,7 @@
 import androidx.test.filters.SmallTest;
 
 import org.junit.ClassRule;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 
 @SmallTest
@@ -45,9 +44,6 @@
     @ClassRule
     public static AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule();
 
-    @Rule
-    public final ExpectedException expectedException = ExpectedException.none();
-
     @UiThreadTest
     @Test
     public void testLoadAnimator() {
@@ -122,45 +118,54 @@
 
     @Test
     public void pathInterpolator_wrongControlPoint() {
-        expectedException.expect(InflateException.class);
-        expectedException.expectMessage("requires the controlY1 attribute");
-        AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
-                R.interpolator.wrong_control_point_interpolator);
+        InflateException exception = assertThrows(InflateException.class, () -> {
+            AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+                    R.interpolator.wrong_control_point_interpolator);
+        });
+        assertEquals("pathInterpolator requires the controlY1 attribute", exception.getMessage());
     }
 
     @Test
     public void pathInterpolator_wrongControlPoints() {
-        expectedException.expect(InflateException.class);
-        expectedException.expectMessage("requires both controlX2 and controlY2 for cubic Beziers");
-        AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
-                R.interpolator.wrong_control_points_interpolator);
+
+
+        InflateException exception = assertThrows(InflateException.class, () -> {
+            AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+                    R.interpolator.wrong_control_points_interpolator);
+        });
+        assertEquals("pathInterpolator requires both controlX2 and controlY2 for cubic Beziers.",
+                exception.getMessage());
     }
 
     @Test
     public void pathInterpolator_wrongStartEnd() {
-        expectedException.expect(IllegalArgumentException.class);
-        expectedException.expectMessage("The Path must start at (0,0) and end at (1,1)");
-        AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
-                R.interpolator.wrong_path_interpolator_1);
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+            AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+                    R.interpolator.wrong_path_interpolator_1);
+        });
+        assertEquals("The Path must start at (0,0) and end at (1,1)", exception.getMessage());
     }
 
     @Test
     public void pathInterpolator_loopBack() {
-        expectedException.expect(IllegalArgumentException.class);
-        expectedException.expectMessage("The Path cannot loop back on itself");
-        AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
-                R.interpolator.wrong_path_interpolator_2);
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+            AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+                    R.interpolator.wrong_path_interpolator_2);
+        });
+        assertEquals("The Path cannot loop back on itself.", exception.getMessage());
+
     }
 
     @Test
     public void pathInterpolator_discontinuity() {
-        expectedException.expect(IllegalArgumentException.class);
-        expectedException.expectMessage(Build.VERSION.SDK_INT >= 26
-                ? "The Path cannot have discontinuity in the X axis"
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+            AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+                    R.interpolator.wrong_path_interpolator_3);
+        });
+        assertEquals(Build.VERSION.SDK_INT >= 26
+                ? "The Path cannot have discontinuity in the X axis."
                 // Older APIs don't detect discontinuity, but they report it as loop back.
-                : "The Path cannot loop back on itself");
-        AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
-                R.interpolator.wrong_path_interpolator_3);
+                : "The Path cannot loop back on itself.", exception.getMessage());
     }
 
     static class TestObject {
diff --git a/core/core-location-altitude/api/current.txt b/core/core-location-altitude/api/current.txt
index e6f50d0..a333f75 100644
--- a/core/core-location-altitude/api/current.txt
+++ b/core/core-location-altitude/api/current.txt
@@ -1 +1,9 @@
 // Signature format: 4.0
+package androidx.core.location.altitude {
+
+  public final class AltitudeConverterCompat {
+    method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
+  }
+
+}
+
diff --git a/core/core-location-altitude/api/public_plus_experimental_current.txt b/core/core-location-altitude/api/public_plus_experimental_current.txt
index e6f50d0..a333f75 100644
--- a/core/core-location-altitude/api/public_plus_experimental_current.txt
+++ b/core/core-location-altitude/api/public_plus_experimental_current.txt
@@ -1 +1,9 @@
 // Signature format: 4.0
+package androidx.core.location.altitude {
+
+  public final class AltitudeConverterCompat {
+    method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
+  }
+
+}
+
diff --git a/core/core-location-altitude/api/restricted_current.txt b/core/core-location-altitude/api/restricted_current.txt
index e6f50d0..a333f75 100644
--- a/core/core-location-altitude/api/restricted_current.txt
+++ b/core/core-location-altitude/api/restricted_current.txt
@@ -1 +1,9 @@
 // Signature format: 4.0
+package androidx.core.location.altitude {
+
+  public final class AltitudeConverterCompat {
+    method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
+  }
+
+}
+
diff --git a/core/core-location-altitude/build.gradle b/core/core-location-altitude/build.gradle
index e91770c..c081e08 100644
--- a/core/core-location-altitude/build.gradle
+++ b/core/core-location-altitude/build.gradle
@@ -28,6 +28,16 @@
 
 dependencies {
     api(libs.kotlinStdlib)
+    api("androidx.annotation:annotation:1.5.0")
+
+    implementation("androidx.concurrent:concurrent-futures:1.1.0")
+    implementation("androidx.core:core:1.9.0")
+
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.truth)
 }
 
 androidx {
diff --git a/core/core-location-altitude/src/androidTest/java/androidx/core/location/altitude/AltitudeConverterCompatTest.java b/core/core-location-altitude/src/androidTest/java/androidx/core/location/altitude/AltitudeConverterCompatTest.java
new file mode 100644
index 0000000..3c61ed9
--- /dev/null
+++ b/core/core-location-altitude/src/androidTest/java/androidx/core/location/altitude/AltitudeConverterCompatTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.location.altitude;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.location.Location;
+
+import androidx.arch.core.util.Function;
+import androidx.core.location.LocationCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AltitudeConverterCompatTest {
+
+    private Location mValidLocation;
+    private Context mContext;
+
+    private static void assertEquals(Location actual, Location expected) {
+        assertThat(actual.getLatitude()).isEqualTo(expected.getLatitude());
+        assertThat(actual.getLongitude()).isEqualTo(expected.getLongitude());
+        assertEquals(actual, expected, Location::hasAltitude, Location::getAltitude);
+        assertEquals(
+                actual,
+                expected,
+                LocationCompat::hasVerticalAccuracy,
+                LocationCompat::getVerticalAccuracyMeters);
+        assertEquals(
+                actual, expected, LocationCompat::hasMslAltitude,
+                LocationCompat::getMslAltitudeMeters);
+        assertEquals(
+                actual,
+                expected,
+                LocationCompat::hasMslAltitudeAccuracy,
+                LocationCompat::getMslAltitudeAccuracyMeters);
+    }
+
+    private static <T> void assertEquals(
+            Location actual,
+            Location expected,
+            Function<Location, Boolean> has,
+            Function<Location, T> get) {
+        assertThat(has.apply(actual)).isEqualTo(has.apply(expected));
+        if (has.apply(expected)) {
+            assertThat(get.apply(actual)).isEqualTo(get.apply(expected));
+        }
+    }
+
+    @Before
+    public void setUp() {
+        mValidLocation = new Location("");
+        mValidLocation.setLatitude(-90);
+        mValidLocation.setLongitude(180);
+        mValidLocation.setAltitude(-1);
+        LocationCompat.setVerticalAccuracyMeters(mValidLocation, 1);
+
+        mContext = ApplicationProvider.getApplicationContext();
+    }
+
+    @Test
+    public void testAddMslAltitude_validLocationThrows() {
+        assertThrows(
+                UnsupportedOperationException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(mValidLocation)));
+    }
+
+    @Test
+    public void testAddMslAltitude_invalidLatitudeThrows() {
+        Location location = new Location(mValidLocation);
+
+        location.setLatitude(Double.NaN);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+
+        location.setLatitude(91);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+
+        location.setLatitude(-91);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+    }
+
+    @Test
+    public void testAddMslAltitude_invalidLongitudeThrows() {
+        Location location = new Location(mValidLocation);
+
+        location.setLongitude(Double.NaN);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+
+        location.setLongitude(181);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+
+        location.setLongitude(-181);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+    }
+
+    @Test
+    public void testAddMslAltitude_invalidAltitudeThrows() {
+        Location location = new Location(mValidLocation);
+
+        location.removeAltitude();
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+
+        location.setAltitude(Double.NaN);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+
+        location.setAltitude(Double.POSITIVE_INFINITY);
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+                        new Location(location)));
+    }
+}
diff --git a/core/core-location-altitude/src/main/assets/database/geoids-v0.db b/core/core-location-altitude/src/main/assets/database/geoids-v0.db
new file mode 100644
index 0000000..1be3bd7
--- /dev/null
+++ b/core/core-location-altitude/src/main/assets/database/geoids-v0.db
Binary files differ
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
new file mode 100644
index 0000000..ce18128
--- /dev/null
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.location.altitude;
+
+import android.content.Context;
+import android.location.Location;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Objects;
+
+/**
+ * Converts altitudes reported above the World Geodetic System 1984 (WGS84) reference ellipsoid
+ * into ones above Mean Sea Level.
+ */
+public final class AltitudeConverterCompat {
+
+    private static final double MAX_ABS_VALID_LATITUDE = 90;
+    private static final double MAX_ABS_VALID_LONGITUDE = 180;
+
+    /** Prevents instantiation. */
+    private AltitudeConverterCompat() {
+    }
+
+    /**
+     * Returns a {@link ListenableFuture} that, upon success, adds a Mean Sea Level altitude to the
+     * {@code location} in {@link Location#getExtras()}. In addition, adds a Mean Sea Level altitude
+     * accuracy if the {@code location} has a valid vertical accuracy.
+     *
+     * <p>The {@link ListenableFuture} leaves the {@code location} unchanged if and only if any
+     * of the following are true:
+     *
+     * <ul>
+     *   <li>the {@code location} has an invalid latitude, longitude, or altitude above WGS84
+     *   <li>an I/O error occurs when loading data from raw assets via {@code context}.
+     * </ul>
+     *
+     * <p>NOTE: Currently throws {@link UnsupportedOperationException} for a valid {@code location}.
+     */
+    @NonNull
+    public static ListenableFuture<Location> addMslAltitudeAsync(
+            @NonNull Context context,
+            @NonNull Location location) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(location);
+        validate(location);
+        throw new UnsupportedOperationException("addMslAltitude method not implemented.");
+    }
+
+    /**
+     * Throws an {@link IllegalArgumentException} if the {@code location} has an invalid latitude,
+     * longitude, or altitude above WGS84.
+     */
+    private static void validate(@NonNull Location location) {
+        Preconditions.checkArgument(isFiniteAndAtAbsMost(location.getLatitude(),
+                MAX_ABS_VALID_LATITUDE), "Location must contain a valid latitude.");
+        Preconditions.checkArgument(isFiniteAndAtAbsMost(location.getLongitude(),
+                MAX_ABS_VALID_LONGITUDE), "Location must contain a valid longitude.");
+        Preconditions.checkArgument(location.hasAltitude() && isFinite(location.getAltitude()),
+                "Location must contain a valid altitude above WGS84.");
+    }
+
+    private static boolean isFiniteAndAtAbsMost(double value, double rhs) {
+        return isFinite(value) && Math.abs(value) <= rhs;
+    }
+
+    private static boolean isFinite(double value) {
+        return !Double.isInfinite(value) && !Double.isNaN(value);
+    }
+}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
index 25a66c6..27d75f7 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
@@ -25,10 +25,10 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.core.splashscreen.SplashScreenViewProvider
-import androidx.test.runner.screenshot.Screenshot
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.atomic.AtomicBoolean
 import androidx.core.splashscreen.R as SR
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 
 /**
  * If true, sets an [androidx.core.splashscreen.SplashScreen.OnExitAnimationListener] on the
@@ -124,7 +124,7 @@
                 waitedLatch.countDown()
                 val shouldWait = waitBarrier.get() || waitedLatch.count > 0L
                 if (!shouldWait && takeScreenShot && splashScreenScreenshot == null) {
-                    splashScreenScreenshot = Screenshot.capture().bitmap
+                    splashScreenScreenshot = getInstrumentation().uiAutomation.takeScreenshot()
                 }
                 shouldWait
             }
@@ -141,7 +141,8 @@
                 if (takeScreenShot) {
                     splashScreenViewProvider.view.postDelayed(
                         {
-                            splashScreenViewScreenShot = Screenshot.capture().bitmap
+                            splashScreenViewScreenShot =
+                                getInstrumentation().uiAutomation.takeScreenshot()
                             splashScreenViewProvider.remove()
                             exitAnimationListenerLatch.countDown()
                         },
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestUtils.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestUtils.kt
index 4d9d38d..ab7c012 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestUtils.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestUtils.kt
@@ -27,6 +27,7 @@
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
 import org.hamcrest.core.IsNull
+import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Assert
 import kotlin.reflect.KClass
 
@@ -49,7 +50,7 @@
 
     // Wait for launcher
     val launcherPackage: String = device.launcherPackageName
-    Assert.assertThat(launcherPackage, IsNull.notNullValue())
+    assertThat(launcherPackage, IsNull.notNullValue())
     device.wait(
         Until.hasObject(By.pkg(launcherPackage).depth(0)),
         LAUNCH_TIMEOUT
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
index ba48c48..e84532f 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
@@ -22,11 +22,10 @@
 import android.view.View
 import android.view.ViewTreeObserver
 import androidx.core.splashscreen.SplashScreenViewProvider
+import androidx.test.core.app.takeScreenshot
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.runner.screenshot.ScreenCapture
-import androidx.test.runner.screenshot.Screenshot
 import androidx.test.screenshot.matchers.MSSIMMatcher
 import androidx.test.uiautomator.UiDevice
 import org.junit.Assert.assertEquals
@@ -179,7 +178,7 @@
         }
         assertTrue(controller.drawnLatch.await(2, TimeUnit.SECONDS))
         Thread.sleep(500)
-        val withoutListener = Screenshot.capture()
+        val withoutListener = takeScreenshot()
 
         // Take a screenshot of the container view while the splash screen view is invisible but
         // not removed
@@ -191,7 +190,7 @@
         }
         val withListener = screenshotContainerInExitListener(controller)
 
-        compareBitmaps(withoutListener.bitmap, withListener.bitmap, 0.999)
+        compareBitmaps(withoutListener, withListener, 0.999)
 
         // Execute the same steps as above but with another set of theme attributes to check.
         controller = startActivityWithSplashScreen(SplashScreenStability2::class, device) {
@@ -201,7 +200,7 @@
         }
         controller.waitForActivityDrawn()
         Thread.sleep(500)
-        val withoutListener2 = Screenshot.capture()
+        val withoutListener2 = takeScreenshot()
 
         controller = startActivityWithSplashScreen(SplashScreenStability2::class, device) {
             // Clear out any previous instances
@@ -210,17 +209,17 @@
             it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
         }
         val withListener2 = screenshotContainerInExitListener(controller)
-        compareBitmaps(withListener2.bitmap, withoutListener2.bitmap)
+        compareBitmaps(withListener2, withoutListener2)
     }
 
     private fun screenshotContainerInExitListener(
         controller: SplashScreenTestController
-    ): ScreenCapture {
-        lateinit var contentViewInListener: ScreenCapture
+    ): Bitmap {
+        lateinit var contentViewInListener: Bitmap
         controller.doOnExitAnimation {
             it.view.visibility = View.INVISIBLE
             it.view.postDelayed({
-                contentViewInListener = Screenshot.capture()
+                contentViewInListener = takeScreenshot()
                 it.remove()
                 controller.exitAnimationListenerLatch.countDown()
             }, 100)
@@ -373,4 +372,4 @@
             exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
         )
     }
-}
\ No newline at end of file
+}
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index 41382a3..041e5ab 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -228,7 +228,21 @@
         assertEquals("testSubText", NotificationCompat.getSubText(n));
     }
 
-    @FlakyTest(bugId = 190533219)
+    @SdkSuppress(maxSdkVersion = 15)
+    @Test
+    public void testActionsUnsupported() {
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
+        Notification nWith = builder.addAction(0, "testAction", null).build();
+        // Version prior to API 16 do not support Actions.
+        assertEquals(0, NotificationCompat.getActionCount(nWith));
+        NotificationCompat.Action action = NotificationCompat.getAction(nWith, 0);
+        assertNull(action);
+
+        Notification nWithout = builder.clearActions().build();
+        assertEquals(0, NotificationCompat.getActionCount(nWithout));
+    }
+
+    @SdkSuppress(minSdkVersion = 16)
     @Test
     public void testActions() {
         NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
diff --git a/core/core/src/androidTest/java/androidx/core/content/PermissionCheckerTest.java b/core/core/src/androidTest/java/androidx/core/content/PermissionCheckerTest.java
index 38c1daa..7465cb3 100644
--- a/core/core/src/androidTest/java/androidx/core/content/PermissionCheckerTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/PermissionCheckerTest.java
@@ -23,10 +23,10 @@
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -43,7 +43,7 @@
         mContext = ApplicationProvider.getApplicationContext();
     }
 
-    @FlakyTest(bugId = 242739867)
+    @Ignore // b/242739867
     @Test
     public void testCheckPermission() throws Exception {
         assertEquals(PermissionChecker.PERMISSION_DENIED, PermissionChecker.checkSelfPermission(
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
index 298c8d0..9492383 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
@@ -226,6 +226,7 @@
         testShow(WindowInsetsCompat.Type.statusBars())
     }
 
+    @Ignore // b/244445899
     @Test
     public fun toggle_NavBar() {
         testHide(WindowInsetsCompat.Type.navigationBars())
diff --git a/credentials/credentials-play-services-auth/OWNERS b/credentials/credentials-play-services-auth/OWNERS
new file mode 100644
index 0000000..7b0e47f
--- /dev/null
+++ b/credentials/credentials-play-services-auth/OWNERS
@@ -0,0 +1,8 @@
+# Bug Component: 1218609
+sgjerry@google.com
+helenqin@google.com
+reemabajwa@google.com
+duqinmei@google.com
+beccahughes@google.com
+leecam@google.com
+akaphle@google.com
diff --git a/credentials/credentials-play-services-auth/build.gradle b/credentials/credentials-play-services-auth/build.gradle
index 725338d..430b672 100644
--- a/credentials/credentials-play-services-auth/build.gradle
+++ b/credentials/credentials-play-services-auth/build.gradle
@@ -26,7 +26,22 @@
     api(libs.kotlinStdlib)
     api project(":credentials:credentials")
 
-    // Add dependencies here
+    implementation project(path: ':activity:activity')
+    // Closed source dependencies
+    implementation(libs.playServicesAuth) {
+        exclude group: "androidx.loader"
+        exclude group: "androidx.fragment"
+    }
+    implementation(libs.playServicesFido)
+
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(project(":internal-testutils-truth"))
+    androidTestImplementation(libs.kotlinCoroutinesAndroid)
 }
 
 android {
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index 59ad679..6e59651 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -18,12 +18,16 @@
 
 import android.app.Activity
 import android.os.CancellationSignal
+import android.util.Log
 import androidx.credentials.CreateCredentialRequest
 import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePublicKeyCredentialRequest
 import androidx.credentials.CredentialManagerCallback
 import androidx.credentials.CredentialProvider
 import androidx.credentials.GetCredentialRequest
 import androidx.credentials.GetCredentialResponse
+import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
 import java.util.concurrent.Executor
 
 /**
@@ -32,6 +36,7 @@
  *
  * @hide
  */
+@Suppress("deprecation")
 class CredentialProviderPlayServicesImpl : CredentialProvider {
     override fun onGetCredential(
         request: GetCredentialRequest,
@@ -40,6 +45,10 @@
         executor: Executor,
         callback: CredentialManagerCallback<GetCredentialResponse>
     ) {
+        if (cancellationSignal != null) {
+            Log.i(TAG, "onCreateCredential cancellationSignal not used")
+            TODO("Use Cancel Operations Properly")
+        }
         TODO("Not yet implemented")
     }
 
@@ -50,10 +59,31 @@
         executor: Executor,
         callback: CredentialManagerCallback<CreateCredentialResponse>
     ) {
-        TODO("Not yet implemented")
+        if (cancellationSignal != null) {
+            Log.i(TAG, "onCreateCredential cancellationSignal not used")
+            TODO("Use Cancel Operations Properly")
+        }
+        val fragmentManager: android.app.FragmentManager = activity!!.fragmentManager
+        // TODO("Manage Fragment Lifecycle and Fragment Manager Properly")
+        if (request is CreatePasswordRequest) {
+            CredentialProviderCreatePasswordController.getInstance(
+                fragmentManager).invokePlayServices(
+                request,
+                callback,
+                executor)
+        } else if (request is CreatePublicKeyCredentialRequest) {
+            TODO("Not yet implemented")
+        } else {
+            throw UnsupportedOperationException(
+                "Unsupported request; not password or publickeycredential")
+        }
     }
 
     override fun isAvailableOnDevice(): Boolean {
         TODO("Not yet implemented")
     }
+
+    companion object {
+        private val TAG = CredentialProviderPlayServicesImpl::class.java.name
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
new file mode 100644
index 0000000..994429a
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers.BeginSignIn
+
+import android.content.Intent
+import android.util.Log
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.SignInCredential
+import java.util.concurrent.Executor
+
+/**
+ * A controller to handle the BeginSignIn flow with play services.
+ *
+ * @hide
+ */
+@Suppress("deprecation")
+class CredentialProviderBeginSignInController : CredentialProviderController<
+    GetCredentialRequest,
+    BeginSignInRequest,
+    SignInCredential,
+    GetCredentialResponse>() {
+
+    /**
+     * The callback object state, used in the protected handleResponse method.
+     */
+    private lateinit var callback: CredentialManagerCallback<GetCredentialResponse>
+    /**
+     * The callback requires an executor to invoke it.
+     */
+    private lateinit var executor: Executor
+
+    override fun invokePlayServices(
+        request: GetCredentialRequest,
+        callback: CredentialManagerCallback<GetCredentialResponse>,
+        executor: Executor
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        handleResponse(requestCode, resultCode, data)
+    }
+
+    private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
+        Log.i(TAG, "$uniqueRequestCode $resultCode $data")
+        TODO("Not yet implemented")
+    }
+
+    override fun convertToPlayServices(request: GetCredentialRequest): BeginSignInRequest {
+        TODO("Not yet implemented")
+    }
+
+    override fun convertToCredentialProvider(response: SignInCredential): GetCredentialResponse {
+        TODO("Not yet implemented")
+    }
+
+    companion object {
+        private val TAG = CredentialProviderBeginSignInController::class.java.name
+        private const val REQUEST_CODE_BEGIN_SIGN_IN: Int = 1
+        // TODO("Ensure this works with the lifecycle")
+
+        /**
+         * This finds a past version of the BeginSignInController if it exists, otherwise
+         * it generates a new instance.
+         *
+         * @param fragmentManager a fragment manager pulled from an android activity
+         * @return a credential provider controller for a specific credential request
+         */
+        @JvmStatic
+        fun getInstance(fragmentManager: android.app.FragmentManager):
+            CredentialProviderBeginSignInController {
+            var controller = findPastController(REQUEST_CODE_BEGIN_SIGN_IN, fragmentManager)
+            if (controller == null) {
+                controller = CredentialProviderBeginSignInController()
+                fragmentManager.beginTransaction().add(controller,
+                    REQUEST_CODE_BEGIN_SIGN_IN.toString())
+                    .commitAllowingStateLoss()
+                fragmentManager.executePendingTransactions()
+            }
+            return controller
+        }
+
+        internal fun findPastController(
+            requestCode: Int,
+            fragmentManager: android.app.FragmentManager
+        ): CredentialProviderBeginSignInController? {
+            try {
+                return fragmentManager.findFragmentByTag(requestCode.toString())
+                    as CredentialProviderBeginSignInController?
+            } catch (e: Exception) {
+                Log.i(TAG, "Old fragment found of different type - replacement required")
+                // TODO("Ensure this is well tested for fragment issues")
+                return null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
new file mode 100644
index 0000000..7015a9a
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers.CreatePassword
+
+import android.content.Intent
+import android.util.Log
+import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.auth.api.identity.SavePasswordRequest
+import java.util.concurrent.Executor
+
+/**
+ * A controller to handle the CreatePassword flow with play services.
+ *
+ * @hide
+ */
+@Suppress("deprecation")
+class CredentialProviderCreatePasswordController : CredentialProviderController<
+        CreatePasswordRequest,
+        SavePasswordRequest,
+        Intent,
+        CreateCredentialResponse>() {
+
+    /**
+     * The callback object state, used in the protected handleResponse method.
+     */
+    private lateinit var callback: CredentialManagerCallback<CreateCredentialResponse>
+
+    /**
+     * The callback requires an executor to invoke it.
+     */
+    private lateinit var executor: Executor
+
+    override fun invokePlayServices(
+        request: CreatePasswordRequest,
+        callback: CredentialManagerCallback<CreateCredentialResponse>,
+        executor: Executor
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        handleResponse(requestCode, resultCode, data)
+    }
+
+    private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
+        Log.i(TAG, "$uniqueRequestCode $resultCode $data")
+        TODO("Not yet implemented")
+    }
+
+    override fun convertToPlayServices(request: CreatePasswordRequest): SavePasswordRequest {
+        TODO("Not yet implemented")
+    }
+
+    override fun convertToCredentialProvider(response: Intent): CreateCredentialResponse {
+        TODO("Not yet implemented")
+    }
+
+    companion object {
+        private val TAG = CredentialProviderCreatePasswordController::class.java.name
+        private const val REQUEST_CODE_GIS_SAVE_PASSWORD: Int = 1
+        // TODO("Ensure this works with the lifecycle")
+
+        /**
+         * This finds a past version of the
+         * [CredentialProviderCreatePasswordController] if it exists, otherwise
+         * it generates a new instance.
+         *
+         * @param fragmentManager a fragment manager pulled from an android activity
+         * @return a credential provider controller for CreatePublicKeyCredential
+         */
+        @JvmStatic
+        fun getInstance(fragmentManager: android.app.FragmentManager):
+            CredentialProviderCreatePasswordController {
+            var controller = findPastController(REQUEST_CODE_GIS_SAVE_PASSWORD, fragmentManager)
+            if (controller == null) {
+                controller = CredentialProviderCreatePasswordController()
+                fragmentManager.beginTransaction().add(controller,
+                    REQUEST_CODE_GIS_SAVE_PASSWORD.toString())
+                    .commitAllowingStateLoss()
+                fragmentManager.executePendingTransactions()
+            }
+            return controller
+        }
+
+        internal fun findPastController(
+            requestCode: Int,
+            fragmentManager: android.app.FragmentManager
+        ): CredentialProviderCreatePasswordController? {
+            try {
+                return fragmentManager.findFragmentByTag(requestCode.toString())
+                    as CredentialProviderCreatePasswordController
+            } catch (e: Exception) {
+                Log.i(TAG, "Old fragment found of different type - replacement required")
+                // TODO("Ensure this is well tested for fragment issues")
+                return null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
new file mode 100644
index 0000000..d6e8763
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers.CreatePublicKeyCredential
+
+import android.content.Intent
+import android.util.Log
+import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
+import java.util.concurrent.Executor
+
+/**
+ * A controller to handle the CreatePublicKeyCredential flow with play services.
+ *
+ * @hide
+ */
+@Suppress("deprecation")
+class CredentialProviderCreatePublicKeyCredentialController :
+        CredentialProviderController<
+            CreatePublicKeyCredentialRequest,
+            PublicKeyCredentialCreationOptions,
+            PublicKeyCredential,
+            CreateCredentialResponse>() {
+
+    /**
+     * The callback object state, used in the protected handleResponse method.
+     */
+    private lateinit var callback: CredentialManagerCallback<CreateCredentialResponse>
+
+    /**
+     * The callback requires an executor to invoke it.
+     */
+    private lateinit var executor: Executor
+
+    override fun invokePlayServices(
+        request: CreatePublicKeyCredentialRequest,
+        callback: CredentialManagerCallback<CreateCredentialResponse>,
+        executor: Executor
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        handleResponse(requestCode, resultCode, data)
+    }
+
+    private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
+        Log.i(TAG, "$uniqueRequestCode $resultCode $data")
+        TODO("Not yet implemented")
+    }
+
+    override fun convertToPlayServices(request: CreatePublicKeyCredentialRequest):
+        PublicKeyCredentialCreationOptions {
+        TODO("Not yet implemented")
+    }
+
+    override fun convertToCredentialProvider(response: PublicKeyCredential):
+        CreateCredentialResponse {
+        TODO("Not yet implemented")
+    }
+
+    companion object {
+        private val TAG = CredentialProviderCreatePublicKeyCredentialController::class.java.name
+        private const val REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL: Int = 1
+        // TODO("Ensure this works with the lifecycle")
+
+        /**
+         * This finds a past version of the
+         * [CredentialProviderCreatePublicKeyCredentialController] if it exists, otherwise
+         * it generates a new instance.
+         *
+         * @param fragmentManager a fragment manager pulled from an android activity
+         * @return a credential provider controller for CreatePublicKeyCredential
+         */
+        @JvmStatic
+        fun getInstance(fragmentManager: android.app.FragmentManager):
+            CredentialProviderCreatePublicKeyCredentialController {
+            var controller = findPastController(
+                REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL,
+                fragmentManager)
+            if (controller == null) {
+                controller = CredentialProviderCreatePublicKeyCredentialController()
+                fragmentManager.beginTransaction().add(controller,
+                    REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL.toString())
+                    .commitAllowingStateLoss()
+                fragmentManager.executePendingTransactions()
+            }
+            return controller
+        }
+
+        internal fun findPastController(
+            requestCode: Int,
+            fragmentManager: android.app.FragmentManager
+        ): CredentialProviderCreatePublicKeyCredentialController? {
+            try {
+                return fragmentManager.findFragmentByTag(requestCode.toString())
+                    as CredentialProviderCreatePublicKeyCredentialController
+            } catch (e: Exception) {
+                Log.i(TAG, "Old fragment found of different type - replacement required")
+                // TODO("Ensure this is well tested for fragment issues")
+                return null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
new file mode 100644
index 0000000..12ab060
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers
+
+import androidx.credentials.CredentialManagerCallback
+import java.util.concurrent.Executor
+
+/**
+ * Extensible abstract class for credential controllers. Please implement this class per every
+ * request/response credential type. Unique logic is left to the use case of the implementation.
+ * If you are building your own version as an OEM, the below can be mimicked to your own
+ * credential provider equivalent and whatever internal service you invoke.
+ *
+ * @param T1 the credential request type from credential manager
+ * @param T2 the credential request type converted to play services
+ * @param R2 the credential response type from play services
+ * @param R1 the credential response type converted back to that used by credential manager
+ *
+ * @hide
+ */
+@Suppress("deprecation")
+abstract class CredentialProviderController<T1 : Any, T2 : Any, R2 : Any, R1 : Any> : android.app
+        .Fragment() {
+
+    /**
+     * Invokes the flow that starts retrieving credential data. In this use case, we invoke
+     * play service modules.
+     *
+     * @param request a credential provider request
+     * @param callback a credential manager callback with a credential provider response
+     * @param executor to be used in any multi-threaded operation calls, such as listenable futures
+     */
+    abstract fun invokePlayServices(
+        request: T1,
+        callback: CredentialManagerCallback<R1>,
+        executor: Executor
+    )
+
+    /**
+     * Allows converting from a credential provider request to a play service request.
+     *
+     * @param request a credential provider request
+     * @return a play service request
+     */
+    protected abstract fun convertToPlayServices(request: T1): T2
+
+    /**
+     * Allows converting from a play service response to a credential provider response.
+     *
+     * @param response a play service response
+     * @return a credential provider response
+     */
+    protected abstract fun convertToCredentialProvider(response: R2): R1
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
index c0c51a0..0be5f0c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
@@ -55,7 +55,7 @@
      */
     fun onGetCredential(
         request: GetCredentialRequest,
-        activity: Activity?,
+        activity: Activity?, // TODO("Update on optionality")
         cancellationSignal: CancellationSignal?,
         executor: Executor,
         callback: CredentialManagerCallback<GetCredentialResponse>,
diff --git a/datastore/datastore-core-okio/build.gradle b/datastore/datastore-core-okio/build.gradle
index 5353b43..3f9c98a 100644
--- a/datastore/datastore-core-okio/build.gradle
+++ b/datastore/datastore-core-okio/build.gradle
@@ -27,7 +27,9 @@
 def enableNative = KmpPlatformsKt.enableNative(project)
 
 androidXMultiplatform {
-    jvm()
+    jvm() {
+        withJava()
+    }
     mac()
     linux()
     ios()
diff --git a/datastore/datastore-core/build.gradle b/datastore/datastore-core/build.gradle
index 26a1921..b5194bd 100644
--- a/datastore/datastore-core/build.gradle
+++ b/datastore/datastore-core/build.gradle
@@ -27,7 +27,9 @@
 def enableNative = KmpPlatformsKt.enableNative(project)
 
 androidXMultiplatform {
-    jvm()
+    jvm() {
+        withJava()
+    }
     mac()
     linux()
     ios()
diff --git a/datastore/datastore-multiprocess/build.gradle b/datastore/datastore-multiprocess/build.gradle
index 3d340db..e59a2cf 100644
--- a/datastore/datastore-multiprocess/build.gradle
+++ b/datastore/datastore-multiprocess/build.gradle
@@ -59,9 +59,6 @@
 }
 
 android {
-    defaultConfig {
-        minSdkVersion 19
-    }
     externalNativeBuild {
         cmake {
             path "src/main/cpp/CMakeLists.txt"
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt
index 8199310..f8b2f2e 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt
+++ b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt
@@ -60,6 +60,7 @@
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.withContext
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
@@ -244,6 +245,7 @@
         }
     }
 
+    @Ignore("b/244352517")
     @Test
     fun testReadFromNonExistentFile() = runTest {
         val nonExistentFile = tempFolder.newFile()
@@ -997,4 +999,4 @@
             throw IOException("Handler thrown exception.")
         }
     }
-}
\ No newline at end of file
+}
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
index 3dc1b02..99af5fe 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
@@ -27,6 +27,7 @@
 import java.io.FileNotFoundException
 import java.io.FileOutputStream
 import java.io.IOException
+import java.nio.channels.FileLock
 import java.util.concurrent.Semaphore
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.coroutineContext
@@ -139,6 +140,8 @@
     private val VERSION_SUFFIX = ".version"
     private val BUG_MESSAGE = "This is a bug in DataStore. Please file a bug at: " +
         "https://issuetracker.google.com/issues/new?component=907884&template=1466542"
+    // TODO(b/255419657): update the shared lock IOException handling logic
+    private val LOCK_ERROR_MESSAGE = "fcntl failed: EAGAIN"
     private val INVALID_VERSION = -1
     private var initTasks: List<suspend (api: InitializerApi<T>) -> Unit>? =
         initTasksList.toList()
@@ -527,11 +530,14 @@
         // semaphore to make the best use of coroutine
         ThreadLock.acquire(threadLockSemaphore).use {
             FileOutputStream(lockFile).use { lockFileStream ->
-                lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
-                    .use { _ ->
-                        val data = block()
-                        return Data(data, data.hashCode(), sharedCounter.getValue())
-                    }
+                var lock: FileLock? = null
+                try {
+                    lock = lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+                    val data = block()
+                    return Data(data, data.hashCode(), sharedCounter.getValue())
+                } finally {
+                    lock?.release()
+                }
             }
         }
     }
@@ -544,10 +550,30 @@
                 return block(false)
             }
             FileInputStream(lockFile).use { lockFileStream ->
-                lockFileStream.getChannel().tryLock(0L, Long.MAX_VALUE, /* shared= */ true)
-                    .use { lock ->
-                        return block(lock != null)
+                var lock: FileLock? = null
+                try {
+                    try {
+                        lock = lockFileStream.getChannel().tryLock(
+                            /* position= */ 0L,
+                            /* size= */ Long.MAX_VALUE,
+                            /* shared= */ true)
+                    } catch (ex: IOException) {
+                        // TODO(b/255419657): Update the shared lock IOException handling logic for
+                        // KMM.
+
+                        // Some platforms / OS do not support shared lock and convert shared lock
+                        // requests to exclusive lock requests. If the lock can't be acquired, it
+                        // will throw an IOException with EAGAIN error, instead of returning null as
+                        // specified in {@link FileChannel#tryLock}. We only continue if the error
+                        // message is EAGAIN, otherwise just throw it.
+                        if (ex.message?.startsWith(LOCK_ERROR_MESSAGE) != true) {
+                            throw ex
+                        }
                     }
+                    return block(lock != null)
+                } finally {
+                    lock?.release()
+                }
             }
         }
     }
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
index e01662d..1442640 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
@@ -75,11 +75,15 @@
 
         internal fun create(enableMlock: Boolean = true, produceFile: () -> File): SharedCounter {
             val file = produceFile()
-            return ParcelFileDescriptor.open(
-                file,
-                ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
-            ).use {
-                createCounterFromFd(it, enableMlock)
+            var pfd: ParcelFileDescriptor? = null
+            try {
+                pfd = ParcelFileDescriptor.open(
+                    file,
+                    ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
+                )
+                return createCounterFromFd(pfd, enableMlock)
+            } finally {
+                pfd?.close()
             }
         }
     }
diff --git a/datastore/datastore-preferences-core/api/current.txt b/datastore/datastore-preferences-core/api/current.txt
index f76fc6f..cdc92af 100644
--- a/datastore/datastore-preferences-core/api/current.txt
+++ b/datastore/datastore-preferences-core/api/current.txt
@@ -62,5 +62,13 @@
     method public static suspend Object? edit(androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>, kotlin.jvm.functions.Function2<? super androidx.datastore.preferences.core.MutablePreferences,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> transform, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>);
   }
 
+  public final class PreferencesSerializer implements androidx.datastore.core.okio.OkioSerializer<androidx.datastore.preferences.core.Preferences> {
+    method public androidx.datastore.preferences.core.Preferences getDefaultValue();
+    method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+    method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? writeTo(androidx.datastore.preferences.core.Preferences t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+    property public androidx.datastore.preferences.core.Preferences defaultValue;
+    field public static final androidx.datastore.preferences.core.PreferencesSerializer INSTANCE;
+  }
+
 }
 
diff --git a/datastore/datastore-preferences-core/api/public_plus_experimental_current.txt b/datastore/datastore-preferences-core/api/public_plus_experimental_current.txt
index f76fc6f..cdc92af 100644
--- a/datastore/datastore-preferences-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-preferences-core/api/public_plus_experimental_current.txt
@@ -62,5 +62,13 @@
     method public static suspend Object? edit(androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>, kotlin.jvm.functions.Function2<? super androidx.datastore.preferences.core.MutablePreferences,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> transform, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>);
   }
 
+  public final class PreferencesSerializer implements androidx.datastore.core.okio.OkioSerializer<androidx.datastore.preferences.core.Preferences> {
+    method public androidx.datastore.preferences.core.Preferences getDefaultValue();
+    method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+    method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? writeTo(androidx.datastore.preferences.core.Preferences t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+    property public androidx.datastore.preferences.core.Preferences defaultValue;
+    field public static final androidx.datastore.preferences.core.PreferencesSerializer INSTANCE;
+  }
+
 }
 
diff --git a/datastore/datastore-preferences-core/api/restricted_current.txt b/datastore/datastore-preferences-core/api/restricted_current.txt
index f76fc6f..cdc92af 100644
--- a/datastore/datastore-preferences-core/api/restricted_current.txt
+++ b/datastore/datastore-preferences-core/api/restricted_current.txt
@@ -62,5 +62,13 @@
     method public static suspend Object? edit(androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>, kotlin.jvm.functions.Function2<? super androidx.datastore.preferences.core.MutablePreferences,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> transform, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>);
   }
 
+  public final class PreferencesSerializer implements androidx.datastore.core.okio.OkioSerializer<androidx.datastore.preferences.core.Preferences> {
+    method public androidx.datastore.preferences.core.Preferences getDefaultValue();
+    method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+    method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? writeTo(androidx.datastore.preferences.core.Preferences t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+    property public androidx.datastore.preferences.core.Preferences defaultValue;
+    field public static final androidx.datastore.preferences.core.PreferencesSerializer INSTANCE;
+  }
+
 }
 
diff --git a/datastore/datastore-preferences-core/build.gradle b/datastore/datastore-preferences-core/build.gradle
index 539d5a9..20f30bf 100644
--- a/datastore/datastore-preferences-core/build.gradle
+++ b/datastore/datastore-preferences-core/build.gradle
@@ -29,7 +29,9 @@
 def enableNative = KmpPlatformsKt.enableNative(project)
 
 androidXMultiplatform {
-    jvm()
+    jvm() {
+        withJava()
+    }
     mac()
     linux()
     ios()
diff --git a/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/Expect.kt b/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/Expect.kt
index 6c4ab296..92c0407 100644
--- a/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/Expect.kt
+++ b/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/Expect.kt
@@ -16,7 +16,6 @@
 
 package androidx.datastore.preferences.core
 
-import androidx.datastore.core.okio.OkioSerializer
 import kotlinx.coroutines.CoroutineDispatcher
 
 internal expect fun <K, V> immutableMap(map: Map<K, V>): Map<K, V>
@@ -29,5 +28,3 @@
     fun set(value: Boolean)
     fun get(): Boolean
 }
-
-internal expect fun getPreferencesSerializer(): OkioSerializer<Preferences>
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java b/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
similarity index 60%
rename from room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java
rename to datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
index 4e95b3d..d6f5aba 100644
--- a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java
+++ b/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
@@ -14,15 +14,13 @@
  * limitations under the License.
  */
 
-package androidx.room;
+package androidx.datastore.preferences.core
 
-import kotlin.coroutines.Continuation;
-import kotlin.jvm.functions.Function1;
+import androidx.datastore.core.okio.OkioSerializer
 
-public class RoomDatabaseKt {
-    public static final <R> Object withTransaction(RoomDatabase db,
-            Function1<? super Continuation<? super R>, ? extends Object> block,
-            Continuation<? super R> cont) {
-        return null;
-    }
-}
\ No newline at end of file
+/**
+ * Proto based serializer for Preferences. Can be used to manually create
+ * [DataStore][androidx.datastore.core.DataStore] using the
+ * [DataStoreFactory#create][androidx.datastore.core.DataStoreFactory.create] function.
+ */
+expect object PreferencesSerializer : OkioSerializer<Preferences>
diff --git a/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesCompatibilityTest.kt b/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesCompatibilityTest.kt
index 6c696a0..8ec132a 100644
--- a/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesCompatibilityTest.kt
+++ b/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesCompatibilityTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.datastore.preferences.core
 
-import androidx.datastore.core.okio.OkioSerializer
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlinx.coroutines.test.runTest
@@ -36,7 +35,6 @@
             "gBCgwKBm15TG9uZxICIAEKGQoIbXlTdHJpbmcSDSoLc3RyaW5nVmFsdWUKDwoJbXlCb29sZWFuEgIIAQo" +
             "bCgtteVN0cmluZ1NldBIMMgoKA29uZQoDdHdvChMKC215Qnl0ZUFycmF5EgRCAgEC"
         val byteString = protoBase64.decodeBase64() ?: throw Exception("Unable to decode")
-        val preferencesSerializer: OkioSerializer<Preferences> = getPreferencesSerializer()
         val expectedProto = preferencesOf(
             Preferences.Pair(floatPreferencesKey("myFloat"), 1.1f),
             Preferences.Pair(doublePreferencesKey("myDouble"), 1.1),
@@ -50,7 +48,7 @@
 
         val protoBuffer = Buffer()
         protoBuffer.write(byteString)
-        val protoPrefsFromBytes = preferencesSerializer.readFrom(protoBuffer)
+        val protoPrefsFromBytes = PreferencesSerializer.readFrom(protoBuffer)
         assertEquals(expectedProto, protoPrefsFromBytes)
     }
 }
\ No newline at end of file
diff --git a/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerTest.kt b/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
index 185d14f..aa06488 100644
--- a/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
+++ b/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
@@ -36,7 +36,7 @@
     private val testIO = OkioTestIO()
 
     private lateinit var testFile: OkioPath
-    private val preferencesSerializer: OkioSerializer<Preferences> = getPreferencesSerializer()
+    private val preferencesSerializer: OkioSerializer<Preferences> = PreferencesSerializer
     private val fileSystem: FileSystem = FileSystem.SYSTEM
 
     @BeforeTest
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt b/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt
index 89dd3e5..aaef346 100644
--- a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt
+++ b/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt
@@ -20,7 +20,6 @@
 
 import androidx.annotation.RestrictTo
 
-import androidx.datastore.core.okio.OkioSerializer
 import java.util.Collections
 import java.util.concurrent.atomic.AtomicBoolean
 import kotlinx.coroutines.CoroutineDispatcher
@@ -46,7 +45,3 @@
         delegate = AtomicBoolean(initialValue)
     }
 }
-
-internal actual fun getPreferencesSerializer(): OkioSerializer<Preferences> {
-    return PreferencesSerializer
-}
\ No newline at end of file
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt b/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt
similarity index 90%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
rename to datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt
index f92d0c8..c413194 100644
--- a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
+++ b/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt
@@ -29,10 +29,12 @@
 import okio.IOException
 
 /**
- * Proto based serializer for Preferences.
+ * Proto based serializer for Preferences. Can be used to manually create
+ * [DataStore][androidx.datastore.core.DataStore] using the
+ * [DataStoreFactory#create][androidx.datastore.core.DataStoreFactory.create] function.
  */
-internal object PreferencesSerializer : OkioSerializer<Preferences> {
-    val fileExtension = "preferences_pb"
+actual object PreferencesSerializer : OkioSerializer<Preferences> {
+    internal const val fileExtension = "preferences_pb"
 
     override val defaultValue: Preferences
         get() {
@@ -52,6 +54,7 @@
         return mutablePreferences.toPreferences()
     }
 
+    @Suppress("InvalidNullabilityOverride") // Remove after b/232460179 is fixed
     @Throws(IOException::class, CorruptionException::class)
     override suspend fun writeTo(t: Preferences, sink: BufferedSink) {
         val preferences = t.asMap()
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt b/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
index fa08b90..eed8f1f 100644
--- a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
+++ b/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
@@ -37,7 +37,7 @@
     private val testIO = OkioTestIO()
 
     private lateinit var testFile: OkioPath
-    private val preferencesSerializer: OkioSerializer<Preferences> = getPreferencesSerializer()
+    private val preferencesSerializer: OkioSerializer<Preferences> = PreferencesSerializer
     private val fileSystem: FileSystem = FileSystem.SYSTEM
 
     @BeforeTest
diff --git a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/Actual.native.kt b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/Actual.native.kt
index ed2de2f..7945474 100644
--- a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/Actual.native.kt
+++ b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/Actual.native.kt
@@ -16,7 +16,6 @@
 
 package androidx.datastore.preferences.core
 
-import androidx.datastore.core.okio.OkioSerializer
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
@@ -42,7 +41,3 @@
 
 // TODO(b/234049307): Pick a better dispatcher for IO
 internal actual fun ioDispatcher(): CoroutineDispatcher = Dispatchers.Default
-
-internal actual fun getPreferencesSerializer(): OkioSerializer<Preferences> {
-    return PreferencesSerializationSerializer
-}
\ No newline at end of file
diff --git a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.native.kt b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.native.kt
index 74e553f2..7f69d8f 100644
--- a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.native.kt
+++ b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.native.kt
@@ -53,11 +53,11 @@
         produceFile: () -> Path
     ): DataStore<Preferences> {
         val delegate = create(
-            storage = OkioStorage(FileSystem.SYSTEM, PreferencesSerializationSerializer) {
+            storage = OkioStorage(FileSystem.SYSTEM, PreferencesSerializer) {
                 val file = produceFile()
-                check(file.name.endsWith(".${PreferencesSerializationSerializer.fileExtension}")) {
+                check(file.name.endsWith(".${PreferencesSerializer.fileExtension}")) {
                     "File extension for file: $file does not match required extension for" +
-                        " Preferences file: ${PreferencesSerializationSerializer.fileExtension}"
+                        " Preferences file: ${PreferencesSerializer.fileExtension}"
                 }
                 file
             },
diff --git a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializationSerializer.kt b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.native.kt
similarity index 92%
rename from datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializationSerializer.kt
rename to datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.native.kt
index b27e26f..55e28c3 100644
--- a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializationSerializer.kt
+++ b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.native.kt
@@ -18,26 +18,30 @@
 
 import androidx.datastore.core.CorruptionException
 import androidx.datastore.core.okio.OkioSerializer
-import okio.BufferedSink
-import okio.BufferedSource
+import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
 import kotlinx.serialization.decodeFromByteArray
 import kotlinx.serialization.encodeToByteArray
 import kotlinx.serialization.protobuf.ProtoBuf
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.SerializationException
+import okio.BufferedSink
+import okio.BufferedSource
 
+/**
+ * Proto based serializer for Preferences. Can be used to manually create
+ * [DataStore][androidx.datastore.core.DataStore] using the
+ * [DataStoreFactory#create][androidx.datastore.core.DataStoreFactory.create] function.
+ */
 @OptIn(ExperimentalSerializationApi::class)
-internal object PreferencesSerializationSerializer : OkioSerializer<Preferences> {
-    val fileExtension = "preferences_pb"
+actual object PreferencesSerializer : OkioSerializer<Preferences> {
+    internal const val fileExtension = "preferences_pb"
 
     override val defaultValue: Preferences
         get() = emptyPreferences()
 
     override suspend fun readFrom(source: BufferedSource): Preferences {
         val prefMap: PreferencesMap = try {
-            ProtoBuf.decodeFromByteArray<PreferencesMap>(
-                source.readByteArray())
+            ProtoBuf.decodeFromByteArray(source.readByteArray())
         } catch (e: SerializationException) {
             throw CorruptionException("Unable to parse preferences proto.", e)
         }
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 6f1bd5f..81afb45 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -23,10 +23,8 @@
 # > Configure project :
 updated local\.properties
 # > Configure project :compose:test\-utils
+# https://youtrack.jetbrains.com/issue/KT-48436
 The following Kotlin source sets were configured but not added to any Kotlin compilation:
-\* iosArm[0-9]+Main
-\* iosSimulatorArm[0-9]+Main
-\* iosX[0-9]+Main
 \* androidAndroidTestDebug
 \* androidAndroidTestRelease
 \* androidTestFixtures
@@ -76,13 +74,14 @@
 Html results of .* zipped into.*\.zip
 # b/230127926
 [0-9]+ problem.* found storing the configuration cache.*
-\- Task `:[:A-Za-z0-9#\-]+` of type `org\.jetbrains\.kotlin\.gradle\.plugin\.mpp\.[A-Za-z0-9]+`: invocation of 'Task\.project' at execution time is unsupported\.
-# https://youtrack.jetbrains.com/issue/KT-52694/
-\- Task `:listTaskOutputs` of type `androidx\.build\.ListTaskOutputsTask`: invocation of 'Task\.project' at execution time is unsupported\.
-See https://docs\.gradle\.org/[0-9]+\.[0-9]+.*/userguide/configuration_cache\.html\#config_cache:requirements:use_project_during_execution
-# https://youtrack.jetbrains.com/issue/KT-52694
-\- Task \`:[:A-Za-z0-9#\-]+` of type \`org\.jetbrains\.kotlin\.gradle\.tooling\.BuildKotlinToolingMetadataTask\$FromKotlinExtension\`\: invocation of \'Task\.project\' at execution time is unsupported\.
 plus [0-9]+ more problems\. Please see the report for details\.
+# https://youtrack.jetbrains.com/issue/KT-43293 fixed in Kotlin 1.8.0
+\- Task \`((\:[a-z1-9-]+)+)?\:[a-zA-Z1-9]+\` of type \`org\.jetbrains(\.[a-zA-Z]+)+\.(CInteropCommonizerTask|KotlinNativeCompile|KotlinNativeLink|KotlinNativeHostTest|CInteropMetadataDependencyTransformationTask|GenerateProjectStructureMetadata|TransformKotlinGranularMetadata|NativeDistributionCommonizerTask)\`\: invocation of \'Task\.project\' at execution time is unsupported\.
+\- Task \`((\:[a-z1-9-]+)+)?\:[a-zA-Z1-9]+\` of type \`org\.jetbrains\.kotlin\.gradle\.targets\.native\.internal\.(CInteropMetadataDependencyTransformationTask|NativeDistributionCommonizerTask)\`: cannot serialize object of type \'org\.gradle(\.[a-zA-Z]+)+\'\, a subtype of \'org\.gradle(\.[a-zA-Z]+)+\'\, as these are not supported with the configuration cache\.
+# https://youtrack.jetbrains.com/issue/KT-54627
+\- Task \`\:commonizeNativeDistribution\` of type \`org\.jetbrains\.kotlin\.gradle\.targets\.native\.internal\.NativeDistributionCommonizerTask\`\: error writing value of type \'java\.util\.concurrent\.locks\.ReentrantLock\'
+See https://docs\.gradle\.org/[0-9]+\.[0-9]+.*/userguide/configuration_cache\.html\#config_cache:requirements:use_project_during_execution
+See https\:\/\/docs\.gradle\.org\/[0-9]+\.[0-9]+\/userguide\/configuration_cache\.html\#config_cache\:requirements\:disallowed_types
 See the complete report at file://\$SUPPORT/build/reports/configuration\-cache/[^/]*/[^/]*/configuration\-cache\-report\.html
 See the complete report at file://\$OUT_DIR/androidx/build/reports/configuration\-cache/[^ ]*/[^ ]*/configuration\-cache\-report\.html
 # > Task :compose:ui:ui:processDebugAndroidTestManifest
diff --git a/development/offlinifyDocs/style.css b/development/offlinifyDocs/style.css
new file mode 100644
index 0000000..c360538
--- /dev/null
+++ b/development/offlinifyDocs/style.css
@@ -0,0 +1 @@
+@charset "UTF-8";body[theme=android-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] .devsite-book-nav-bg:after,body[theme=android-theme][layout=docs]{background-color:#e8eaed}body[theme=android-theme][layout=docs] .devsite-article{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-border-radius:2px;border-radius:2px}body[theme=android-theme][layout=docs] .devsite-landing-row:first-child{-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0}body[theme=android-theme][layout=docs] .devsite-landing-row:last-child{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px}body[theme=android-theme] devsite-header .devsite-top-logo-row,body[theme=android-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper,body[theme=android-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper:before,body[theme=android-theme] devsite-header cloudx-tabs-nav.upper-tabs .devsite-tabs-wrapper,body[theme=android-theme] devsite-header devsite-tabs.upper-tabs .devsite-tabs-wrapper,body[theme=android-theme] devsite-header devsite-tabs.upper-tabs tab[overflow-tab]:after{background:#fff}body[theme=android-theme] devsite-header .devsite-header-billboard{background-color:#f7f9fa}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-header .devsite-breadcrumb-link{color:rgba(0,0,0,.65);fill:rgba(0,0,0,.65)}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:hover,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:hover{color:rgba(0,0,0,.87)}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path{fill:rgba(0,0,0,.87)}body[theme=android-theme] devsite-header .devsite-product-description-row{color:#202124}body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link{color:#202124;fill:#202124}body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:hover,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:hover{color:rgba(32,33,36,.7);fill:rgba(32,33,36,.7)}body[theme=android-theme] devsite-header .devsite-header-icon-button{color:rgba(0,0,0,.65)}body[theme=android-theme] devsite-header .devsite-header-icon-button:active,body[theme=android-theme] devsite-header .devsite-header-icon-button:focus,body[theme=android-theme] devsite-header .devsite-header-icon-button:hover{color:rgba(0,0,0,.87)}body[theme=android-theme] devsite-header .devsite-top-button{color:rgba(32,33,36,.7)}body[theme=android-theme] devsite-header .devsite-top-button:focus,body[theme=android-theme] devsite-header .devsite-top-button:hover{background-color:hsla(0,0%,80%,.15);color:#202124}body[theme=android-theme] devsite-header .devsite-top-button:active{background-color:hsla(0,0%,80%,.3);color:#202124}body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn{background:transparent;color:#1a73e8}body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:active,body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:focus,body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:hover,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:active,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:focus,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:hover{background:transparent;border:0;-webkit-box-shadow:none;box-shadow:none;color:#1967d2}body[theme=android-theme] devsite-header .devsite-header-link,body[theme=android-theme] devsite-header .devsite-header-link:visited,body[theme=android-theme] devsite-header .devsite-settings-kabob,body[theme=android-theme] devsite-header .devsite-settings-kabob:visited{color:#1a73e8}body[theme=android-theme] devsite-header .devsite-header-link:focus,body[theme=android-theme] devsite-header .devsite-header-link:hover,body[theme=android-theme] devsite-header .devsite-settings-kabob:focus,body[theme=android-theme] devsite-header .devsite-settings-kabob:hover{color:#1967d2}body[theme=android-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,body[theme=android-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,body[theme=android-theme] devsite-header .devsite-header--loading:after,body[theme=android-theme] devsite-header .devsite-header--loading span:after{background-color:#3ddc84}body[theme=android-theme] devsite-toc>.devsite-nav-list{border-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-cta:not([background]){background-color:#3ddc84;color:#fff}body[theme=android-theme] .devsite-feedback-item-icon-color{background-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-cta.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,body[theme=android-theme] .devsite-landing-row-cta.devsite-landing-row h2{color:#fff}body[theme=android-theme] .devsite-search-project .devsite-product-logo-container,body[theme=android-theme] devsite-header .devsite-product-logo-container{color:#3ddc84}body[theme=android-theme] .devsite-search-project .devsite-product-logo-container[background],body[theme=android-theme] devsite-header .devsite-product-logo-container[background]{background:#3ddc84;color:#fff}body[theme=android-theme] devsite-header .devsite-product-logo{color:inherit}body[theme=android-theme] .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-theme] .devsite-landing-row-item-icon-container[foreground=theme],body[theme=android-theme] .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-theme] .devsite-landing-row :focus .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-theme] .devsite-landing-row :link .devsite-landing-row-item-icon-container[background][foreground=theme]:hover,body[theme=android-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=theme],body[theme=android-theme] :link .devsite-landing-row-item-list-item-icon-container[background][foreground=theme]:hover{color:#3ddc84}body[theme=android-theme] .devsite-collapsible-section{background-color:#f7f9fa}body[theme=android-theme] cloudx-tabs-nav.lower-tabs a,body[theme=android-theme] devsite-tabs.lower-tabs a{color:rgba(32,33,36,.7)}body[theme=android-theme] cloudx-tabs-nav.lower-tabs a:focus,body[theme=android-theme] cloudx-tabs-nav.lower-tabs a:hover,body[theme=android-theme] cloudx-tabs-nav.lower-tabs tab[active]>a,body[theme=android-theme] devsite-tabs.lower-tabs a:focus,body[theme=android-theme] devsite-tabs.lower-tabs a:hover,body[theme=android-theme] devsite-tabs.lower-tabs tab[active]>a{color:#202124}body[theme=android-theme] cloudx-tabs-nav.lower-tabs tab[active]>a:after,body[theme=android-theme] devsite-tabs.lower-tabs tab[active]>a:after{background:#3ddc84}body[theme=android-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-item-description,body[theme=android-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-title,body[theme=android-theme] devsite-tabs tab[dropdown] .devsite-nav-item-description,body[theme=android-theme] devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368}body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a,body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:visited,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124}body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:focus,body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:hover,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab>a,body[theme=android-theme] devsite-tabs.upper-tabs tab>a{color:#5f6368}body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu,body[theme=android-theme] devsite-tabs.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-theme] devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:focus,body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:hover,body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab>a:focus,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab>a:hover,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab[active]>a{color:#202124}body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a:after,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab[active]>a:after{background:#3ddc84}body[theme=android-theme] devsite-user .devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=android-theme] devsite-user .devsite-user-dialog .devsite-user-dialog-photo{background:#3ddc84;color:#fff}body[theme=android-theme] .devsite-landing-row-item-custom-image:not([background]),body[theme=android-theme] [background=theme]{background-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-item[foreground=theme] :link h2,body[theme=android-theme] .devsite-landing-row-item[foreground=theme] :link h3,body[theme=android-theme] [foreground=theme] .button,body[theme=android-theme] [foreground=theme] :focus>:not(.material-icons),body[theme=android-theme] [foreground=theme] :link>:not(.material-icons):hover,body[theme=android-theme] [foreground=theme] a:not(.button),body[theme=android-theme] [foreground=theme] a:not(.button) h2,body[theme=android-theme] [foreground=theme] a:not(.button) h3{color:#3ddc84}body[theme=android-theme] [foreground=theme] .button:active,body[theme=android-theme] [foreground=theme] .button:focus,body[theme=android-theme] [foreground=theme] .button:hover{background:#ecfcf3;color:#689f38}body[theme=android-theme] [foreground=theme] .button-primary{background:#3ddc84;color:#fff}body[theme=android-theme] [foreground=theme] .button-primary:active,body[theme=android-theme] [foreground=theme] .button-primary:focus,body[theme=android-theme] [foreground=theme] .button-primary:hover{background:#689f38;color:#fff}body[theme=android-theme] [background=theme] .devsite-landing-row-description,body[theme=android-theme] [background=theme] .devsite-landing-row-item-list-item-icon-container:not([foreground]),body[theme=android-theme] [background=theme] :link .devsite-landing-row-item-list-item-description h4+p,body[theme=android-theme] [background=theme] :link:not(.button),body[theme=android-theme] [background=theme]:not(.devsite-landing-row-cards) .button-white,body[theme=android-theme] [background=theme]:not(.devsite-landing-row-cards) h3,body[theme=android-theme] [background=theme]:not([foreground]):not(.devsite-landing-row-cards),body[theme=android-theme] [background=theme] :visited:not(.button),body[theme=android-theme] [background=theme] h2,body[theme=android-theme] [background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white{color:#fff}body[theme=android-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-theme] :link .devsite-landing-row-item-list-item-icon-container[foreground=theme]:hover,body[theme=android-theme] [background=theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,body[theme=android-theme] [background=theme] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}body[theme=android-theme] devsite-content .devsite-404-header h3,body[theme=android-theme] devsite-content .devsite-offline-header h3{color:#689f38}body[theme=android-theme] devsite-header .devsite-search-background,body[theme=android-theme] devsite-header devsite-search .devsite-searchbox:before{background:#fff}body[theme=android-theme] devsite-header .devsite-header-billboard-search .devsite-search-background,body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-searchbox:before{background:#f7f9fa}body[theme=android-theme] devsite-header .devsite-search-background:after,body[theme=android-theme] devsite-header[search-active] .devsite-search-background:after{background:#f1f3f4}body[theme=android-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-theme] devsite-header devsite-search .devsite-search-image,body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-image{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field{background:#f1f3f4}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field:hover{background:#e8eaed}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field,body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field:hover{background:#f1f3f4;color:#202124}body[theme=android-theme] devsite-book-nav .devsite-mobile-header{background:#fff;border-bottom:1px solid #dadce0}@media screen and (max-width:840px){body[theme=android-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] devsite-header .devsite-search-background,body[theme=android-theme] devsite-header .devsite-search-background:after,body[theme=android-theme] devsite-header[search-active] .devsite-search-background:after,body[theme=android-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-theme] devsite-header devsite-search .devsite-search-field:hover{background:0}body[theme=android-theme][layout=docs] .devsite-article{-webkit-border-radius:0;border-radius:0}}body[theme=android-ndk-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] .devsite-book-nav-bg:after,body[theme=android-ndk-theme][layout=docs]{background-color:#e8eaed}body[theme=android-ndk-theme][layout=docs] .devsite-article{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-border-radius:2px;border-radius:2px}body[theme=android-ndk-theme][layout=docs] .devsite-landing-row:first-child{-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0}body[theme=android-ndk-theme][layout=docs] .devsite-landing-row:last-child{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper:before,body[theme=android-ndk-theme] devsite-header cloudx-tabs-nav.upper-tabs .devsite-tabs-wrapper,body[theme=android-ndk-theme] devsite-header devsite-tabs.upper-tabs .devsite-tabs-wrapper,body[theme=android-ndk-theme] devsite-header devsite-tabs.upper-tabs tab[overflow-tab]:after{background:#fff}body[theme=android-ndk-theme] devsite-header .devsite-header-billboard{background-color:#24c1e0}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link{color:rgba(0,0,0,.65);fill:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path{fill:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-product-description-row{color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link{color:#fff;fill:#fff}body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:hover{color:hsla(0,0%,100%,.7);fill:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button{color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:active,body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-top-button{color:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] devsite-header .devsite-top-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-top-button:hover{background-color:hsla(0,0%,80%,.15);color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-top-button:active{background-color:hsla(0,0%,80%,.3);color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn{background:transparent;color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:active,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:hover,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:active,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:focus,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:hover{background:transparent;border:0;-webkit-box-shadow:none;box-shadow:none;color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-header-link,body[theme=android-ndk-theme] devsite-header .devsite-header-link:visited,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:visited{color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-header-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-header-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:focus,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,body[theme=android-ndk-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,body[theme=android-ndk-theme] devsite-header .devsite-header--loading:after,body[theme=android-ndk-theme] devsite-header .devsite-header--loading span:after{background-color:#24c1e0}body[theme=android-ndk-theme] devsite-toc>.devsite-nav-list{border-color:#24c1e0}body[theme=android-ndk-theme] .devsite-landing-row-cta:not([background]){background-color:#24c1e0;color:#202124}body[theme=android-ndk-theme] .devsite-feedback-item-icon-color{background-color:#24c1e0}body[theme=android-ndk-theme] .devsite-landing-row-cta.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,body[theme=android-ndk-theme] .devsite-landing-row-cta.devsite-landing-row h2{color:#202124}body[theme=android-ndk-theme] .devsite-search-project .devsite-product-logo-container,body[theme=android-ndk-theme] devsite-header .devsite-product-logo-container{color:#24c1e0}body[theme=android-ndk-theme] .devsite-search-project .devsite-product-logo-container[background],body[theme=android-ndk-theme] devsite-header .devsite-product-logo-container[background]{background:#24c1e0;color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-product-logo{color:inherit}body[theme=android-ndk-theme] .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row-item-icon-container[foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row :focus .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row :link .devsite-landing-row-item-icon-container[background][foreground=theme]:hover,body[theme=android-ndk-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] :link .devsite-landing-row-item-list-item-icon-container[background][foreground=theme]:hover{color:#24c1e0}body[theme=android-ndk-theme] .devsite-collapsible-section{background-color:#24c1e0}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a{color:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a:hover,body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs tab[active]>a,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a:focus,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a:hover,body[theme=android-ndk-theme] devsite-tabs.lower-tabs tab[active]>a{color:#fff}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs tab[active]>a:after,body[theme=android-ndk-theme] devsite-tabs.lower-tabs tab[active]>a:after{background:#fff}body[theme=android-ndk-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-item-description,body[theme=android-ndk-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-title,body[theme=android-ndk-theme] devsite-tabs tab[dropdown] .devsite-nav-item-description,body[theme=android-ndk-theme] devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368}body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a,body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:visited,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124}body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:hover,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab>a,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab>a{color:#5f6368}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:hover,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab>a:focus,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab>a:hover,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab[active]>a{color:#202124}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a:after,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab[active]>a:after{background:#129eaf}body[theme=android-ndk-theme] devsite-user .devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=android-ndk-theme] devsite-user .devsite-user-dialog .devsite-user-dialog-photo{background:#78d9ec;color:#202124}body[theme=android-ndk-theme] .devsite-landing-row-item-custom-image:not([background]),body[theme=android-ndk-theme] [background=theme]{background-color:#78d9ec}body[theme=android-ndk-theme] .devsite-landing-row-item[foreground=theme] :link h2,body[theme=android-ndk-theme] .devsite-landing-row-item[foreground=theme] :link h3,body[theme=android-ndk-theme] [foreground=theme] .button,body[theme=android-ndk-theme] [foreground=theme] :focus>:not(.material-icons),body[theme=android-ndk-theme] [foreground=theme] :link>:not(.material-icons):hover,body[theme=android-ndk-theme] [foreground=theme] a:not(.button),body[theme=android-ndk-theme] [foreground=theme] a:not(.button) h2,body[theme=android-ndk-theme] [foreground=theme] a:not(.button) h3{color:#24c1e0}body[theme=android-ndk-theme] [foreground=theme] .button:active,body[theme=android-ndk-theme] [foreground=theme] .button:focus,body[theme=android-ndk-theme] [foreground=theme] .button:hover{background:#e9f9fc;color:#129eaf}body[theme=android-ndk-theme] [foreground=theme] .button-primary{background:#24c1e0;color:#202124}body[theme=android-ndk-theme] [foreground=theme] .button-primary:active,body[theme=android-ndk-theme] [foreground=theme] .button-primary:focus,body[theme=android-ndk-theme] [foreground=theme] .button-primary:hover{background:#129eaf;color:#202124}body[theme=android-ndk-theme] [background=theme] .devsite-landing-row-description,body[theme=android-ndk-theme] [background=theme] .devsite-landing-row-item-list-item-icon-container:not([foreground]),body[theme=android-ndk-theme] [background=theme] :link .devsite-landing-row-item-list-item-description h4+p,body[theme=android-ndk-theme] [background=theme] :link:not(.button),body[theme=android-ndk-theme] [background=theme]:not(.devsite-landing-row-cards) .button-white,body[theme=android-ndk-theme] [background=theme]:not(.devsite-landing-row-cards) h3,body[theme=android-ndk-theme] [background=theme]:not([foreground]):not(.devsite-landing-row-cards),body[theme=android-ndk-theme] [background=theme] :visited:not(.button),body[theme=android-ndk-theme] [background=theme] h2,body[theme=android-ndk-theme] [background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white{color:#202124}body[theme=android-ndk-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-ndk-theme] :link .devsite-landing-row-item-list-item-icon-container[foreground=theme]:hover,body[theme=android-ndk-theme] [background=theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,body[theme=android-ndk-theme] [background=theme] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}body[theme=android-ndk-theme] devsite-content .devsite-404-header h3,body[theme=android-ndk-theme] devsite-content .devsite-offline-header h3{color:#129eaf}body[theme=android-ndk-theme] devsite-header .devsite-search-background,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-searchbox:before{background:#fff}body[theme=android-ndk-theme] devsite-header .devsite-header-billboard-search .devsite-search-background,body[theme=android-ndk-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-searchbox:before{background:#24c1e0}body[theme=android-ndk-theme] devsite-header .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header[search-active] .devsite-search-background:after{background:#f1f3f4}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-image,body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-image{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field{background:#f1f3f4}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:hover{background:#e8eaed}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field:hover{background:#f1f3f4;color:#202124}body[theme=android-ndk-theme] devsite-book-nav .devsite-mobile-header{background:#fff;border-bottom:1px solid #dadce0}@media screen and (max-width:840px){body[theme=android-ndk-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] devsite-header .devsite-search-background,body[theme=android-ndk-theme] devsite-header .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header[search-active] .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:hover{background:0}body[theme=android-ndk-theme][layout=docs] .devsite-article{-webkit-border-radius:0;border-radius:0}}.devsite-site-logo{width:134px}body,dd,div,dl,figure,form,img,input,menu{margin:0;padding:0}body[no-overflow]{overflow:hidden}iframe{border:0}iframe:not([src]){display:none}.caution>:first-child,.dogfood>:first-child,.key-point>:first-child,.key-term>:first-child,.note>:first-child,.objective>:first-child,.quickstart-left>:first-child,.quickstart-wide>:first-child,.special>:first-child,.success>:first-child,.warning>:first-child,aside>:first-child,blockquote>:first-child,dd>:first-child,li>p:first-child,td>.devsite-table-wrapper:first-child>table,td>.expandable:first-child>:nth-child(2),td>:first-child,td>:first-child>li:first-child{margin-top:0}.caution>:last-child,.dogfood>:last-child,.key-point>:last-child,.key-term>:last-child,.note>:last-child,.objective>:last-child,.quickstart-left>:last-child,.quickstart-wide>:last-child,.special>:last-child,.success>:last-child,.warning>:last-child,aside>:last-child,blockquote>:last-child,dd>:last-child,li>p:last-child,td>.devsite-table-wrapper:last-child>table,td>.expandable:last-child>:last-child,td>:last-child,td>:last-child>li:last-child{margin-bottom:0}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}.clearfix:after,.quickstart-step:after{clear:both;content:"";display:table;height:0;visibility:hidden}body,html{color:#202124;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;height:100%;margin:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}body[sitemask--active]{overflow:hidden}p{margin:16px 0;padding:0}img,video{border:0;height:auto;max-width:100%}table img{max-width:272px}:link,:visited{color:#1a73e8;outline:0;text-decoration:none}a:focus{text-decoration:underline}a:focus img{-webkit-filter:brightness(75%);filter:brightness(75%)}.devsite-toast-content :link,.devsite-toast-content :visited{color:#fff;text-decoration:underline}.devsite-toast-content a:focus{background:hsla(0,0%,100%,.3);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}sup{line-height:1}dd,ol,ul{margin:0;padding-left:40px}td>dl>dd,td>ol,td>ul{padding-left:20px}ol{list-style:decimal outside}ol ol{list-style-type:lower-alpha}ol ol ol{list-style-type:lower-roman}ol.upper-alpha{list-style-type:upper-alpha}ul{list-style:disc outside}li,li p{margin:12px 0;padding:0}dt{font:700 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}blockquote,dd,dt{margin:16px 0}blockquote{background:#f1f3f4;padding:8px;quotes:none}hr{background:#e8eaed;border:0;height:1px;margin:16px 0;width:100%}abbr,acronym{border-bottom:1px dotted #5f6368;cursor:help}address,cite,dfn,em{font-style:italic}strong{font-weight:700}[visually-hidden]{opacity:0!important;pointer-events:none!important;visibility:hidden!important}.hidden,[hidden]{display:none!important}[render-hidden]{display:inline!important;position:absolute!important;visibility:hidden!important}[no-scroll]{overflow:hidden}#app-progress{left:0;position:fixed;right:0;top:0;z-index:1011}.devsite-article .material-icons{vertical-align:bottom}.devsite-article-body .material-icons:not(:link),[type=landing] .devsite-article .material-icons:not(:link){cursor:default}.footnotes ol{padding-left:16px}.footnotes li{font:400 13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}#qv-wrapper,#table-of-contents,#tb-wrapper,.inline-toc,div.toc:not(.class):not(.group):not(.type):not(.interface),h2#contents,h2.toc,h3#contents,h3.toc,ol.toc,section.toc,ul.toc{display:none}@media screen and (max-width:840px){#app-progress{z-index:1014}}.no-feedback devsite-feedback{display:none!important}.preserve-case{text-transform:none}a.external:after,a[href*=man7\.org]:after,a[href*=oracle\.com]:after{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"open_in_new";font-size:18px;margin:0;vertical-align:text-bottom}[dir=ltr] a.external:after,[dir=ltr] a[href*=man7\.org]:after,[dir=ltr] a[href*=oracle\.com]:after{margin-left:4px}[dir=rtl] a.external:after,[dir=rtl] a[href*=man7\.org]:after,[dir=rtl] a[href*=oracle\.com]:after{margin-right:4px;-webkit-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}a.download:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"file_download";display:inline-block;font-size:18px;margin:0;vertical-align:text-bottom}[dir=ltr] a.download:before{margin-right:4px}[dir=rtl] a.download:before{margin-left:4px}devsite-content{counter-reset:numbered}h2.numbered{line-height:48px;margin-top:60px;padding-bottom:19px}h2.numbered:before{background:#bdc1c6;-webkit-border-radius:50%;border-radius:50%;color:#fff;content:counter(numbered);counter-increment:numbered;display:inline-block;height:48px;line-height:48px;margin:0 20px 0 0;text-align:center;width:48px}[dir=rtl] h2.numbered:before{margin:0 0 0 20px}.compare-better,.compare-no,.compare-worse,.compare-yes{font-weight:700}.compare-better:before,.compare-blank:before,.compare-no:before,.compare-worse:before,.compare-yes:before{content:"";display:inline-block;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;word-wrap:normal;margin:-4px 4px 0 0;text-transform:none;vertical-align:middle;width:24px}[dir=rtl] .compare-better:before,[dir=rtl] .compare-blank:before,[dir=rtl] .compare-no:before,[dir=rtl] .compare-worse:before,[dir=rtl] .compare-yes:before{margin:-4px 0 0 4px}.compare-better:before{color:#34a853;content:"thumb_up"}.compare-no:before{color:#dd2c00;content:"not_interested"}.compare-worse:before{color:#dd2c00;content:"thumb_down"}.compare-yes:before{color:#34a853;content:"check"}.align-center{text-align:center}.align-right{text-align:right}.hanging-indent,.members.function td:first-child{padding-left:25px;text-indent:-17px}[dir=rtl] .hanging-indent,[dir=rtl] .members.function td:first-child{padding-left:0;padding-right:25px}.bad-table{table-layout:fixed}.bad-table td,.bad-table tr{word-wrap:break-word}.bad-table pre{word-wrap:normal}.screenshot{border:1px solid #e8eaed;padding:3px}.columns td,.columns th,.columns tr{background:0;border:0;font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0}[dir=ltr] .columns td,[dir=ltr] .columns th{padding-right:20px}[dir=rtl] .columns td,[dir=rtl] .columns th{padding-left:20px}.columns th{color:#202124;font-weight:500}.columns code,.columns pre{background:#f1f3f4}.inline:not(.expandable){display:inline}.inline-block{display:inline-block}.block{display:block}img.inline-icon{height:1.2em;vertical-align:sub}.no-select{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.attempt-left,.attempt-right,aside.attempt-left,aside.attempt-right{max-width:-webkit-calc((100% - 40px)/2);max-width:calc((100% - 40px)/2)}.attempt-left,.video-wrapper-left,aside.attempt-left{float:left;margin:0 40px 40px 0}.attempt-right,.video-wrapper,[dir=rtl] .attempt-left,[dir=rtl] .video-wrapper-left,[dir=rtl] aside.attempt-left,aside.attempt-right{float:right;margin:0 0 40px 40px}[dir=rtl] .attempt-right,[dir=rtl] .video-wrapper,[dir=rtl] aside.attempt-right{float:left;margin:0 40px 40px 0}.attempt-left+.attempt-right,.attempt-left+.video-wrapper,.video-wrapper-left+.attempt-right,.video-wrapper-left+.video-wrapper,[dir=rtl] .attempt-left+.attempt-right,[dir=rtl] .attempt-left+.video-wrapper,[dir=rtl] .video-wrapper-left+.attempt-right,[dir=rtl] .video-wrapper-left+.video-wrapper{margin:0 0 40px}.video-wrapper,.video-wrapper-full-width{overflow:hidden;position:relative}.video-wrapper,.video-wrapper-left{width:-webkit-calc((100% - 40px)/2);width:calc((100% - 40px)/2)}.video-wrapper-full-width{margin:16px 0;width:100%}.video-wrapper-full-width embed,.video-wrapper-full-width iframe,.video-wrapper-full-width object,.video-wrapper-left embed,.video-wrapper-left iframe,.video-wrapper-left object,.video-wrapper embed,.video-wrapper iframe,.video-wrapper object{height:101%;left:-.5%;position:absolute;top:-.5%;width:101%}@media screen and (max-width:840px){.attempt-left,.attempt-right,aside.attempt-left,aside.attempt-right{display:block;max-width:100%}.attempt-left,.attempt-right,.video-wrapper,.video-wrapper-left,aside.attempt-left,aside.attempt-right{float:none;margin:16px 0;width:100%}}body[pending] #gc-wrapper{margin-top:0!important}body[ready] #gc-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-wrapper{min-height:100vh}body[ready] .devsite-wrapper{min-height:100%;overflow:hidden}@supports ((display:-webkit-flex) or (display:flex)){body[ready] .devsite-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}.full-bleed{margin:0;padding:0}.devsite-book-nav-bg,devsite-book-nav{width:268px}body[pending] devsite-book-nav{position:absolute}@media screen and (max-width:840px){body[devsite-book-nav--open]{overflow:hidden}body[devsite-book-nav--open] devsite-book-nav[fixed]{-webkit-transform:translateZ(0)!important;transform:translateZ(0)!important}}body devsite-toc.devsite-toc{-ms-grid-column:5;grid-column:3;-ms-grid-row:1;grid-row:1;margin:24px 24px 0 0;min-width:0;width:auto}[dir=rtl] body devsite-toc.devsite-toc{margin:24px 0 0 24px}body devsite-toc>.devsite-nav-list{width:auto}.devsite-main-content{margin:0 auto;position:relative;width:100%;z-index:1003}#contain-402{z-index:1004!important}body[pending] .devsite-main-content{min-height:-webkit-calc(100vh - 456px);min-height:calc(100vh - 456px)}body[ready] .devsite-main-content{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;min-height:auto}body[layout=full] .devsite-main-content{max-width:1600px;padding:40px 80px}body[layout=full][type=error] .devsite-main-content{max-width:1600px;padding:0 80px}body[layout=full][type=landing] .devsite-main-content{max-width:none;padding:0}body[layout=docs] .devsite-main-content{display:-ms-grid;display:grid;grid-gap:24px;-ms-grid-columns:1fr 24px minmax(752px,936px) 24px 1fr;grid-template-columns:1fr minmax(752px,936px) 1fr;-ms-grid-rows:1fr;grid-template-rows:1fr}body[layout=docs] .devsite-main-content[has-toc]{-ms-grid-columns:1fr 24px minmax(752px,936px) 24px minmax(160px,1fr);grid-template-columns:1fr minmax(752px,936px) minmax(160px,1fr)}body[layout=docs] .devsite-main-content[has-book-nav]{-ms-grid-columns:minmax(268px,1fr) 24px minmax(752px,936px) 24px 1fr;grid-template-columns:minmax(268px,1fr) minmax(752px,936px) 1fr}body[layout=docs] .devsite-main-content[has-book-nav][has-toc]{-ms-grid-columns:minmax(268px,1fr) 24px minmax(752px,936px) 24px minmax(160px,1fr);grid-template-columns:minmax(268px,1fr) minmax(752px,936px) minmax(160px,1fr)}.devsite-main-content[has-book-nav]~.devsite-footer,.devsite-main-content[has-book-nav]~devsite-footer-promos,.devsite-main-content[has-book-nav]~devsite-footer-utility{margin:0 0 0 268px}[dir=rtl] .devsite-main-content[has-book-nav]~.devsite-footer,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility{margin:0 268px 0 0}@media screen and (max-width:840px){.devsite-main-content[has-book-nav]~.devsite-footer,.devsite-main-content[has-book-nav]~devsite-footer-promos,.devsite-main-content[has-book-nav]~devsite-footer-utility{margin-left:0}[dir=rtl] .devsite-main-content[has-book-nav]~.devsite-footer,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility{margin-right:0}}@media screen and (max-width:1252px){body[layout=docs] .devsite-main-content[has-toc]{-ms-grid-columns:1fr 24px minmax(752px,936px) 24px 1fr;grid-template-columns:1fr minmax(752px,936px) 1fr}body[layout=docs] .devsite-main-content[has-book-nav],body[layout=docs] .devsite-main-content[has-book-nav][has-toc]{-ms-grid-columns:268px 24px 1fr 24px;grid-template-columns:268px 1fr 0}}@media screen and (max-width:840px){body[layout=full] .devsite-main-content{padding:24px}body[layout=full][type=error] .devsite-main-content{padding:0 24px}body[layout=docs] .devsite-main-content{display:block;min-width:100%}devsite-content-footer{padding:0 24px}}@media screen and (max-width:600px){body[layout=full] .devsite-main-content{padding:16px}body[layout=full][type=error] .devsite-main-content,devsite-content-footer{padding:0 16px}}.devsite-icon:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}.devsite-icon-arrow-drop-down:before{content:"arrow_drop_down"}.devsite-icon-code:before{content:"code"}.devsite-icon-code-dark:before,.devsite-icon-code-light:before{content:"brightness_medium"}.devsite-icon-copy:before{content:"content_copy"}aside{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] aside{padding-left:60px}[dir=rtl] aside{padding-right:60px}body[layout=full]:not([type=landing]) aside{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) aside{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) aside{padding-right:calc(50vw - 50% + 36px)}aside:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] aside:before{float:left;margin-left:-36px}[dir=rtl] aside:before{float:right;margin-right:-36px}aside :link,aside :visited{text-decoration:underline}aside a:focus,aside a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}aside pre{background:hsla(0,0%,100%,.75)}aside code{font-weight:700;padding:0}.caution{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .caution{padding-left:60px}[dir=rtl] .caution{padding-right:60px}body[layout=full]:not([type=landing]) .caution{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .caution{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .caution{padding-right:calc(50vw - 50% + 36px)}.caution:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .caution:before{float:left;margin-left:-36px}[dir=rtl] .caution:before{float:right;margin-right:-36px}.caution :link,.caution :visited{text-decoration:underline}.caution a:focus,.caution a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.caution pre{background:hsla(0,0%,100%,.75)}.caution code{font-weight:700;padding:0}.dogfood{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .dogfood{padding-left:60px}[dir=rtl] .dogfood{padding-right:60px}body[layout=full]:not([type=landing]) .dogfood{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .dogfood{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .dogfood{padding-right:calc(50vw - 50% + 36px)}.dogfood:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .dogfood:before{float:left;margin-left:-36px}[dir=rtl] .dogfood:before{float:right;margin-right:-36px}.dogfood :link,.dogfood :visited{text-decoration:underline}.dogfood a:focus,.dogfood a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.dogfood pre{background:hsla(0,0%,100%,.75)}.dogfood code{font-weight:700;padding:0}.key-point{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .key-point{padding-left:60px}[dir=rtl] .key-point{padding-right:60px}body[layout=full]:not([type=landing]) .key-point{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .key-point{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .key-point{padding-right:calc(50vw - 50% + 36px)}.key-point:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .key-point:before{float:left;margin-left:-36px}[dir=rtl] .key-point:before{float:right;margin-right:-36px}.key-point :link,.key-point :visited{text-decoration:underline}.key-point a:focus,.key-point a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.key-point pre{background:hsla(0,0%,100%,.75)}.key-point code{font-weight:700;padding:0}.key-term{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .key-term{padding-left:60px}[dir=rtl] .key-term{padding-right:60px}body[layout=full]:not([type=landing]) .key-term{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .key-term{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .key-term{padding-right:calc(50vw - 50% + 36px)}.key-term:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .key-term:before{float:left;margin-left:-36px}[dir=rtl] .key-term:before{float:right;margin-right:-36px}.key-term :link,.key-term :visited{text-decoration:underline}.key-term a:focus,.key-term a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.key-term pre{background:hsla(0,0%,100%,.75)}.key-term code{font-weight:700;padding:0}.note{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .note{padding-left:60px}[dir=rtl] .note{padding-right:60px}body[layout=full]:not([type=landing]) .note{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .note{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .note{padding-right:calc(50vw - 50% + 36px)}.note:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .note:before{float:left;margin-left:-36px}[dir=rtl] .note:before{float:right;margin-right:-36px}.note :link,.note :visited{text-decoration:underline}.note a:focus,.note a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.note pre{background:hsla(0,0%,100%,.75)}.note code{font-weight:700;padding:0}.objective{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .objective{padding-left:60px}[dir=rtl] .objective{padding-right:60px}body[layout=full]:not([type=landing]) .objective{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .objective{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .objective{padding-right:calc(50vw - 50% + 36px)}.objective:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .objective:before{float:left;margin-left:-36px}[dir=rtl] .objective:before{float:right;margin-right:-36px}.objective :link,.objective :visited{text-decoration:underline}.objective a:focus,.objective a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.objective pre{background:hsla(0,0%,100%,.75)}.objective code{font-weight:700;padding:0}.special{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .special{padding-left:60px}[dir=rtl] .special{padding-right:60px}body[layout=full]:not([type=landing]) .special{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .special{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .special{padding-right:calc(50vw - 50% + 36px)}.special:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .special:before{float:left;margin-left:-36px}[dir=rtl] .special:before{float:right;margin-right:-36px}.special :link,.special :visited{text-decoration:underline}.special a:focus,.special a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.special pre{background:hsla(0,0%,100%,.75)}.special code{font-weight:700;padding:0}.success{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .success{padding-left:60px}[dir=rtl] .success{padding-right:60px}body[layout=full]:not([type=landing]) .success{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .success{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .success{padding-right:calc(50vw - 50% + 36px)}.success:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .success:before{float:left;margin-left:-36px}[dir=rtl] .success:before{float:right;margin-right:-36px}.success :link,.success :visited{text-decoration:underline}.success a:focus,.success a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.success pre{background:hsla(0,0%,100%,.75)}.success code{font-weight:700;padding:0}.warning{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .warning{padding-left:60px}[dir=rtl] .warning{padding-right:60px}body[layout=full]:not([type=landing]) .warning{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .warning{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .warning{padding-right:calc(50vw - 50% + 36px)}.warning:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .warning:before{float:left;margin-left:-36px}[dir=rtl] .warning:before{float:right;margin-right:-36px}.warning :link,.warning :visited{text-decoration:underline}.warning a:focus,.warning a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.warning pre{background:hsla(0,0%,100%,.75)}.warning code{font-weight:700;padding:0}@media screen and (max-width:600px){body[layout=full] aside{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] aside{padding-left:76px}[dir=rtl] body[layout=full] aside{padding-right:76px}body[layout=full] .caution{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .caution{padding-left:76px}[dir=rtl] body[layout=full] .caution{padding-right:76px}body[layout=full] .dogfood{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .dogfood{padding-left:76px}[dir=rtl] body[layout=full] .dogfood{padding-right:76px}body[layout=full] .key-point{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .key-point{padding-left:76px}[dir=rtl] body[layout=full] .key-point{padding-right:76px}body[layout=full] .key-term{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .key-term{padding-left:76px}[dir=rtl] body[layout=full] .key-term{padding-right:76px}body[layout=full] .note{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .note{padding-left:76px}[dir=rtl] body[layout=full] .note{padding-right:76px}body[layout=full] .objective{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .objective{padding-left:76px}[dir=rtl] body[layout=full] .objective{padding-right:76px}body[layout=full] .special{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .special{padding-left:76px}[dir=rtl] body[layout=full] .special{padding-right:76px}body[layout=full] .success{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .success{padding-left:76px}[dir=rtl] body[layout=full] .success{padding-right:76px}body[layout=full] .warning{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .warning{padding-left:76px}[dir=rtl] body[layout=full] .warning{padding-right:76px}}aside var{background:inherit;font-weight:700;padding:0}.note,.note :link,.note :visited,.note code,.special,.special :link,.special :visited,.special code,aside,aside :link,aside :visited,aside code{background:#e1f5fe;color:#01579b}.note:before,.special:before,aside:before{content:"star"}.caution,.caution :link,.caution :visited,.caution code{background:#feefe3;color:#bf360c}.caution:before{content:"error"}.dogfood,.dogfood :link,.dogfood :visited,.dogfood code{background:#eceff1;color:#546e7a}.dogfood:before{content:"pets"}.key-point,.key-point :link,.key-point :visited,.key-point code{background:#e8eaf6;color:#3f51b5}.key-point:before{content:"lightbulb_outline"}.key-term,.key-term :link,.key-term :visited,.key-term code{background:#f3e8fd;color:#9334e6}.key-term:before{content:"font_download"}.objective,.objective :link,.objective :visited,.objective code,.success,.success :link,.success :visited,.success code{background:#e0f2f1;color:#00796b}.objective:before{content:"school"}.success:before{content:"check_circle"}.warning,.warning :link,.warning :visited,.warning code{background:#fce8e6;color:#d50000}.warning:before{content:"warning"}.caution :focus code,.caution :hover code,.dogfood :focus code,.dogfood :hover code,.key-point :focus code,.key-point :hover code,.key-term :focus code,.key-term :hover code,.note :focus code,.note :hover code,.objective :focus code,.objective :hover code,.special :focus code,.special :hover code,.success :focus code,.success :hover code,.warning :focus code,.warning :hover code,aside :focus code,aside :hover code{background:transparent}.devsite-no-page-title>.caution:first-child,.devsite-no-page-title>.dogfood:first-child,.devsite-no-page-title>.key-point:first-child,.devsite-no-page-title>.key-term:first-child,.devsite-no-page-title>.note:first-child,.devsite-no-page-title>.objective:first-child,.devsite-no-page-title>.special:first-child,.devsite-no-page-title>.success:first-child,.devsite-no-page-title>.warning:first-child,.devsite-no-page-title>aside:first-child{clear:right}.devsite-banner{font-size:14px}.devsite-banner :link,.devsite-banner :visited{text-decoration:underline}body[layout=full] .devsite-banner{margin:-40px calc(50% - 50vw) 40px}body[type=landing][layout] .devsite-banner{margin:0}body[layout=docs] .devsite-banner{margin:-40px -40px 40px}.devsite-banner-message{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0 auto;max-width:1520px;padding:20px 40px}body[layout=full] .devsite-banner-message,body[type=landing] .devsite-banner-message,body[type=landing][layout=docs] .devsite-banner-message{padding:20px 40px}[dir=ltr] .devsite-banner-message-text{margin-right:auto}[dir=rtl] .devsite-banner-message-text{margin-left:auto}.devsite-banner[background] a:not(.button):focus,.devsite-banner a:not(.button):focus{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.devsite-banner[background=white]{border-bottom:1px solid #dadce0}.devsite-banner-announcement,.devsite-banner-announcement :link,.devsite-banner-announcement :visited{background:#e1f5fe}.devsite-banner-announcement[background] :link,.devsite-banner-announcement[background] :visited{background:0}.devsite-banner-confidential{background:#feefe3;color:#bf360c}.devsite-banner-confidential .devsite-banner-message:before{content:"warning";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}[dir=ltr] .devsite-banner-confidential .devsite-banner-message:before{margin-right:16px}[dir=rtl] .devsite-banner-confidential .devsite-banner-message:before{margin-left:16px}.devsite-banner-translated{background:#f1f3f4;color:rgba(0,0,0,.65)}.devsite-banner-translated :link{text-decoration:none}.devsite-banner .button,.devsite-banner button{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;margin:-6px 0;text-decoration:none}[dir=ltr] .devsite-banner .button,[dir=ltr] .devsite-banner button{margin-left:16px}[dir=rtl] .devsite-banner .button,[dir=rtl] .devsite-banner button{margin-right:16px}.devsite-banner .material-icons{margin:-2px 0;vertical-align:middle}.devsite-banner-translated-image{margin:4px 0 -4px;width:122px}[dir=ltr] .devsite-banner-translated-image{margin-right:24px}[dir=rtl] .devsite-banner-translated-image{margin-left:24px}.devsite-banner-heading{font-weight:700}@media screen and (max-width:1252px){.devsite-banner-translated .devsite-banner-translated-text{display:block}}@media screen and (max-width:840px){body[layout=docs] .devsite-banner,body[layout=full] .devsite-banner{margin:-24px -24px 24px}.devsite-banner-message,body[layout] .devsite-banner-message,body[layout][type] .devsite-banner-message,body[type] .devsite-banner-message{padding:20px 24px}body[layout=full] .devsite-banner{margin-bottom:40px}}@media screen and (max-width:600px){body[layout=docs] .devsite-banner,body[layout=full] .devsite-banner{margin:-16px -16px 16px}body[layout=full] .devsite-banner{margin-bottom:40px}.devsite-banner-message,body[layout] .devsite-banner-message,body[layout][type] .devsite-banner-message,body[type] .devsite-banner-message{display:block;padding:16px}[dir] .devsite-banner .button,[dir] .devsite-banner button{margin:12px 0 0}}.devsite-card-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-24px 0 0 -24px}[dir=rtl] .devsite-card-group{margin:-24px -24px 0 0}.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 33.333%;flex:0 0 33.333%;min-width:0;padding:24px 0 0 24px}[dir=rtl] .devsite-card-wrapper{padding:24px 24px 0 0}.devsite-card-wrapper[hidden]{display:none}.devsite-card-list-link,.devsite-card h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em}.devsite-card h3{margin:0 0 10px}.devsite-card{background:#fff;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;height:100%;position:relative}.devsite-card-image-bg{background-position:50%;background-repeat:no-repeat;-o-background-size:cover;background-size:cover;padding:0 0 56.25%}.devsite-card-content-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-card-content{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;padding:16px;word-break:break-word}.devsite-card-category{font:700 12px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.3px;margin-top:0;text-transform:uppercase}.devsite-card-summary{-webkit-box-orient:vertical;-webkit-line-clamp:4;display:-webkit-box;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:16px 0 0;max-height:96px;overflow:hidden}.devsite-card-author{border-top:1px solid #dadce0;-webkit-box-sizing:content-box;box-sizing:content-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;font-size:12px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;line-height:16px;min-height:40px;position:relative}.devsite-card-author,.devsite-card-buttons{padding:16px}.devsite-card-author-date,.devsite-card-author-name{margin:0}.devsite-card-author-name+.devsite-card-author-date{margin-top:8px}.devsite-card-author+.devsite-card-buttons,.devsite-card-content+.devsite-card-buttons{padding-top:0}.devsite-card-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse;margin:auto 0 0}.devsite-card-list{list-style:none;padding:0}.devsite-card-list-item{-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}.devsite-card-list-item:not(:last-child){margin:0 0 20px;padding:0}.devsite-card-list-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#1a73e8;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;padding:16px 20px;-webkit-transition:background .2s,color .2s;-o-transition:background .2s,color .2s;transition:background .2s,color .2s;width:100%}.devsite-card-list-link:focus,.devsite-card-list-link:hover{background:#e4eefc}.devsite-card-list-link:focus{text-decoration:none}.devsite-card-list-link:after{content:"arrow_forward";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin-left:auto;opacity:0;-webkit-transform:translateX(-20px);-o-transform:translateX(-20px);transform:translateX(-20px);-webkit-transition:opacity .2s,-webkit-transform .2s;transition:opacity .2s,-webkit-transform .2s;-o-transition:opacity .2s,-o-transform .2s;transition:opacity .2s,transform .2s;transition:opacity .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}.devsite-card-list-link:focus:after,.devsite-card-list-link:hover:after{opacity:1;-webkit-transform:translateX(0);-o-transform:translateX(0);transform:translateX(0)}@media screen and (max-width:840px){.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}}@media screen and (max-width:600px){.devsite-card-group{margin:-16px 0 0 -16px}[dir=rtl] .devsite-card-group{margin:-16px -16px 0 0}.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:16px 0 0 16px}[dir=rtl] .devsite-card-wrapper{padding:16px 16px 0 0}.devsite-card-summary{font-size:14px;line-height:20px;max-height:80px}}.pre-style,code,pre{background:#f1f3f4;color:#37474f;font:400 100%/1 Roboto Mono,monospace;padding:1px 4px}code{font:500 90%/1 Roboto Mono,monospace;word-break:break-word}.pre-style code,pre code,table code{font-weight:400;word-break:normal}.pre-style,pre{font:14px/20px Roboto Mono,monospace;margin:16px 0;overflow-x:auto;padding:24px;position:relative}.pre-style code,pre code{background:0;font-size:14px;padding:0}b code,strong code{font-weight:700}pre.devsite-code-highlight>span{opacity:.54}td>pre:only-child{padding:0}td>devsite-code:only-child pre,td>devsite-code pre.inline-code{padding:0 64px 0 0}td>devsite-code:not([dark-code]):only-child pre,td>devsite-code pre.inline-code{background:0}td>devsite-code:only-child pre~.devsite-code-buttons-container,td>devsite-code pre.inline-code~.devsite-code-buttons-container{top:-6px}h1 code,h2 code,h3 code,h4 code,h5 code,h6 code{background:0;color:#212121;padding:0}h1 code{color:#757575}a code,td a code{color:#1967d2}body[layout] .devsite-main-content var span,var,var code{color:#ec407a;-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-weight:700}pre.clear-for-copy{all:initial;left:-99999px;position:absolute;top:-99999px;white-space:pre}pre.clear-for-copy *{all:unset;font-family:Roboto Mono,monospace;white-space:pre}fieldset{border:0;margin:0;padding:0}label{color:#5f6368;display:block;font-size:12px}input+label{color:#202124;display:inline;font-size:16px}label[for]{cursor:pointer}input[type=checkbox],input[type=radio]{-webkit-appearance:none;background:#fff;-webkit-border-radius:2px;border-radius:2px;cursor:pointer;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;height:18px;margin:-2px 8px 2px 0;outline:0;position:relative;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;vertical-align:middle;width:18px}[dir=rtl] input[type=checkbox],[dir=rtl] input[type=radio]{margin:-2px 0 2px 8px}input[type=checkbox]{color:#5f6368}input[type=radio]{-webkit-border-radius:50%;border-radius:50%;color:#5f6368;-webkit-transition:none;-o-transition:none;transition:none}input[type=checkbox]:focus:before,input[type=radio]:focus:before{background:#e8eaed}input[type=checkbox]:checked,input[type=checkbox]:indeterminate,input[type=radio]:checked{color:#1a73e8}input[type=checkbox]:checked:focus:before,input[type=checkbox]:indeterminate:focus:before,input[type=radio]:focus:before{background:#d2e3fc}input[type=checkbox]:after,input[type=radio]:after{content:"check_box_outline_blank";position:relative;right:3px;top:-3px;z-index:1}[dir=rtl] input[type=checkbox]:after,[dir=rtl] input[type=radio]:after{left:3px;right:auto}input[type=checkbox]:checked:after{content:"check_box"}input[type=checkbox]:indeterminate:after{content:"indeterminate_check_box"}input[type=radio]:after{content:"radio_button_unchecked"}input[type=radio]:checked:after{content:"radio_button_checked"}input[type=checkbox]:before,input[type=radio]:before{-webkit-border-radius:50%;border-radius:50%;content:"";display:block;height:36px;left:-9px;position:absolute;top:-9px;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s;width:36px}[dir=rtl] input[type=checkbox]:before,[dir=rtl] input[type=radio]:before{left:auto;right:-9px}input:disabled+label,input[type=checkbox]:disabled,input[type=radio]:disabled{color:#bdc1c6;cursor:default}input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select[multiple],select[size],textarea{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;color:#202124;font:16px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;max-width:100%;outline:0;padding:7px;-webkit-transition:border-color .2s;-o-transition:border-color .2s;transition:border-color .2s;vertical-align:middle}input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus,select[multiple]:focus,select[size]:focus,textarea:focus{border-bottom:2px solid #1a73e8;padding-bottom:6px}input[type=date]:disabled,input[type=datetime-local]:disabled,input[type=datetime]:disabled,input[type=email]:disabled,input[type=month]:disabled,input[type=number]:disabled,input[type=password]:disabled,input[type=search]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=time]:disabled,input[type=url]:disabled,input[type=week]:disabled,select[multiple]:disabled,select[size]:disabled,textarea:disabled{background:#f1f3f4}body input[type=file]{height:auto;line-height:1;padding:8px 16px}select{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;padding:0 27px 0 7px;-moz-appearance:none;-webkit-appearance:none;background:#fff url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23212121'/></svg>") no-repeat 100%;-webkit-box-shadow:none;box-shadow:none;color:#202124;cursor:pointer;display:inline-block;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:36px;line-height:34px;max-width:256px;min-width:72px;outline:0;overflow:hidden;text-align:left;text-indent:.01px;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s;-o-transition:background-color .2s;transition:background-color .2s;vertical-align:middle;white-space:nowrap}select:focus,select:hover{background-color:#f1f3f4}select:active{background-color:#e8eaed}select:disabled{background:#f1f3f4 url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23bdbdbd'/></svg>") no-repeat 100%;border-color:transparent;color:#bdc1c6;cursor:default}select::-ms-expand{display:none}devsite-book-nav .devsite-breadcrumb-list,devsite-content .devsite-breadcrumb-list,devsite-header .devsite-breadcrumb-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:0;white-space:nowrap}.devsite-search-project .devsite-breadcrumb-list,devsite-book-nav .devsite-breadcrumb-list,devsite-header .devsite-breadcrumb-list{display:-webkit-box;display:-webkit-flex;display:flex}devsite-content .devsite-breadcrumb-list{display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-flex-wrap:wrap;flex-wrap:wrap;font-size:13px}body[layout=full] devsite-content .devsite-breadcrumb-list,body[type=landing] devsite-content .devsite-breadcrumb-list{display:none}devsite-book-nav .devsite-breadcrumb-item,devsite-content .devsite-breadcrumb-item,devsite-header .devsite-breadcrumb-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0}devsite-book-nav .devsite-breadcrumb-guillemet,devsite-header .devsite-breadcrumb-guillemet{font-size:24px;margin:0 4px;width:24px}devsite-content .devsite-breadcrumb-guillemet{color:#5f6368;font-size:18px;margin:0 4px;width:18px}devsite-book-nav .devsite-breadcrumb-guillemet:before,devsite-content .devsite-breadcrumb-guillemet:before,devsite-header .devsite-breadcrumb-guillemet:before{content:"chevron_right"}[dir=rtl] devsite-book-nav .devsite-breadcrumb-guillemet:before,[dir=rtl] devsite-content .devsite-breadcrumb-guillemet:before,[dir=rtl] devsite-header .devsite-breadcrumb-guillemet:before{content:"chevron_left"}devsite-book-nav .devsite-breadcrumb-link,devsite-content .devsite-breadcrumb-link,devsite-header .devsite-breadcrumb-link{display:inline-block;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-book-nav .devsite-breadcrumb-link:focus,devsite-book-nav .devsite-breadcrumb-link:hover,devsite-header .devsite-breadcrumb-link:focus,devsite-header .devsite-breadcrumb-link:hover{text-decoration:none}devsite-content .devsite-breadcrumb-link{color:#5f6368}devsite-content .devsite-breadcrumb-link:focus,devsite-content .devsite-breadcrumb-link:hover{color:#1a73e8;text-decoration:none}.devsite-nav{font-size:13px}.devsite-nav-list,.devsite-nav-responsive-tabs,.devsite-nav-section{list-style-type:none;padding:0}.devsite-nav-item{line-height:16px;margin:0}.devsite-nav-title{color:#202124;display:-webkit-box;display:-webkit-flex;display:flex;padding:4px 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.devsite-nav-title[href]:focus,.devsite-nav-title[href]:hover{color:#1a73e8;text-decoration:none}.devsite-nav-heading>.devsite-nav-title{color:rgba(0,0,0,.65);font-weight:700}.devsite-nav-active{font-weight:500}.devsite-nav-active,.devsite-nav-active.devsite-nav-title,.devsite-nav-active.devsite-nav-title>.devsite-nav-icon:before,.devsite-nav-deprecated .devsite-nav-active.devsite-nav-title{color:#1a73e8}.devsite-nav-text{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}.devsite-nav-text>span{pointer-events:none}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title-no-path:focus,.devsite-nav-title-no-path:focus{color:#1a73e8}.devsite-nav-icon{cursor:default;font-size:18px;margin:-1px 0 -1px 4px}[dir=rtl] .devsite-nav-icon{margin:-1px 4px -1px 0}.devsite-nav-icon:before{color:#5f6368;content:"info"}.devsite-nav-icon[data-icon=alpha]:before,.devsite-nav-icon[data-icon=beta]:before,.devsite-nav-icon[data-icon=experimental]:before{content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><path d='M15.78,13.39L11,7V4h2V2H5v2h2v3l-4.9,6.53c-0.34,0.47-0.39,1.1-0.12,1.62C2.24,15.67,2.77,16,3.36,16h11.28 c0.86,0,1.56-0.7,1.56-1.56C16.2,14.04,16.03,13.67,15.78,13.39z' fill='%2380868b'/></svg>")}.devsite-nav-deprecated.devsite-nav-accordion .devsite-nav-title,.devsite-nav-deprecated .devsite-nav-title,.devsite-nav-icon[data-icon=deprecated]:before{color:#bdc1c6}.devsite-nav-icon[data-icon=deprecated]:before{content:"not_interested"}.devsite-nav-icon[data-icon=external]:before{content:"open_in_new"}[dir=rtl] .devsite-nav-icon[data-icon=external]:before{display:inline-block;-webkit-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.devsite-nav-icon[data-icon=forward]:before{content:"arrow_forward";cursor:pointer}[dir=rtl] .devsite-nav-icon[data-icon=forward]:before{content:"arrow_back"}.devsite-nav-icon[data-icon=limited]:before{content:"verified_user"}.devsite-nav-icon[data-icon=new]:before{content:"new_releases"}.devsite-nav-icon[data-icon=nightly]:before{content:"nights_stay"}.button,.devsite-footer-utility-button>a,button,input[type=button],input[type=file],input[type=image],input[type=reset],input[type=submit]{-moz-appearance:none;-webkit-appearance:none;background:#fff;-webkit-box-sizing:border-box;box-sizing:border-box;color:#1a73e8;cursor:pointer;display:inline-block;height:36px;margin:0;min-width:36px;outline:0;overflow:hidden;text-decoration:none;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s,border .2s,-webkit-box-shadow .2s;transition:background-color .2s,border .2s,-webkit-box-shadow .2s;-o-transition:background-color .2s,border .2s,box-shadow .2s;transition:background-color .2s,border .2s,box-shadow .2s;transition:background-color .2s,border .2s,box-shadow .2s,-webkit-box-shadow .2s;vertical-align:middle;white-space:nowrap;border:0;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 16px;text-transform:uppercase}.button:focus,.button:hover,.devsite-footer-utility-button>a:focus,.devsite-footer-utility-button>a:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=file]:focus,input[type=file]:hover,input[type=image]:focus,input[type=image]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:#e4eefc}.button:active,.devsite-footer-utility-button>a:active,button:active,input[type=button]:active,input[type=file]:active,input[type=image]:active,input[type=reset]:active,input[type=submit]:active{background:#c8ddf9;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.button.button-disabled,.button.button-disabled:active,.button.button-disabled:focus,.button.button-disabled:hover,[foreground] .button.button-disabled,[foreground] .button.button-disabled:active,[foreground] .button.button-disabled:focus,[foreground] .button.button-disabled:hover,body[theme] [foreground] .button.button-disabled,body[theme] [foreground] .button.button-disabled:active,body[theme] [foreground] .button.button-disabled:focus,body[theme] [foreground] .button.button-disabled:hover,button[disabled],button[disabled]:active,button[disabled]:focus,button[disabled]:hover,input[type=button][disabled],input[type=button][disabled]:active,input[type=button][disabled]:focus,input[type=button][disabled]:hover,input[type=file][disabled],input[type=file][disabled]:active,input[type=file][disabled]:focus,input[type=file][disabled]:hover,input[type=image][disabled],input[type=image][disabled]:active,input[type=image][disabled]:focus,input[type=image][disabled]:hover,input[type=reset][disabled],input[type=reset][disabled]:active,input[type=reset][disabled]:focus,input[type=reset][disabled]:hover,input[type=submit][disabled],input[type=submit][disabled]:active,input[type=submit][disabled]:focus,input[type=submit][disabled]:hover{-webkit-box-shadow:none;box-shadow:none;cursor:default;pointer-events:none;background:#ddd;color:rgba(0,0,0,.26)}.button-blue,.button-green,.button-primary,.button-red,body devsite-footer-utility .devsite-footer-utility-button>a{background:#1a73e8;color:#fff}.button-blue:focus,.button-blue:hover,.button-green:focus,.button-green:hover,.button-primary:focus,.button-primary:hover,.button-red:focus,.button-red:hover,body devsite-footer-utility .devsite-footer-utility-button>a:focus,body devsite-footer-utility .devsite-footer-utility-button>a:hover{background:#1765cc;color:#fff}.button-blue:active,.button-green:active,.button-primary:active,.button-red:active,body devsite-footer-utility .devsite-footer-utility-button>a:active{background:#0277bd;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary{background:#fff;color:#1a73e8}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:focus,[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:hover{background:#e4eefc}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:active{background:#c8ddf9}.button-white{background:0;color:#1a73e8;padding:0 8px}.button-white,.button-white:active,.button-white:focus,.button-white:hover{border:0;-webkit-box-shadow:none;box-shadow:none}.button-white.button-disabled,.button-white[disabled]{background:0}.button-raised{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}.button-raised:focus,.button-raised:hover{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.button-raised:active{-webkit-box-shadow:0 1px 3px 0 rgba(60,64,67,.3),0 4px 8px 3px rgba(60,64,67,.15);box-shadow:0 1px 3px 0 rgba(60,64,67,.3),0 4px 8px 3px rgba(60,64,67,.15)}.button+.button,button+button,input[type=button]+input[type=button],input[type=file]+input[type=file],input[type=image]+input[type=image],input[type=reset]+input[type=reset],input[type=submit]+input[type=submit]{margin-left:16px}[dir=rtl] .button+.button,[dir=rtl] button+button,[dir=rtl] input[type=button]+input[type=button],[dir=rtl] input[type=file]+input[type=file],[dir=rtl] input[type=image]+input[type=image],[dir=rtl] input[type=reset]+input[type=reset],[dir=rtl] input[type=submit]+input[type=submit]{margin-left:0;margin-right:16px}.button-flat+.button-flat,.button-white+.button-white,button+.button{margin-left:8px}[dir=rtl] .button-flat+.button-flat,[dir=rtl] .button-white+.button-white,[dir=rtl] button+.button{margin-left:0;margin-right:8px}.button:focus{text-decoration:none}.button-flat{padding:0 8px}.button-flat,.button-flat:active,.button-flat:focus,.button-flat:hover{background:0;border:0;-webkit-box-shadow:none;box-shadow:none}.button-flat:disabled{background-color:transparent}.button-transparent{padding:0 8px}.button-transparent,.button-transparent:focus,.button-transparent:hover{background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.button-text-white{color:#fff}.button-text-blue{color:#1a73e8}.button-lowercase{text-transform:none}.button-unindented{margin-left:-8px}[dir=rtl] .button-unindented{margin-left:0;margin-right:-8px}.button-icon,.button>.material-icons,button>.material-icons{font-size:18px;height:18px;width:18px}.button.button-with-icon,.button.external{padding:0 16px}.button>.material-icons,button>.material-icons{margin:0 8px;position:relative;top:-2px;vertical-align:middle}.button>.button-icon,button>.button-icon{margin:0 8px}.button-with-icon>.button-icon,.button-with-icon>.material-icons{margin:0 8px 0 -4px}.button-with-icon>.icon-after,.button.external:not(.button-with-icon):after,[dir=rtl] .button-with-icon>.button-icon,[dir=rtl] .button-with-icon>.material-icons,button.external:not(.button-with-icon):after{margin:0 -4px 0 8px}[dir=rtl] .button-with-icon>.icon-after,[dir=rtl] .button.external:not(.button-with-icon):after,[dir=rtl] button.external:not(.button-with-icon):after{margin:0 8px 0 -4px}.button:not(.button-with-icon)>.material-icons:not(.icon-after){margin-left:-4px}.button:not(.button-with-icon)>.icon-after,[dir=rtl] .button:not(.button-with-icon)>.material-icons:not(.icon-after){margin-right:-4px}[dir=rtl] .button:not(.button-with-icon)>.icon-after{margin-left:-4px}.button-white:not(.button-with-icon)>.material-icons:not(.icon-after){margin-left:4px}.button-white:not(.button-with-icon)>.icon-after,[dir=rtl] .button-white:not(.button-with-icon)>.material-icons:not(.icon-after){margin-right:4px}[dir=rtl] .button-white:not(.button-with-icon)>.icon-after{margin-left:4px}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white:hover{background:rgba(154,160,166,.3)}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white:focus{background:rgba(154,160,166,.5)}.devsite-landing-row-item[foreground=grey] .button,[foreground=grey] .button{background:#5f6368}.devsite-landing-row-item[foreground=grey] .button:active,.devsite-landing-row-item[foreground=grey] .button:focus,.devsite-landing-row-item[foreground=grey] .button:hover,[foreground=grey] .button:active,[foreground=grey] .button:focus,[foreground=grey] .button:hover{background:#3c4043}devsite-header .button,devsite-header .button:active,devsite-header .button:focus,devsite-header .button:hover{-webkit-box-shadow:none;box-shadow:none}h1,h2,h3,h4,h5,h6{outline:0}[layout=docs] h1,[layout=docs] h2,[layout=docs] h3,[layout=docs] h4,[layout=docs] h5,[layout=docs] h6{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}.devsite-article h1:first-of-type{margin-top:0;position:relative;top:-4px}.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,.devsite-landing-row h2,h1{color:#5f6368;font:300 34px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em;margin:40px 0 20px}.devsite-landing-row .devsite-catalog-alphabet-letter-heading h2{margin:20px 0}[layout=docs] h2{border-bottom:1px solid #e8eaed;padding-bottom:3px}.devsite-landing-row h3,h2{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em;margin:40px 0 20px}h3{margin:32px 0 16px}.devsite-landing-row-item-no-media h3,.devsite-landing-row h4,h3{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row-item-no-media h3,.devsite-landing-row h4{margin:32px 0 12px;padding:0}.devsite-landing-row-large-headings .devsite-landing-row-item-list h4{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em}h4,h5,h6{margin:32px 0 16px}h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h5{font:700 14px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h6{font:500 14px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h1+dl>dt:first-child,h1+h1,h1+h2,h1+h3,h1+h4,h1+h5,h1+h6,h2+dl>dt:first-child,h2+h1,h2+h2,h2+h3,h2+h4,h2+h5,h2+h6,h3+dl>dt:first-child,h3+h1,h3+h2,h3+h3,h3+h4,h3+h5,h3+h6,h4+dl>dt:first-child,h4+h1,h4+h2,h4+h3,h4+h4,h4+h5,h4+h6,h5+dl>dt:first-child,h5+h1,h5+h2,h5+h3,h5+h4,h5+h5,h5+h6,h6+dl>dt:first-child,h6+h1,h6+h2,h6+h3,h6+h4,h6+h5,h6+h6{margin-top:0}@media screen and (max-width:600px){.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,.devsite-landing-row h2,h1{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}table{border:0;border-collapse:collapse;border-spacing:0;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:16px 0 15px;width:100%}caption{background:#f1f3f4;font-weight:500;padding:12px 8px;text-align:center}tr{border:0;border-bottom:1px solid #dadce0}tr:first-child{border-top:1px solid #dadce0}td,th{border:0;margin:0;text-align:left}[dir=rtl] td,[dir=rtl] th{text-align:right}th{height:48px;padding:8px;vertical-align:middle}th>devsite-heading>h2,th>devsite-heading>h3,th>h2,th>h3{border:0;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0!important;padding:0!important}td>.expandable>h2.showalways,td>.expandable>h3.showalways,td>h2:only-child,td>h3:only-child{border:0;font:500 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding-bottom:0}td>.expandable>h2.showalways,td>.expandable>h3.showalways{line-height:24px}td b,td strong,th b,th strong{font-weight:500}td,td code{padding:7px 8px 8px}td code,th code{background:0;font:500 100%/1 Roboto Mono,monospace;-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;padding:0}td pre code{color:#37474f;font-weight:400;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}table.responsive td,table.responsive th{min-width:214px}table.responsive table:not(.responsive) td,table.responsive table:not(.responsive) th{min-width:120px}table.responsive td code,table.responsive th code{word-break:break-all;word-break:break-word}table.responsive td tr:not(.alt) td:first-child,table.responsive tr:not(.alt) td td:first-child,td{background:hsla(0,0%,100%,.95);vertical-align:top}.devsite-table-wrapper{margin:16px 0;overflow:auto}.devsite-table-wrapper .devsite-table-wrapper{margin:0;overflow:visible}.devsite-table-wrapper table{margin:0}.devsite-table-wrapper .devsite-table-wrapper table{margin:16px 0}table.responsive table.responsive{margin:0}table.responsive td tr:first-child td{padding-top:0}table.responsive td tr:last-child td{padding-bottom:0}[dir=ltr] table.responsive td td:first-child{padding-left:0}[dir=rtl] table.responsive td td:first-child{padding-right:0}table.responsive>*>tr>th:not(:first-child),table.responsive>tr>th:not(:first-child){display:none}table.columns tr{border:0}table table tr:first-child{border-top:0}devsite-selector .devsite-table-wrapper:last-child tr:last-child,table table tr:last-child{border-bottom:0}th,th code{background:#e8eaed;color:#202124;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}table.vertical-rules tr>td:not(:last-child),table.vertical-rules tr>th:not(:last-child){border-right:1px solid #dadce0}.alt td,td.alt{background:rgba(241,243,244,.75)}table.responsive>*>tr:not(.alt) td:first-child,table.responsive>tr:not(.alt) td:first-child{background:hsla(0,0%,96.5%,.87)}table.blue tr{background:#e8eaf6;border-bottom-color:#c5cae9}table.cyan tr{background:#e4f7fb;border-bottom-color:#a1e4f2}table.green tr{background:#e0f2f1;border-bottom-color:#b2dfdb}table.orange tr{background:#feefe3;border-bottom-color:#fedfc8}table.pink tr{background:#fde7f3;border-bottom-color:#fdcfe8}table.purple tr{background:#f3e8fd;border-bottom-color:#e9d2fd}table.blue tr:first-child{border-top-color:#c5cae9}table.cyan tr:first-child{border-top-color:#a1e4f2}table.green tr:first-child{border-top-color:#b2dfdb}table.orange tr:first-child{border-top-color:#fedfc8}table.pink tr:first-child{border-top-color:#fdcfe8}table.purple tr:first-child{border-top-color:#e9d2fd}table.blue th,table.cyan th,table.green th,table.orange th,table.pink th,table.purple th{background:inherit}table.blue tr.alt td,table.responsive.blue tr:not(.alt) td:first-child{background:#f6f7fb}table.responsive.blue table td:first-child{background:#fff}table.cyan tr.alt td,table.responsive.cyan tr:not(.alt) td:first-child{background:#f4fcfd}table.responsive.cyan table td:first-child{background:#fff}table.green tr.alt td,table.responsive.green tr:not(.alt) td:first-child{background:#f3faf9}table.responsive.green table td:first-child{background:#fff}table.orange tr.alt td,table.responsive.orange tr:not(.alt) td:first-child{background:#fff9f4}table.responsive.orange table td:first-child{background:#fff}table.pink tr.alt td,table.responsive.pink tr:not(.alt) td:first-child{background:#fef5fa}table.responsive.pink table td:first-child{background:#fff}table.purple tr.alt td,table.responsive.purple tr:not(.alt) td:first-child{background:#faf6fe}table.responsive.purple table td:first-child{background:#fff}table.vertical-rules.blue tr>td:not(:last-child),table.vertical-rules.blue tr>th:not(:last-child){border-right:1px solid #c5cae9}table.vertical-rules.cyan tr>td:not(:last-child),table.vertical-rules.cyan tr>th:not(:last-child){border-right:1px solid #a1e4f2}table.vertical-rules.green tr>td:not(:last-child),table.vertical-rules.green tr>th:not(:last-child){border-right:1px solid #b2dfdb}table.vertical-rules.orange tr>td:not(:last-child),table.vertical-rules.orange tr>th:not(:last-child){border-right:1px solid #fedfc8}table.vertical-rules.pink tr>td:not(:last-child),table.vertical-rules.pink tr>th:not(:last-child){border-right:1px solid #fdcfe8}table.vertical-rules.purple tr>td:not(:last-child),table.vertical-rules.purple tr>th:not(:last-child){border-right:1px solid #e9d2fd}.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -16px}}.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:16px}}.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:16px}}.devsite-full-width-table table table td:first-child,.devsite-full-width-table table table th:first-child{padding-left:0}@media screen and (max-width:840px){.devsite-full-width-table table table td:first-child,.devsite-full-width-table table table th:first-child{padding-right:0}}.devsite-full-width-table table table td:last-child,.devsite-full-width-table table table th:last-child{padding-right:0}@media screen and (max-width:840px){.devsite-full-width-table table table td:last-child,.devsite-full-width-table table table th:last-child{padding-left:0}}@media screen and (max-width:840px){table.responsive td,table.responsive th,table.responsive tr{display:block}table.responsive table:not(.responsive) tr{display:table-row}table.responsive table:not(.responsive) td,table.responsive table:not(.responsive) th{display:table-cell}table.responsive>*>th,table.responsive>th{height:auto;padding:14px 8px}}.devsite-book-nav::-webkit-scrollbar,.devsite-dialog::-webkit-scrollbar,.devsite-popout::-webkit-scrollbar,.devsite-table-wrapper::-webkit-scrollbar,.devsite-tabs-overflow-menu::-webkit-scrollbar,.devsite-toc::-webkit-scrollbar,[scrollbars]::-webkit-scrollbar,pre::-webkit-scrollbar{height:8px;width:8px}.devsite-book-nav::-webkit-scrollbar-thumb,.devsite-dialog::-webkit-scrollbar-thumb,.devsite-popout::-webkit-scrollbar-thumb,.devsite-table-wrapper::-webkit-scrollbar-thumb,.devsite-tabs-overflow-menu::-webkit-scrollbar-thumb,.devsite-toc::-webkit-scrollbar-thumb,[scrollbars]::-webkit-scrollbar-thumb,pre::-webkit-scrollbar-thumb{background:rgba(128,134,139,.26);-webkit-border-radius:8px;border-radius:8px}.devsite-doc-set-nav-row::-webkit-scrollbar,.devsite-header-upper-tabs::-webkit-scrollbar,[no-horizontal-scrollbars]::-webkit-scrollbar{height:0;width:0}.devsite-table-wrapper::-webkit-scrollbar-corner,[scrollbars]::-webkit-scrollbar-corner,pre::-webkit-scrollbar-corner{background:0}.devsite-cse-confidential-results{background:rgba(254,239,227,.5);margin:16px -40px;padding:0 40px 16px}.devsite-cse-confidential-results+aside{margin-top:-16px!important}.devsite-search-results-stats{margin-bottom:8px}.devsite-search-results-restricted .gs-title{font-weight:500}.devsite-search-results-restricted .gs-title:link,.devsite-search-results-restricted .gs-title:visited{color:#039be5}.devsite-search-results-restricted .gs-visibleUrl{color:#1e8e3e;font-size:14px}.devsite-result-item-link .devsite-result-item-confidential,.devsite-search-results-restricted .gs-title-label{background:#feefe3;-webkit-border-radius:4px;border-radius:4px;color:#bf360c;display:inline-block;font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin:0 8px;padding:5px 8px 3px;text-transform:uppercase}body[type=search] .gsc-webResult .gsc-result{border:none;margin:24px 0;padding:0}.devsite-search-page-controls{margin-top:8px}.devsite-search-project{border-bottom:1px solid #dadce0;margin-bottom:24px;padding-bottom:23px}.devsite-search-project .devsite-project-scoped-results-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-bottom:8px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link:focus,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link:hover,.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo{height:32px}.devsite-search-project .devsite-project-scoped-results-title .devsite-has-google-wordmark>.devsite-breadcrumb-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}.devsite-search-project .devsite-project-scoped-results-title .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}.devsite-search-project .devsite-project-scoped-results-title .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link canvas{height:auto!important}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] .devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{margin-right:4px}[dir=rtl] .devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{margin-left:4px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}.devsite-search-project .devsite-project-scoped-results-title .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-item,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-list,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-product-name{width:100%}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{position:relative;margin-left:.3em}.devsite-search-project .devsite-breadcrumb-list,.devsite-search-project .devsite-project-scoped-results-title{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-search-project .devsite-breadcrumb-link,.devsite-search-project .devsite-breadcrumb-link:hover{color:#202124}.devsite-search-project .devsite-breadcrumb-link .devsite-google-wordmark{fill:currentColor}.devsite-search-title{margin:0;padding:0}.devsite-search-title .devsite-search-term{color:#202124;font-weight:500}.devsite-steps{padding:24px 0 40px;-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-steps,.devsite-steps .steps-direction{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-steps .steps-direction{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;font-weight:500;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:relative}.devsite-steps .steps-previous{margin-left:40px}.devsite-steps .steps-next{margin-right:40px;text-align:right}.devsite-steps .steps-link:focus{text-decoration:none}.devsite-steps .steps-link-direction{color:#1a73e8;display:block;font-size:14px}.devsite-steps .steps-link-title{color:#01579b;font-size:18px}.devsite-steps .steps-link:focus .steps-link-title{border-bottom:1px solid #01579b}.devsite-steps .steps-link-arrow{color:#1a73e8;position:absolute}.devsite-steps .steps-link-arrow-left{left:-40px}.devsite-steps .steps-link-arrow-right{right:-40px}@media screen and (max-width:840px){.devsite-steps .steps-link-title{font-size:14px}.devsite-steps .steps-previous{margin-left:24px}.devsite-steps .steps-next{margin-right:24px}.devsite-steps .steps-link-arrow-left{left:-24px}.devsite-steps .steps-link-arrow-right{right:-24px}.devsite-steps .steps-link-arrow{bottom:4px;font-size:16px}}@media screen and (max-width:600px){.devsite-steps{padding:8px 0 16px}.devsite-steps .steps-next,.devsite-steps .steps-previous{margin:0}.devsite-steps .steps-link-arrow{display:none}}.devsite-jsfiddle-hide,devsite-googler-buttons,devsite-playlist,devsite-quiz,devsite-search .devsite-popout,html[cached] .devsite-wrapper{display:none}.devsite-dialog:not([is-upgraded]),iframe.devsite-embedded-youtube-video:not([is-upgraded]){pointer-events:none;visibility:hidden}.code-sample,.data-sample,.ds-selector-dropdown,.ds-selector-tabs,.expandable,.kd-tabbed-horz,.kd-tabbed-vert{display:none}devsite-selector{pointer-events:none;visibility:hidden}devsite-search .devsite-searchbox{background:#f1f3f4;-webkit-border-radius:2px;border-radius:2px}devsite-page-rating[position=header]{-webkit-box-flex:0;-webkit-flex:0 0 120px;flex:0 0 120px;margin:0 0 0 16px;width:120px}[dir=rtl] devsite-page-rating[position=header]{margin:0 16px 0 0}google-codelab{display:none}iframe.framebox,iframe.inherit-locale{display:block;width:100%}[background]:not(.devsite-landing-row-cards),[background]:not(.devsite-landing-row-cards) h3,[background] h2{color:#fff}[background=grey]{background-color:#f1f3f4}[background=grey] h2{color:#5f6368}[background=grey]:not(.devsite-landing-row-cards),[background=grey]:not(.devsite-landing-row-cards) [background] h3,[background=grey]:not(.devsite-landing-row-cards) h3{color:inherit}[background] .devsite-landing-row-description{color:#fff}[background=grey] .devsite-landing-row-description{color:#202124}[background] :link:not(.button),[background] :visited:not(.button){color:#fff}[background=grey] :link:not(.button),[background=grey] :visited:not(.button),[background].devsite-landing-row-cards :link:not(.button),[background].devsite-landing-row-cards :visited:not(.button){color:#1a73e8}[background]:not([background=grey]) :focus>:not(.material-icons),[background]:not([background=grey]) :link>:not(.material-icons):hover,[background]:not([background=grey]) p>a:not(.button){text-decoration:underline}[background]:not([background=grey]) p>a:focus{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}[foreground] :focus>:not(.material-icons),[foreground] :link>:not(.material-icons):hover{text-decoration:underline}[foreground=blue-grey] .button{color:#607d8b}[foreground=blue-grey] .button:active,[foreground=blue-grey] .button:focus,[foreground=blue-grey] .button:hover{background:#eff2f3}[foreground=blue-grey] .button-primary{background:#607d8b;color:#fff}[foreground=blue-grey] .button-primary:active,[foreground=blue-grey] .button-primary:focus,[foreground=blue-grey] .button-primary:hover{background:#455a64}[background=blue-grey]{background-color:#607d8b}[foreground=blue-grey] :focus>:not(.material-icons),[foreground=blue-grey] :link>:not(.material-icons):hover,[foreground=blue-grey] a:not(.button) h2,[foreground=blue-grey] a:not(.button) h3{color:#607d8b}.devsite-landing-row[background=blue-grey]+.devsite-landing-row[background=blue-grey]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=blue-grey],.devsite-landing-row-item-icon-container[foreground=blue-grey],.devsite-landing-row-item-list-item-icon-container[background][foreground=blue-grey],.devsite-landing-row-item-list-item-icon-container[foreground=blue-grey],.devsite-landing-row-item[foreground=blue-grey] :link h2,.devsite-landing-row-item[foreground=blue-grey] :link h3{color:#607d8b}.devsite-landing-row-item-icon-container[background=blue-grey],.devsite-landing-row-item-list-item-icon-container[background=blue-grey]{background:#607d8b}[foreground=blue-grey-dark] .button{color:#455a64}[foreground=blue-grey-dark] .button:active,[foreground=blue-grey-dark] .button:focus,[foreground=blue-grey-dark] .button:hover{background:#eceff0}[foreground=blue-grey-dark] .button-primary{background:#455a64;color:#fff}[foreground=blue-grey-dark] .button-primary:active,[foreground=blue-grey-dark] .button-primary:focus,[foreground=blue-grey-dark] .button-primary:hover{background:#37474f}[background=blue-grey-dark]{background-color:#455a64}[foreground=blue-grey-dark] :focus>:not(.material-icons),[foreground=blue-grey-dark] :link>:not(.material-icons):hover,[foreground=blue-grey-dark] a:not(.button) h2,[foreground=blue-grey-dark] a:not(.button) h3{color:#455a64}.devsite-landing-row[background=blue-grey-dark]+.devsite-landing-row[background=blue-grey-dark]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=blue-grey-dark],.devsite-landing-row-item-icon-container[foreground=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[background][foreground=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[foreground=blue-grey-dark],.devsite-landing-row-item[foreground=blue-grey-dark] :link h2,.devsite-landing-row-item[foreground=blue-grey-dark] :link h3{color:#455a64}.devsite-landing-row-item-icon-container[background=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[background=blue-grey-dark]{background:#455a64}[foreground=deep-orange] .button{color:#ff5722}[foreground=deep-orange] .button:active,[foreground=deep-orange] .button:focus,[foreground=deep-orange] .button:hover{background:#ffeee9}[foreground=deep-orange] .button-primary{background:#ff5722;color:#fff}[foreground=deep-orange] .button-primary:active,[foreground=deep-orange] .button-primary:focus,[foreground=deep-orange] .button-primary:hover{background:#e64a19}[background=deep-orange]{background-color:#ff5722}[foreground=deep-orange] :focus>:not(.material-icons),[foreground=deep-orange] :link>:not(.material-icons):hover,[foreground=deep-orange] a:not(.button) h2,[foreground=deep-orange] a:not(.button) h3{color:#ff5722}.devsite-landing-row[background=deep-orange]+.devsite-landing-row[background=deep-orange]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=deep-orange],.devsite-landing-row-item-icon-container[foreground=deep-orange],.devsite-landing-row-item-list-item-icon-container[background][foreground=deep-orange],.devsite-landing-row-item-list-item-icon-container[foreground=deep-orange],.devsite-landing-row-item[foreground=deep-orange] :link h2,.devsite-landing-row-item[foreground=deep-orange] :link h3{color:#ff5722}.devsite-landing-row-item-icon-container[background=deep-orange],.devsite-landing-row-item-list-item-icon-container[background=deep-orange]{background:#ff5722}[foreground=deep-purple] .button{color:#673ab7}[foreground=deep-purple] .button:active,[foreground=deep-purple] .button:focus,[foreground=deep-purple] .button:hover{background:#f0ebf8}[foreground=deep-purple] .button-primary{background:#673ab7;color:#fff}[foreground=deep-purple] .button-primary:active,[foreground=deep-purple] .button-primary:focus,[foreground=deep-purple] .button-primary:hover{background:#512da8}[background=deep-purple]{background-color:#673ab7}[foreground=deep-purple] :focus>:not(.material-icons),[foreground=deep-purple] :link>:not(.material-icons):hover,[foreground=deep-purple] a:not(.button) h2,[foreground=deep-purple] a:not(.button) h3{color:#673ab7}.devsite-landing-row[background=deep-purple]+.devsite-landing-row[background=deep-purple]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=deep-purple],.devsite-landing-row-item-icon-container[foreground=deep-purple],.devsite-landing-row-item-list-item-icon-container[background][foreground=deep-purple],.devsite-landing-row-item-list-item-icon-container[foreground=deep-purple],.devsite-landing-row-item[foreground=deep-purple] :link h2,.devsite-landing-row-item[foreground=deep-purple] :link h3{color:#673ab7}.devsite-landing-row-item-icon-container[background=deep-purple],.devsite-landing-row-item-list-item-icon-container[background=deep-purple]{background:#673ab7}[foreground=google-blue] .button{color:#1a73e8}[foreground=google-blue] .button:active,[foreground=google-blue] .button:focus,[foreground=google-blue] .button:hover{background:#e8f1fd}[foreground=google-blue] .button-primary{background:#1a73e8;color:#fff}[foreground=google-blue] .button-primary:active,[foreground=google-blue] .button-primary:focus,[foreground=google-blue] .button-primary:hover{background:#185abc}[background=google-blue]{background-color:#1a73e8}[foreground=google-blue] :focus>:not(.material-icons),[foreground=google-blue] :link>:not(.material-icons):hover,[foreground=google-blue] a:not(.button) h2,[foreground=google-blue] a:not(.button) h3{color:#1a73e8}.devsite-landing-row[background=google-blue]+.devsite-landing-row[background=google-blue]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-blue],.devsite-landing-row-item-icon-container[foreground=google-blue],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-blue],.devsite-landing-row-item-list-item-icon-container[foreground=google-blue],.devsite-landing-row-item[foreground=google-blue] :link h2,.devsite-landing-row-item[foreground=google-blue] :link h3{color:#1a73e8}.devsite-landing-row-item-icon-container[background=google-blue],.devsite-landing-row-item-list-item-icon-container[background=google-blue]{background:#1a73e8}[foreground=google-green] .button{color:#1e8e3e}[foreground=google-green] .button:active,[foreground=google-green] .button:focus,[foreground=google-green] .button:hover{background:#e9f4ec}[foreground=google-green] .button-primary{background:#1e8e3e;color:#fff}[foreground=google-green] .button-primary:active,[foreground=google-green] .button-primary:focus,[foreground=google-green] .button-primary:hover{background:#137333}[background=google-green]{background-color:#1e8e3e}[foreground=google-green] :focus>:not(.material-icons),[foreground=google-green] :link>:not(.material-icons):hover,[foreground=google-green] a:not(.button) h2,[foreground=google-green] a:not(.button) h3{color:#1e8e3e}.devsite-landing-row[background=google-green]+.devsite-landing-row[background=google-green]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-green],.devsite-landing-row-item-icon-container[foreground=google-green],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-green],.devsite-landing-row-item-list-item-icon-container[foreground=google-green],.devsite-landing-row-item[foreground=google-green] :link h2,.devsite-landing-row-item[foreground=google-green] :link h3{color:#1e8e3e}.devsite-landing-row-item-icon-container[background=google-green],.devsite-landing-row-item-list-item-icon-container[background=google-green]{background:#1e8e3e}[foreground=google-red] .button{color:#d93025}[foreground=google-red] .button:active,[foreground=google-red] .button:focus,[foreground=google-red] .button:hover{background:#fbeae9}[foreground=google-red] .button-primary{background:#d93025;color:#fff}[foreground=google-red] .button-primary:active,[foreground=google-red] .button-primary:focus,[foreground=google-red] .button-primary:hover{background:#b31412}[background=google-red]{background-color:#d93025}[foreground=google-red] :focus>:not(.material-icons),[foreground=google-red] :link>:not(.material-icons):hover,[foreground=google-red] a:not(.button) h2,[foreground=google-red] a:not(.button) h3{color:#d93025}.devsite-landing-row[background=google-red]+.devsite-landing-row[background=google-red]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-red],.devsite-landing-row-item-icon-container[foreground=google-red],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-red],.devsite-landing-row-item-list-item-icon-container[foreground=google-red],.devsite-landing-row-item[foreground=google-red] :link h2,.devsite-landing-row-item[foreground=google-red] :link h3{color:#d93025}.devsite-landing-row-item-icon-container[background=google-red],.devsite-landing-row-item-list-item-icon-container[background=google-red]{background:#d93025}[foreground=indigo] .button{color:#3f51b5}[foreground=indigo] .button:active,[foreground=indigo] .button:focus,[foreground=indigo] .button:hover{background:#eceef8}[foreground=indigo] .button-primary{background:#3f51b5;color:#fff}[foreground=indigo] .button-primary:active,[foreground=indigo] .button-primary:focus,[foreground=indigo] .button-primary:hover{background:#303f9f}[background=indigo]{background-color:#3f51b5}[foreground=indigo] :focus>:not(.material-icons),[foreground=indigo] :link>:not(.material-icons):hover,[foreground=indigo] a:not(.button) h2,[foreground=indigo] a:not(.button) h3{color:#3f51b5}.devsite-landing-row[background=indigo]+.devsite-landing-row[background=indigo]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=indigo],.devsite-landing-row-item-icon-container[foreground=indigo],.devsite-landing-row-item-list-item-icon-container[background][foreground=indigo],.devsite-landing-row-item-list-item-icon-container[foreground=indigo],.devsite-landing-row-item[foreground=indigo] :link h2,.devsite-landing-row-item[foreground=indigo] :link h3{color:#3f51b5}.devsite-landing-row-item-icon-container[background=indigo],.devsite-landing-row-item-list-item-icon-container[background=indigo]{background:#3f51b5}[foreground=light-blue] .button{color:#0288d1}[foreground=light-blue] .button:active,[foreground=light-blue] .button:focus,[foreground=light-blue] .button:hover{background:#e6f3fa}[foreground=light-blue] .button-primary{background:#0288d1;color:#fff}[foreground=light-blue] .button-primary:active,[foreground=light-blue] .button-primary:focus,[foreground=light-blue] .button-primary:hover{background:#01579b}[background=light-blue]{background-color:#0288d1}[foreground=light-blue] :focus>:not(.material-icons),[foreground=light-blue] :link>:not(.material-icons):hover,[foreground=light-blue] a:not(.button) h2,[foreground=light-blue] a:not(.button) h3{color:#0288d1}.devsite-landing-row[background=light-blue]+.devsite-landing-row[background=light-blue]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=light-blue],.devsite-landing-row-item-icon-container[foreground=light-blue],.devsite-landing-row-item-list-item-icon-container[background][foreground=light-blue],.devsite-landing-row-item-list-item-icon-container[foreground=light-blue],.devsite-landing-row-item[foreground=light-blue] :link h2,.devsite-landing-row-item[foreground=light-blue] :link h3{color:#0288d1}.devsite-landing-row-item-icon-container[background=light-blue],.devsite-landing-row-item-list-item-icon-container[background=light-blue]{background:#0288d1}[foreground=nest-theme] .button{color:#00afd8}[foreground=nest-theme] .button:active,[foreground=nest-theme] .button:focus,[foreground=nest-theme] .button:hover{background:#e6f7fb}[foreground=nest-theme] .button-primary{background:#00afd8;color:#fff}[foreground=nest-theme] .button-primary:active,[foreground=nest-theme] .button-primary:focus,[foreground=nest-theme] .button-primary:hover{background:#0096c8}[background=nest-theme]{background-color:#00afd8}[foreground=nest-theme] :focus>:not(.material-icons),[foreground=nest-theme] :link>:not(.material-icons):hover,[foreground=nest-theme] a:not(.button) h2,[foreground=nest-theme] a:not(.button) h3{color:#00afd8}.devsite-landing-row[background=nest-theme]+.devsite-landing-row[background=nest-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=nest-theme],.devsite-landing-row-item-icon-container[foreground=nest-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=nest-theme],.devsite-landing-row-item-list-item-icon-container[foreground=nest-theme],.devsite-landing-row-item[foreground=nest-theme] :link h2,.devsite-landing-row-item[foreground=nest-theme] :link h3{color:#00afd8}.devsite-landing-row-item-icon-container[background=nest-theme],.devsite-landing-row-item-list-item-icon-container[background=nest-theme]{background:#00afd8}[foreground=pink] .button{color:#e52592}[foreground=pink] .button:active,[foreground=pink] .button:focus,[foreground=pink] .button:hover{background:#fce9f4}[foreground=pink] .button-primary{background:#e52592;color:#fff}[foreground=pink] .button-primary:active,[foreground=pink] .button-primary:focus,[foreground=pink] .button-primary:hover{background:#b80672}[background=pink]{background-color:#e52592}[foreground=pink] :focus>:not(.material-icons),[foreground=pink] :link>:not(.material-icons):hover,[foreground=pink] a:not(.button) h2,[foreground=pink] a:not(.button) h3{color:#e52592}.devsite-landing-row[background=pink]+.devsite-landing-row[background=pink]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=pink],.devsite-landing-row-item-icon-container[foreground=pink],.devsite-landing-row-item-list-item-icon-container[background][foreground=pink],.devsite-landing-row-item-list-item-icon-container[foreground=pink],.devsite-landing-row-item[foreground=pink] :link h2,.devsite-landing-row-item[foreground=pink] :link h3{color:#e52592}.devsite-landing-row-item-icon-container[background=pink],.devsite-landing-row-item-list-item-icon-container[background=pink]{background:#e52592}[foreground=purple] .button{color:#9334e6}[foreground=purple] .button:active,[foreground=purple] .button:focus,[foreground=purple] .button:hover{background:#f4ebfd}[foreground=purple] .button-primary{background:#9334e6;color:#fff}[foreground=purple] .button-primary:active,[foreground=purple] .button-primary:focus,[foreground=purple] .button-primary:hover{background:#7627bb}[background=purple]{background-color:#9334e6}[foreground=purple] :focus>:not(.material-icons),[foreground=purple] :link>:not(.material-icons):hover,[foreground=purple] a:not(.button) h2,[foreground=purple] a:not(.button) h3{color:#9334e6}.devsite-landing-row[background=purple]+.devsite-landing-row[background=purple]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=purple],.devsite-landing-row-item-icon-container[foreground=purple],.devsite-landing-row-item-list-item-icon-container[background][foreground=purple],.devsite-landing-row-item-list-item-icon-container[foreground=purple],.devsite-landing-row-item[foreground=purple] :link h2,.devsite-landing-row-item[foreground=purple] :link h3{color:#9334e6}.devsite-landing-row-item-icon-container[background=purple],.devsite-landing-row-item-list-item-icon-container[background=purple]{background:#9334e6}[foreground=stadia-theme] .button{color:#9b0063}[foreground=stadia-theme] .button:active,[foreground=stadia-theme] .button:focus,[foreground=stadia-theme] .button:hover{background:#f5e6ef}[foreground=stadia-theme] .button-primary{background:#9b0063;color:#fff}[foreground=stadia-theme] .button-primary:active,[foreground=stadia-theme] .button-primary:focus,[foreground=stadia-theme] .button-primary:hover{background:#680039}[background=stadia-theme]{background-color:#9b0063}[foreground=stadia-theme] :focus>:not(.material-icons),[foreground=stadia-theme] :link>:not(.material-icons):hover,[foreground=stadia-theme] a:not(.button) h2,[foreground=stadia-theme] a:not(.button) h3{color:#9b0063}.devsite-landing-row[background=stadia-theme]+.devsite-landing-row[background=stadia-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=stadia-theme],.devsite-landing-row-item-icon-container[foreground=stadia-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=stadia-theme],.devsite-landing-row-item-list-item-icon-container[foreground=stadia-theme],.devsite-landing-row-item[foreground=stadia-theme] :link h2,.devsite-landing-row-item[foreground=stadia-theme] :link h3{color:#9b0063}.devsite-landing-row-item-icon-container[background=stadia-theme],.devsite-landing-row-item-list-item-icon-container[background=stadia-theme]{background:#9b0063}[foreground=teal] .button{color:#009688}[foreground=teal] .button:active,[foreground=teal] .button:focus,[foreground=teal] .button:hover{background:#e6f5f3}[foreground=teal] .button-primary{background:#009688;color:#fff}[foreground=teal] .button-primary:active,[foreground=teal] .button-primary:focus,[foreground=teal] .button-primary:hover{background:#00796b}[background=teal]{background-color:#009688}[foreground=teal] :focus>:not(.material-icons),[foreground=teal] :link>:not(.material-icons):hover,[foreground=teal] a:not(.button) h2,[foreground=teal] a:not(.button) h3{color:#009688}.devsite-landing-row[background=teal]+.devsite-landing-row[background=teal]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=teal],.devsite-landing-row-item-icon-container[foreground=teal],.devsite-landing-row-item-list-item-icon-container[background][foreground=teal],.devsite-landing-row-item-list-item-icon-container[foreground=teal],.devsite-landing-row-item[foreground=teal] :link h2,.devsite-landing-row-item[foreground=teal] :link h3{color:#009688}.devsite-landing-row-item-icon-container[background=teal],.devsite-landing-row-item-list-item-icon-container[background=teal]{background:#009688}[foreground=youtube-theme] .button{color:red}[foreground=youtube-theme] .button:active,[foreground=youtube-theme] .button:focus,[foreground=youtube-theme] .button:hover{background:#ffe6e6}[foreground=youtube-theme] .button-primary{background:red;color:#fff}[foreground=youtube-theme] .button-primary:active,[foreground=youtube-theme] .button-primary:focus,[foreground=youtube-theme] .button-primary:hover{background:#c20000}[background=youtube-theme]{background-color:red}[foreground=youtube-theme] :focus>:not(.material-icons),[foreground=youtube-theme] :link>:not(.material-icons):hover,[foreground=youtube-theme] a:not(.button) h2,[foreground=youtube-theme] a:not(.button) h3{color:red}.devsite-landing-row[background=youtube-theme]+.devsite-landing-row[background=youtube-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=youtube-theme],.devsite-landing-row-item-icon-container[foreground=youtube-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=youtube-theme],.devsite-landing-row-item-list-item-icon-container[foreground=youtube-theme],.devsite-landing-row-item[foreground=youtube-theme] :link h2,.devsite-landing-row-item[foreground=youtube-theme] :link h3{color:red}.devsite-landing-row-item-icon-container[background=youtube-theme],.devsite-landing-row-item-list-item-icon-container[background=youtube-theme]{background:red}[foreground=cyan] .button{color:#12b5cb}[foreground=cyan] .button:active,[foreground=cyan] .button:focus,[foreground=cyan] .button:hover{background:#e7f8fa}[foreground=cyan] .button-primary{background:#12b5cb;color:#202124}[foreground=cyan] .button-primary:active,[foreground=cyan] .button-primary:focus,[foreground=cyan] .button-primary:hover{background:#098591}[background=cyan]{background-color:#43cde6}[foreground=cyan] :focus>:not(.material-icons),[foreground=cyan] :link>:not(.material-icons):hover,[foreground=cyan] a:not(.button) h2,[foreground=cyan] a:not(.button) h3{color:#12b5cb}.devsite-landing-row[background=cyan]+.devsite-landing-row[background=cyan]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=cyan],.devsite-landing-row-item-icon-container[foreground=cyan],.devsite-landing-row-item-list-item-icon-container[background][foreground=cyan],.devsite-landing-row-item-list-item-icon-container[foreground=cyan],.devsite-landing-row-item[foreground=cyan] :link h2,.devsite-landing-row-item[foreground=cyan] :link h3{color:#12b5cb}.devsite-landing-row-item-icon-container[background=cyan],.devsite-landing-row-item-list-item-icon-container[background=cyan]{background:#43cde6}[foreground=google-yellow] .button{color:#f9ab00}[foreground=google-yellow] .button:active,[foreground=google-yellow] .button:focus,[foreground=google-yellow] .button:hover{background:#fef7e6}[foreground=google-yellow] .button-primary{background:#f9ab00;color:#202124}[foreground=google-yellow] .button-primary:active,[foreground=google-yellow] .button-primary:focus,[foreground=google-yellow] .button-primary:hover{background:#ea8600}[background=google-yellow]{background-color:#fcc934}[foreground=google-yellow] :focus>:not(.material-icons),[foreground=google-yellow] :link>:not(.material-icons):hover,[foreground=google-yellow] a:not(.button) h2,[foreground=google-yellow] a:not(.button) h3{color:#f9ab00}.devsite-landing-row[background=google-yellow]+.devsite-landing-row[background=google-yellow]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-yellow],.devsite-landing-row-item-icon-container[foreground=google-yellow],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-yellow],.devsite-landing-row-item-list-item-icon-container[foreground=google-yellow],.devsite-landing-row-item[foreground=google-yellow] :link h2,.devsite-landing-row-item[foreground=google-yellow] :link h3{color:#f9ab00}.devsite-landing-row-item-icon-container[background=google-yellow],.devsite-landing-row-item-list-item-icon-container[background=google-yellow]{background:#fcc934}[foreground=light-green] .button{color:#8bc34a}[foreground=light-green] .button:active,[foreground=light-green] .button:focus,[foreground=light-green] .button:hover{background:#f3f9ed}[foreground=light-green] .button-primary{background:#8bc34a;color:#202124}[foreground=light-green] .button-primary:active,[foreground=light-green] .button-primary:focus,[foreground=light-green] .button-primary:hover{background:#689f38}[background=light-green]{background-color:#aed581}[foreground=light-green] :focus>:not(.material-icons),[foreground=light-green] :link>:not(.material-icons):hover,[foreground=light-green] a:not(.button) h2,[foreground=light-green] a:not(.button) h3{color:#8bc34a}.devsite-landing-row[background=light-green]+.devsite-landing-row[background=light-green]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=light-green],.devsite-landing-row-item-icon-container[foreground=light-green],.devsite-landing-row-item-list-item-icon-container[background][foreground=light-green],.devsite-landing-row-item-list-item-icon-container[foreground=light-green],.devsite-landing-row-item[foreground=light-green] :link h2,.devsite-landing-row-item[foreground=light-green] :link h3{color:#8bc34a}.devsite-landing-row-item-icon-container[background=light-green],.devsite-landing-row-item-list-item-icon-container[background=light-green]{background:#aed581}[foreground=orange] .button{color:#e8710a}[foreground=orange] .button:active,[foreground=orange] .button:focus,[foreground=orange] .button:hover{background:#fdf1e7}[foreground=orange] .button-primary{background:#e8710a;color:#202124}[foreground=orange] .button-primary:active,[foreground=orange] .button-primary:focus,[foreground=orange] .button-primary:hover{background:#c26401}[background=orange]{background-color:#fcad70}[foreground=orange] :focus>:not(.material-icons),[foreground=orange] :link>:not(.material-icons):hover,[foreground=orange] a:not(.button) h2,[foreground=orange] a:not(.button) h3{color:#e8710a}.devsite-landing-row[background=orange]+.devsite-landing-row[background=orange]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=orange],.devsite-landing-row-item-icon-container[foreground=orange],.devsite-landing-row-item-list-item-icon-container[background][foreground=orange],.devsite-landing-row-item-list-item-icon-container[foreground=orange],.devsite-landing-row-item[foreground=orange] :link h2,.devsite-landing-row-item[foreground=orange] :link h3{color:#e8710a}.devsite-landing-row-item-icon-container[background=orange],.devsite-landing-row-item-list-item-icon-container[background=orange]{background:#fcad70}[foreground=white] .button{color:#fff}[foreground=white] .button:active,[foreground=white] .button:focus,[foreground=white] .button:hover{background:#fff}[foreground=white] .button-primary{background:#fff;color:#202124}[foreground=white] .button-primary:active,[foreground=white] .button-primary:focus,[foreground=white] .button-primary:hover{background:#202124}[background=white]{background-color:#fff}[foreground=white] :focus>:not(.material-icons),[foreground=white] :link>:not(.material-icons):hover,[foreground=white] a:not(.button) h2,[foreground=white] a:not(.button) h3{color:#039be5}.devsite-landing-row[background=white]+.devsite-landing-row[background=white]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=white],.devsite-landing-row-item-icon-container[foreground=white],.devsite-landing-row-item-list-item-icon-container[background][foreground=white],.devsite-landing-row-item-list-item-icon-container[foreground=white],.devsite-landing-row-item[foreground=white] :link h2,.devsite-landing-row-item[foreground=white] :link h3{color:#039be5}.devsite-landing-row-item-icon-container[background=white],.devsite-landing-row-item-list-item-icon-container[background=white]{background:#fff}[background=cyan] .devsite-landing-row-description,[background=cyan] .devsite-landing-row-item-icon-container:not([foreground]),[background=cyan] :link:not(.button),[background=cyan]:not(.devsite-landing-row-cards) h3,[background=cyan]:not([foreground]):not(.devsite-landing-row-cards),[background=cyan] :visited:not(.button),[background=cyan] h2{color:#202124}[background=cyan] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=cyan] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=cyan] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=cyan] :focus .devsite-landing-row-item-icon-container[background],[background=cyan] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=cyan] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=cyan] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=cyan] :link .devsite-landing-row-item-list-item-description h4+p,[background=google-yellow] .devsite-landing-row-description,[background=google-yellow] .devsite-landing-row-item-icon-container:not([foreground]),[background=google-yellow] :link:not(.button),[background=google-yellow]:not(.devsite-landing-row-cards) h3,[background=google-yellow]:not([foreground]):not(.devsite-landing-row-cards),[background=google-yellow] :visited:not(.button),[background=google-yellow] h2{color:#202124}[background=google-yellow] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=google-yellow] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=google-yellow] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=google-yellow] :focus .devsite-landing-row-item-icon-container[background],[background=google-yellow] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=google-yellow] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=google-yellow] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=google-yellow] :link .devsite-landing-row-item-list-item-description h4+p,[background=light-green] .devsite-landing-row-description,[background=light-green] .devsite-landing-row-item-icon-container:not([foreground]),[background=light-green] :link:not(.button),[background=light-green]:not(.devsite-landing-row-cards) h3,[background=light-green]:not([foreground]):not(.devsite-landing-row-cards),[background=light-green] :visited:not(.button),[background=light-green] h2{color:#202124}[background=light-green] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=light-green] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=light-green] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=light-green] :focus .devsite-landing-row-item-icon-container[background],[background=light-green] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=light-green] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=light-green] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=light-green] :link .devsite-landing-row-item-list-item-description h4+p,[background=orange] .devsite-landing-row-description,[background=orange] .devsite-landing-row-item-icon-container:not([foreground]),[background=orange] :link:not(.button),[background=orange]:not(.devsite-landing-row-cards) h3,[background=orange]:not([foreground]):not(.devsite-landing-row-cards),[background=orange] :visited:not(.button),[background=orange] h2{color:#202124}[background=orange] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=orange] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=orange] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=orange] :focus .devsite-landing-row-item-icon-container[background],[background=orange] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=orange] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=orange] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=orange] :link .devsite-landing-row-item-list-item-description h4+p,[background=white] .devsite-landing-row-description,[background=white] .devsite-landing-row-item-icon-container:not([foreground]),[background=white] :link:not(.button),[background=white]:not(.devsite-landing-row-cards) h3,[background=white]:not([foreground]):not(.devsite-landing-row-cards),[background=white] :visited:not(.button),[background=white] h2{color:#202124}[background=white] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=white] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:hsla(0,0%,100%,.7)}[background=white] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=white] :focus .devsite-landing-row-item-icon-container[background],[background=white] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=white] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=white] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}[background=white] :link .devsite-landing-row-item-list-item-description h4+p{color:#202124}body[theme=white] devsite-user div.devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=white] devsite-user div.devsite-user-dialog .devsite-user-dialog-photo{background-color:#1a73e8;color:#fff}body[theme=cloud-theme] .devsite-feedback-item-icon-container.devsite-feedback-item-icon-color,body[theme=white] .devsite-feedback-item-icon-container.devsite-feedback-item-icon-color{background-color:#1a73e8}devsite-content{display:block;position:relative}body[layout=docs] devsite-content{align-self:start;-ms-grid-column:3;grid-column:2;-ms-grid-row:1;grid-row:1;margin:24px 0;max-width:936px;min-width:0}body[layout=docs] .devsite-article{background-color:#fff;padding:40px}body[layout=docs][type=landing] .devsite-article{padding:0}.devsite-article-meta{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;margin:0 0 20px}.devsite-banner+.devsite-article-meta{margin-top:-16px}body[layout=full] .devsite-article-meta,body[type=landing] .devsite-article-meta{margin:0}devsite-feedback[position=header]{display:block;position:relative;top:-4px}[dir=ltr] devsite-feedback[position=header]{float:right;margin-left:24px}[dir=rtl] devsite-feedback[position=header]{float:left;margin-right:24px}body[layout=full] devsite-feedback[position=header],body[type=landing] devsite-feedback[position=header]{display:none}@media screen and (max-width:840px){body[layout=docs] devsite-content{margin:0}body[layout=docs] .devsite-article{padding:24px}.devsite-banner+.devsite-article-meta{margin-top:0}}@media screen and (max-width:600px){body[layout=docs] .devsite-article{padding:16px}.devsite-article-meta{display:block;margin:0 0 12px}[dir] devsite-feedback[position=header]{float:none;margin:0 0 12px;position:static}}#devsite-support-form{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:0 0 0 -40px}#devsite-support-form>*{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:0 0 0 40px}.devsite-support-form-error{color:#dd2c00}.devsite-support-form-hidden{display:none}.devsite-support-form-field{margin:0 0 8px}.devsite-support-form-field input:not([type=checkbox]):not([type=radio]),.devsite-support-form-field select,.devsite-support-form-field textarea{width:100%}.devsite-support-form-cc{color:#5f6368;display:block;font-size:13px}#devsite-support-form>.devsite-support-form-half{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}.devsite-support-quota{font:italic 400 12px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:8px}.devsite-support-quota-help{font-size:18px;margin-left:4px;vertical-align:top}.devsite-support-quota-help:after{content:"help";color:#bdc1c6}@media screen and (max-width:600px){#devsite-support-form{display:block}}.devsite-404-wrapper,.devsite-offline-wrapper{margin:0 auto;max-width:804px;position:relative;text-align:center}.devsite-404-header,.devsite-offline-header{margin:120px 24px 20px;position:relative;z-index:2}.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin:0 0 160px;position:relative;z-index:1}.devsite-404-search devsite-search .devsite-popout-result{max-height:304px}.devsite-offline-reload{text-align:center}.devsite-404-header h3{font:400 64px/64px Roboto Mono,monospace}.devsite-offline-header h3{font:400 32px/48px Roboto Mono,monospace}.devsite-404-wrapper devsite-search,.devsite-404-wrapper devsite-search .devsite-searchbox,[dir=rtl] .devsite-404-wrapper devsite-search,[dir=rtl] .devsite-404-wrapper devsite-search .devsite-searchbox{margin:0;width:100%}.devsite-404-wrapper devsite-search .devsite-search-button{display:none}.devsite-offline-wrapper .devsite-offline-suggestions{text-align:left}[dir=rtl] .devsite-offline-wrapper .devsite-offline-suggestions{text-align:right}.devsite-offline-wrapper .devsite-offline-suggestions h3,.devsite-offline-wrapper .devsite-offline-suggestions ul{margin:0}.devsite-404-wrapper .devsite-404-links{border-top:1px solid #dadce0;margin:0 calc(50% - 50vw) 40px;padding:0 calc(50vw - 50%);text-align:left}[dir=rtl] .devsite-404-wrapper .devsite-404-links{text-align:right}.devsite-404-wrapper .devsite-404-links ul{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;list-style:none;padding:0}.devsite-404-wrapper .devsite-404-links li{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4);margin-left:24px}[dir=rtl] .devsite-404-wrapper .devsite-404-links li{margin-left:0;margin-right:24px}.devsite-404-wrapper .devsite-404-links li:nth-of-type(4n+1){margin-left:0}[dir=rtl] .devsite-404-wrapper .devsite-404-links li:nth-of-type(4n+1){margin-right:0}@media screen and (max-width:840px){.devsite-404-header,.devsite-offline-header{margin-top:40px}.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin-bottom:80px}.devsite-404-wrapper .devsite-404-links li{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-404-wrapper .devsite-404-links li:nth-of-type(odd){margin-left:0}[dir=rtl] .devsite-404-wrapper .devsite-404-links li:nth-of-type(odd){margin-right:0}}@media screen and (max-width:600px){.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin-bottom:40px}}devsite-dynamic-content .devsite-card-image-bg{background-image:url(../images/dynamic-content-card-default.png)}@media print{.caution,.caution a,.devsite-banner,.devsite-banner a,.dogfood,.dogfood a,.key-point,.key-point a,.key-term,.key-term a,.note,.note a,.objective,.objective a,.prettyprint a,.special,.special a,.success,.success a,.warning,.warning a,:link,:visited,a .atn,a .atv,a .com,a .dec,a .kwd,a .lit,a .pln,a .pun,a .str,a .tag,a .typ,a code,aside,aside :link,aside :visited,body,code,h1,h1 code,h2,h2 code,h3,h3 code,h4,h4 code,h5,h5 code,h6,h6 code,html,pre,pre .atn,pre .atv,pre .com,pre .dec,pre .kwd,pre .lit,pre .pln,pre .pun,pre .str,pre .tag,pre .typ,td,td code,th,th :link,th :visited,th code,var{color:#000!important;padding-left:0!important;padding-right:0!important}#gc-wrapper{margin:0!important}devsite-expandable>:not(.showalways):not(.exw-control):not(.exw-expanded-content):not(.expand-control){display:block!important}:link,:visited{text-decoration:underline}.devsite-article-meta,.devsite-banner-confidential .button,.devsite-book-nav-bg,.devsite-code-buttons-container,devsite-book-nav,devsite-feedback,devsite-footer-linkboxes,devsite-footer-promos,devsite-footer-utility,devsite-googler-buttons,devsite-header,devsite-page-rating,devsite-toc{display:none!important}.devsite-article,.devsite-main-content,devsite-content{background:0!important;border:0!important;-webkit-box-shadow:none!important;box-shadow:none!important;display:block!important;margin:0!important;max-width:none!important;padding:0!important;width:auto!important}.devsite-banner{margin-top:0}.attempt-left,.attempt-right,.video-wrapper{float:none;margin:16px 0}img,video{display:block!important;page-break-inside:avoid!important}.devsite-main-content a[href]:after{content:" (" attr(href) ")";display:inline-block;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-width:100%;word-wrap:break-word}}@page{margin:.75in}.devsite-product-platform-row{margin:8px 0;padding:0 24px}.devsite-header-no-lower-tabs .devsite-product-platform-row{margin-top:-12px;padding-bottom:24px}.devsite-platform-container{display:inline-block}.devsite-platform-container+.devsite-platform-container{margin-left:16px}.devsite-platform-icon-container{background:#fff;-webkit-border-radius:50%;border-radius:50%;height:40px;margin:0 auto;width:40px}.devsite-platform-icon{color:#5f6368;font-size:24px;height:24px;margin:8px;width:24px}@media screen and (max-width:1000px){div.devsite-collapsible-section,div.devsite-header-background{background-image:none}}@media screen and (max-width:600px){.devsite-product-platform-row{padding:0 16px}}.devsite-landing-row{padding:40px 0}.devsite-landing-row-group,.devsite-landing-row-html{margin:0 auto;max-width:1520px;padding:0 40px}.devsite-landing-row:not([background]){background-color:#fff}.devsite-landing-row:not([background])+.devsite-landing-row:not([background]),.devsite-landing-row[background=grey]+.devsite-landing-row[background=grey],.devsite-landing-row[background=theme]+.devsite-landing-row[background=theme]{padding-top:0}.devsite-landing-row:not([background])+.devsite-landing-row.devsite-landing-row-cta{padding-top:40px}@media screen and (max-width:840px){.devsite-landing-row{padding:24px 0}.devsite-landing-row-group,.devsite-landing-row-html{padding:0 24px}}@media screen and (max-width:600px){.devsite-landing-row{padding:16px 0}.devsite-landing-row-group,.devsite-landing-row-html{padding:0 16px}}.devsite-landing-row-cta{text-align:center}.devsite-landing-row-cta .devsite-landing-row-item{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.devsite-landing-row-cta h3{font:300 34px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400;letter-spacing:-.01em;margin-bottom:16px}.devsite-landing-row-cta h3+.devsite-landing-row-item-buttons{margin-top:8px}.devsite-landing-row-cta.devsite-landing-row-1-up .devsite-landing-row-item-description{margin:0}.devsite-landing-row-header{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end;margin:0 auto 32px;max-width:1520px;padding:0 40px}.devsite-landing-row-header+.devsite-landing-row-group{margin-top:32px}.devsite-landing-row-header-text{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto}.devsite-landing-row-header-text>h2{margin:0}.devsite-landing-row-description{font:18px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{color:#5f6368}h2+.devsite-landing-row-description{margin-top:16px}.devsite-landing-row-header-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;margin:-4px}.devsite-landing-row-header-buttons>.button{margin:4px}.devsite-landing-row-header-centered .devsite-landing-row-header{display:block;text-align:center}.devsite-landing-row-header-centered .devsite-landing-row-description{margin-left:auto;margin-right:auto;max-width:856px}.devsite-landing-row-header-centered .devsite-landing-row-header-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:24px}@media screen and (max-width:840px){.devsite-landing-row-header{padding:0 24px}}@media screen and (max-width:600px){.devsite-landing-row-header{display:block;padding:0 16px}.devsite-landing-row-header-text+.devsite-landing-row-header-buttons{display:block;margin:16px 0 0 -4px}}.devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-landing-row-column>.devsite-landing-row-item:not(:first-child){margin-top:32px}.devsite-landing-row-column,.devsite-landing-row-item{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;max-width:100%;min-width:0}.devsite-landing-row-item-hidden{visibility:hidden}.devsite-landing-row-1-up .devsite-landing-row-item{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-row-1-up.devsite-landing-row-100 .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-column:not(:first-child),.devsite-landing-row-item:not(:first-child){margin-left:24px}[dir=rtl] .devsite-landing-row-column:not(:first-child),[dir=rtl] .devsite-landing-row-item:not(:first-child){margin-left:0;margin-right:24px}.devsite-landing-row-column>.devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none;margin-left:0}[dir=rtl] .devsite-landing-row-column>.devsite-landing-row-item{margin-right:0}@media screen and (max-width:840px){.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2)}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),[dir=rtl] .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:0 24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(3){margin:24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(4),.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin:24px 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(4),[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin:24px 24px 0 0}.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:0 24px 0 0}.devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item{width:100%}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media){-webkit-flex-basis:100%;flex-basis:100%;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;margin-left:0}.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description{display:block}.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item{-webkit-flex-basis:auto;flex-basis:auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{width:-webkit-calc((100% - 24px)/2);width:calc((100% - 24px)/2)}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2){margin:0 24px 0 0}.devsite-landing-row-3-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3)~.devsite-landing-row-item-hidden,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item-hidden:nth-of-type(n+3){display:none}}@media screen and (max-width:600px){.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-group{display:block}.devsite-landing-row-1-up .devsite-landing-row-column,.devsite-landing-row-1-up .devsite-landing-row-item,.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media){-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-2-up .devsite-landing-row-column,.devsite-landing-row-3-up .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(2n),.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2n),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-flex-basis:100%;flex-basis:100%}.devsite-landing-row-logos .devsite-landing-row-column,.devsite-landing-row-logos .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2)}.devsite-landing-row-logos .devsite-landing-row-column:nth-child(n+3),.devsite-landing-row-logos .devsite-landing-row-item:nth-child(n+3){margin:24px 0 0}.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-column,.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 32px)/3);flex-basis:calc((100% - 32px)/3)}.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-column:not(:first-child),.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-item:not(:first-child){margin:0}.devsite-landing-row-logos .devsite-landing-row-item:nth-child(2n){margin-left:24px}[dir=rtl] .devsite-landing-row-logos .devsite-landing-row-item:nth-child(2n){margin-left:0;margin-right:24px}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{width:100%}.devsite-landing-row-4-up.devsite-landing-row-logos .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-logos .devsite-landing-row-item-hidden:nth-of-type(3)~.devsite-landing-row-item-hidden,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-hidden{display:none}}.devsite-landing-row-item-media{margin:0 0 32px;min-width:0}.devsite-landing-row-item-code devsite-code,.devsite-landing-row-item[background] .devsite-landing-row-item-media{margin:0}.devsite-landing-row-item-video{display:block}.devsite-landing-row-item[background] .devsite-landing-row-item-description{padding:16px}.devsite-landing-row-item-body,.devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;min-width:0;width:100%}.devsite-landing-row-item .devsite-landing-row-item-buttons{margin:auto 0 -8px -12px;padding-top:8px}[dir=rtl] .devsite-landing-row-item .devsite-landing-row-item-buttons{margin:auto -12px -8px 0}.devsite-landing-row-1-up .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((200% - 40px)/3);flex:0 0 calc((200% - 40px)/3);margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row-1-up .devsite-landing-row-item-description{display:block;-webkit-box-flex:0;-webkit-flex:0 1 -webkit-calc((200% - 40px)/3);flex:0 1 calc((200% - 40px)/3);margin:0 24px 0 0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-75 .devsite-landing-row-item-description,.devsite-landing-row-100 .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.devsite-landing-row-1-up.devsite-landing-row-100 .devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-media-left{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 0 0 40px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 24px 0 0}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description,.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 40px}.devsite-landing-row-50 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-landing-row-67 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 48px)/3);flex:0 0 calc((100% - 48px)/3)}.devsite-landing-row-75 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4)}.devsite-landing-row-100 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:32px 0 0}.devsite-landing-row-item-description h2,.devsite-landing-row-large-headings h3,.devsite-landing-row h3:first-child,.devsite-landing-row h4+p,.devsite-landing-row h4:first-child,.devsite-landing-row h5:first-child,.devsite-landing-row h6:first-child,.devsite-landing-row p:first-child{margin-top:0}.devsite-landing-row-item-description-content>:last-child{margin-bottom:0}.devsite-landing-row-item-centered .devsite-landing-row-item-description-content,.devsite-landing-row-item-centered h3{text-align:center}.devsite-landing-row-item-centered .devsite-landing-row-item-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}@media screen and (max-width:840px){.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-1-up .devsite-landing-row-item-media-left,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2);margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;overflow:hidden}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:unset;-webkit-flex:unset;flex:unset;margin:0 0 32px}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item[background] .devsite-landing-row-item-media{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-description,.devsite-landing-row .devsite-landing-row-item-description{display:block;margin:0 24px 0 0}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-description,[dir=rtl] .devsite-landing-row .devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-4-up .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-landing-row-3-up .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 48px)/3);flex:0 0 calc((100% - 48px)/3)}.devsite-landing-row-1-up .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-item-no-media .devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-description{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{-webkit-box-ordinal-group:2;-webkit-order:1;order:1;margin:0 24px 0 0}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description{margin:0;padding:24px}.devsite-landing-row-logos .devsite-landing-row-item-description{margin:0}.devsite-landing-row-100 .devsite-landing-row-item-media,.devsite-landing-row-100:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:32px 0 0}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-media,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:0 0 32px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-description,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-description{-webkit-box-ordinal-group:3;-webkit-order:2;order:2;width:100%}}@media screen and (max-width:600px){.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-1-up .devsite-landing-row-item-media-left,.devsite-landing-row-item-media,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:0 0 32px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-description,.devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description{padding:16px}.devsite-landing-row-item-no-media:not(:first-child),.devsite-landing-row .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n){margin:24px 0 0}.devsite-landing-row-cta .devsite-landing-row-item-description{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.devsite-landing-row-item-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-left:-12px;padding-top:8px}.devsite-landing-row-item-buttons .button{margin:4px 4px 4px 12px}.devsite-landing-row-item-buttons .button>.material-icons{top:-1px}.devsite-landing-row-item-buttons .button-white:not(.button-raised),.devsite-landing-row-item-buttons .button-white:not(.button-raised)+.button-white:not(.button-raised){margin:0 4px}.devsite-landing-row-cta .devsite-landing-row-item-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:24px 0 0}p+.devsite-landing-row-item-buttons{margin-top:-8px}.devsite-landing-row-item-custom-image{padding:0 0 56.25%;position:relative}.devsite-landing-row-item-image.devsite-landing-row-item-custom-image:not([background]){background:#455a64}.devsite-landing-row-item-custom-image[background=grey]{background:#f1f3f4}.devsite-landing-row-item-custom-image[background=white]{background:#fff}.devsite-landing-row-no-image-background .devsite-landing-row-item-custom-image:not([background]){background:0}.devsite-landing-row-item-custom-image-icon-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:100%;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:absolute;width:100%}.devsite-landing-row-item-custom-image-icon-container[background]{background:0}.devsite-landing-row-item-custom-image-icon{color:#fff}.devsite-landing-row-item-custom-image[background=grey]:not([foreground]) .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-item-custom-image[background=white]:not([foreground]) .devsite-landing-row-item-custom-image-icon{color:#5f6368}.devsite-landing-row-item-custom-image-icon.material-icons{opacity:.8}.devsite-landing-row-1-up .devsite-landing-row-item-custom-image-icon{font-size:256px;max-height:256px;width:256px}.devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-50 .devsite-landing-row-item-custom-image-icon{font-size:192px;max-height:192px;width:192px}.devsite-landing-row-3-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-67 .devsite-landing-row-item-custom-image-icon,[layout=docs] .devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon{font-size:128px;max-height:128px;width:128px}.devsite-landing-row-4-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-75 .devsite-landing-row-item-custom-image-icon,[layout=docs] .devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon{font-size:96px;max-height:96px;width:96px}@media screen and (max-width:840px){.devsite-landing-row-1-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-3-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-4-up .devsite-landing-row-item-custom-image-icon{font-size:128px;max-height:128px;width:128px}}.devsite-landing-row-1-up .devsite-landing-row-item-description[icon-position=left],.devsite-landing-row-item-description[icon-position]{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-row-item-description[icon-position=top]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-item-icon-container{color:#5f6368;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:56px;margin:0 20px 8px 0;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s;width:56px}[dir=rtl] .devsite-landing-row-item-icon-container{margin:0 0 8px 20px}.devsite-landing-row-item-icon{font-size:48px;height:48px;margin:0 0 0 4px;width:48px}[dir=rtl] .devsite-landing-row-item-icon{margin:0 4px 0 0}.devsite-landing-row-item-icon-container[background]{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px}.devsite-landing-row-item-icon-container[background] .devsite-landing-row-item-icon{font-size:36px;height:36px;margin:0;width:36px}[background] .devsite-landing-row-item-icon-container:not([foreground]){color:#fff}[background=grey] .devsite-landing-row-item-icon-container:not([foreground]){color:#5f6368}:focus .devsite-landing-row-item-icon-container,:link .devsite-landing-row-item-icon-container:hover{color:#1a73e8}[foreground] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[foreground] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:hsla(0,0%,100%,.7)}.devsite-landing-row-item-icon-container[background=grey]:not([foreground]),.devsite-landing-row-item-icon-container[background=white]:not([foreground]),.devsite-landing-row-item-icon-container[foreground=grey]{color:#5f6368}.devsite-landing-row-item-icon-container[background=grey]{background:#f1f3f4}.devsite-landing-row-item-icon-container[background=white]{background:#fff}.devsite-landing-row-item-icon-container[background][foreground=white],.devsite-landing-row-item-icon-container[foreground=white]{color:#fff}:focus .devsite-landing-row-item-icon-container[background][foreground=grey],:link .devsite-landing-row-item-icon-container[background][foreground=grey]:hover{color:#5f6368}:focus .devsite-landing-row-item-icon-container[background][foreground=white],:link .devsite-landing-row-item-icon-container[background][foreground=white]:hover{color:#fff}:focus .devsite-landing-row-item-icon-container[background],:link .devsite-landing-row-item-icon-container[background]:hover{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}[background] :focus .devsite-landing-row-item-icon-container[background],[background] :link .devsite-landing-row-item-icon-container[background]:hover{color:#fff}[background=grey] :focus .devsite-landing-row-item-icon-container[background],[background=grey] :link .devsite-landing-row-item-icon-container[background]:hover{color:#5f6368}.devsite-landing-row-item-image{-webkit-align-self:flex-start;align-self:flex-start}.devsite-landing-row-item-image:not([background]){background:#e8eaed}.devsite-landing-row-item-image img{vertical-align:middle;width:100%}.devsite-landing-row-item-image a{display:block}.devsite-landing-row-no-image-background .devsite-landing-row-item-image:not([background]){background:0}.devsite-landing-row-item-labels>a,.devsite-landing-row-item-labels>span{display:inline-block;font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin-bottom:8px;text-transform:uppercase}.devsite-landing-row-item-labels>a+a,.devsite-landing-row-item-labels>a+span,.devsite-landing-row-item-labels>span+a,.devsite-landing-row-item-labels>span+span{margin-left:8px}.devsite-landing-row-item-labels>a[background],.devsite-landing-row-item-labels>span[background]{-webkit-border-radius:4px;border-radius:4px;margin-bottom:16px;padding:4px 8px}.devsite-landing-row-item-description-callout{font-weight:700}.devsite-landing-row-item-description-feature{margin-top:16px;position:relative}.devsite-landing-row-item-description-feature+.devsite-landing-row-item-description-feature{margin:0}.devsite-landing-row-item-description-feature-link{border-bottom:1px solid #e8eaed;font-weight:500;padding:12px 0 11px}.devsite-landing-row-item-description-feature-tooltip{background:#455a64;color:hsla(0,0%,100%,.7);-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;min-width:200px;opacity:0;padding:24px;position:absolute;-webkit-transition:opacity .2s,visibility .2s;-o-transition:opacity .2s,visibility .2s;transition:opacity .2s,visibility .2s;visibility:hidden;width:67%;z-index:1020}.no-touch .devsite-landing-row-item-description-feature-link:hover+.devsite-landing-row-item-description-feature-tooltip{opacity:1;visibility:visible}.devsite-landing-row-item-description-feature-tooltip:before{border-bottom:8px solid #455a64;border-left:8px solid transparent;border-right:8px solid transparent;content:"";position:absolute;top:-8px}.devsite-landing-row-item-description-feature-tooltip h3,.devsite-landing-row-large-headings .devsite-landing-row-item-description-feature-tooltip h3{color:#fff;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:8px;padding:0}.devsite-landing-row-item-list{padding:0}.devsite-landing-row-item-description-content+.devsite-landing-row-item-list{margin-top:32px}.devsite-landing-row-item-list-item{list-style:none}.devsite-landing-row-item-list-item-content{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-landing-row-item-list-item-content[icon-position=top]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-item-list-item-icon-container{color:#5f6368;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:40px;margin:0 20px 0 0;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s;width:40px}[dir=rtl] .devsite-landing-row-item-list-item-icon-container{margin:0 0 0 20px}.devsite-landing-row-item-list-item-icon{font-size:32px;height:32px;width:32px}.devsite-landing-row-item-list-item-icon-container[background]{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.devsite-landing-row-item-list-item-icon-container[background] .devsite-landing-row-item-list-item-icon{font-size:24px;height:24px;width:24px}.devsite-landing-row-item-list h4{font:400 16px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row .devsite-landing-row-item-list h4{margin:0 0 4px}.devsite-landing-row-item-list-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;font:400 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-width:-webkit-calc(100% - 40px);max-width:calc(100% - 40px)}@media screen and (max-width:840px){.devsite-landing-row-item-list-item-description{max-width:none}}.devsite-landing-row-item-list-item:not(:last-child) .devsite-landing-row-item-list-item-description-content{margin-bottom:24px}[background] .devsite-landing-row-item-list-item-icon-container:not([foreground]){color:#fff}.devsite-landing-row-item-list-item-icon-container[background=grey]:not([foreground]),.devsite-landing-row-item-list-item-icon-container[background=white]:not([foreground]),.devsite-landing-row-item-list-item-icon-container[background][foreground=grey],.devsite-landing-row-item-list-item-icon-container[foreground=grey]{color:#5f6368}.devsite-landing-row-item-list-item-icon-container[background=grey]{background:#f1f3f4}.devsite-landing-row-item-list-item-icon-container[background=white]{background:#fff}.devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=white]{color:#fff}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=grey],:link .devsite-landing-row-item-list-item-icon-container[foreground=grey]:hover{color:rgba(154,160,166,.5)}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=white],:link .devsite-landing-row-item-list-item-icon-container[foreground=white]:hover{color:hsla(0,0%,100%,.7)}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=grey],:link .devsite-landing-row-item-list-item-icon-container[background][foreground=grey]:hover{color:#5f6368}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=white],:link .devsite-landing-row-item-list-item-icon-container[background][foreground=white]:hover{color:#fff}.devsite-landing-row-item-list-item a:focus,:link>.devsite-landing-row-item-list-item-content:not(.material-icons):hover,[background] :link>.devsite-landing-row-item-list-item-content:not(.material-icons):hover{text-decoration:none}[foreground] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[foreground] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}:link .devsite-landing-row-item-list-item-description h4+p{color:#202124;text-decoration:none}[background] :link .devsite-landing-row-item-list-item-description h4+p{color:#fff}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background],:link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container[background]{background:hsla(0,0%,100%,.7);-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.devsite-landing-row :focus .devsite-landing-row-item-list-item-description>:first-child,[background] :link .devsite-landing-row-item-list-item-description>:first-child{text-decoration:underline}.devsite-landing-row-logos .devsite-landing-row-item-media{margin:0}.devsite-landing-row-logos .devsite-landing-row-item-custom-image{padding-bottom:96px}body[theme] .devsite-landing-row-logos .devsite-landing-row-item-custom-image:not([background]){background:0}.devsite-landing-row-logos .devsite-landing-row-item-custom-image-icon{opacity:1;width:96px}.devsite-landing-row-logos .devsite-landing-row-item-description{text-align:center}.devsite-landing-row-logos .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4)}@media screen and (max-width:600px){.devsite-landing-row-logos .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}}.devsite-landing-row-cards .devsite-landing-row-item{-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);overflow:hidden;position:relative}.devsite-landing-row-cards .devsite-landing-row-item:not([background]){background-color:#fff}body[theme] .devsite-landing-row-cards[background=theme] :link:not(.button),body[theme] .devsite-landing-row-cards[background=theme] :visited:not(.button){color:#1a73e8}.devsite-landing-row-cards .devsite-landing-row-column{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:not(:first-child){margin-top:24px}.devsite-landing-row-cards .devsite-landing-row-column .devsite-landing-row-item{display:block;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto}.devsite-landing-row-cards .devsite-landing-row-item-media{margin-bottom:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-cards .devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding:16px}.devsite-landing-row-cards .devsite-landing-row-item-buttons{padding-top:16px}.devsite-landing-row-cards .devsite-landing-row-item-no-media{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards[background=theme] .devsite-landing-row-item-icon{color:#5f6368}.devsite-landing-row-cards .devsite-landing-row-item-no-media h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 20px}.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;margin:0}@media screen and (max-width:840px){.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-cards:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;margin:0}.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2);-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2),[dir=rtl] .devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2){margin:0 24px 0 0}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column>.devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:nth-child(n+2){margin:24px 0 0}.devsite-landing-row-cards .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}}@media screen and (max-width:600px){.devsite-landing-row-cards{-webkit-flex-basis:100%;flex-basis:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding-bottom:16px;padding-top:16px}.devsite-landing-row-cards+.devsite-landing-row-cards .devsite-landing-row-header{padding-top:16px}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column>.devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:nth-child(n+2),.devsite-landing-row-cards:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:16px 0 0}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child),.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child),.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:16px 0 0}.devsite-landing-row-cards .devsite-landing-row-item-no-media .devsite-landing-row-item-description{padding-top:16px}}.devsite-landing-row-1-up.devsite-landing-row-marquee,.devsite-landing-row-1-up.devsite-landing-row-marquee[background]{padding:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-header{margin:0;padding:40px 0 0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media{-webkit-align-self:center;align-self:center;margin:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-image{background:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-description{-webkit-align-self:center;align-self:center;padding:40px 0}@media screen and (max-width:840px){.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media{margin:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;overflow:visible;width:100%}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;width:100%}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0}}@media screen and (max-width:600px){.devsite-landing-row-1-up.devsite-landing-row-marquee[background]{margin:0 -16px;padding:0}.devsite-landing-row-1-up.devsite-landing-row-marquee[background] .devsite-landing-row-item-description{padding:32px 16px}}body[layout=full] devsite-code{overflow:visible}body[layout=full] devsite-code:after{background:#f1f3f4;content:"";display:block;height:100%;left:-webkit-calc(50% - 50vw);left:calc(50% - 50vw);position:absolute;top:0;width:100vw;z-index:-1}body[layout=full] devsite-code[dark-code]:after{background:#283142}[background] :link:not(.button),[background] :visited:not(.button){color:inherit}body[type=landing][layout=docs] h2{border-bottom:0;padding-bottom:0}[background]:not([background=grey]) :link>:not(.material-icons):hover,[background]:not([background=grey]) p>a:not(.button){text-decoration:none}.dac-center{-webkit-box-flex:1;-webkit-flex:1;flex:1;text-align:center}.dac-grow-1{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-grow-2{-webkit-box-flex:2;-webkit-flex-grow:2;flex-grow:2}.dac-grow-3{-webkit-box-flex:3;-webkit-flex-grow:3;flex-grow:3}.dac-grow-4{-webkit-box-flex:4;-webkit-flex-grow:4;flex-grow:4}.dac-grow-5{-webkit-box-flex:5;-webkit-flex-grow:5;flex-grow:5}.dac-grow-6{-webkit-box-flex:6;-webkit-flex-grow:6;flex-grow:6}.dac-grow-7{-webkit-box-flex:7;-webkit-flex-grow:7;flex-grow:7}.dac-grow-8{-webkit-box-flex:8;-webkit-flex-grow:8;flex-grow:8}.dac-grow-9{-webkit-box-flex:9;-webkit-flex-grow:9;flex-grow:9}.dac-grow-10{-webkit-box-flex:10;-webkit-flex-grow:10;flex-grow:10}.dac-grow-11{-webkit-box-flex:11;-webkit-flex-grow:11;flex-grow:11}.dac-grow-12{-webkit-box-flex:12;-webkit-flex-grow:12;flex-grow:12}.dac-full-width{width:100%}.dac-hidden-sm-up,.dac-hidden-xs-down.dac-hidden-sm-up,.dac-hidden-xs-up{display:none!important}.dac-flex-spread{-webkit-box-align:baseline;-webkit-align-items:baseline;align-items:baseline;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}@media screen and (max-width:720px){.dac-hidden-xs-down{display:none!important}.dac-hidden-sm-up,.dac-hidden-xs-up{display:block!important}}@media screen and (max-width:1000px){.dac-hidden-sm-down{display:none!important}}@media screen and (min-width:720px) and (max-width:1000px){.dac-hidden-xs-down.dac-hidden-sm-up{display:block!important}}.dac-landing-row-item-height-small{min-height:350px}.dac-landing-row-item-height-medium{min-height:500px}.dac-landing-row-item-height-large{min-height:750px}@media screen and (max-width:1000px){.dac-landing-row-item-height-small{min-height:inherit}.dac-landing-row-item-height-large,.dac-landing-row-item-height-medium{min-height:250px}}.dac-heading-hidden h1,.dac-heading-hidden h2,.dac-heading-hidden h3,.dac-heading-hidden h4,.dac-heading-hidden h5{display:none}.dac-banner-card,.dac-banner-card.dac-landing-row-bg-slate,.dac-landing-row-bg-blob-8,.dac-landing-row.dac-landing-row-collapse{background-clip:content-box}.devsite-landing-row:not([background]).dac-landing-row-bg-blob-8,[background=grey]{background-color:#f7f9fa}.dac-landing-row-bg-mint{background-color:#55ffb5}.dac-landing-row-bg-yellow{background-color:#ffd600}.dac-landing-row-bg-mint,.dac-landing-row-bg-yellow .button.dac-outline-button{border-color:#414141;color:#414141}.dac-landing-row-bg-mint:focus,.dac-landing-row-bg-mint:hover,.dac-landing-row-bg-yellow .button.dac-outline-button:focus,.dac-landing-row-bg-yellow .button.dac-outline-button:hover{background:#414141;border-color:#414141;color:#fff}.dac-landing-row-bg-blue{background-color:#2196f3}.dac-landing-row-bg-blue:link:not(.dac-button),[background=grey]:not(.devsite-landing-row-cards) .dac-landing-row-bg-slate h3{color:#fff}.dac-landing-row-bg-slate,.devsite-landing-row:not([background]).dac-landing-row-bg-slate{background-color:#455a64}.dac-landing-row-bg-green,.dac-landing-row-item-description-bg-green .devsite-landing-row-item-description{background-color:#4b7d2f}.dac-landing-row-bg-blue .dac-button,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p :link,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p :visited,.dac-landing-row-bg-green .dac-button,.dac-landing-row-bg-green .devsite-landing-row-item-body h2,.dac-landing-row-bg-green .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-green .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-green .devsite-landing-row-item-body h3,.dac-landing-row-bg-green .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-green .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-green .devsite-landing-row-item-description-content,.dac-landing-row-bg-green .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-green .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-slate .dac-button,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p :link,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p :visited{color:#fff}.dac-landing-row-bg-blue .dac-button:focus,.dac-landing-row-bg-blue .dac-button:hover,.dac-landing-row-bg-green .dac-button:focus,.dac-landing-row-bg-green .dac-button:hover,.dac-landing-row-bg-slate .dac-button:focus,.dac-landing-row-bg-slate .dac-button:hover{opacity:.4}.dac-landing-row-bg-blue .dac-outline-button,.dac-landing-row-bg-green .dac-outline-button,.dac-landing-row-bg-slate .dac-outline-button{border-color:#fff}.dac-landing-row-bg-blue .dac-outline-button:focus,.dac-landing-row-bg-blue .dac-outline-button:hover,.dac-landing-row-bg-green .dac-outline-button:focus,.dac-landing-row-bg-green .dac-outline-button:hover,.dac-landing-row-bg-slate .dac-outline-button:focus,.dac-landing-row-bg-slate .dac-outline-button:hover{background:#fff;border-color:#fff;color:#414141;opacity:1}.dac-landing-row-bg-mint .dac-outline-button,.dac-landing-row-bg-yellow .dac-outline-button{border-color:#414141;color:#414141}.dac-landing-row-bg-mint .dac-outline-button:focus,.dac-landing-row-bg-mint .dac-outline-button:hover,.dac-landing-row-bg-yellow .dac-outline-button:focus,.dac-landing-row-bg-yellow .dac-outline-button:hover{background:#414141;border-color:#414141;color:#fff}.devsite-banner-announcement,.devsite-banner-announcement :link,.devsite-banner-announcement :visited{background:#eff7cf}.dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}}.dac-banner-card.devsite-landing-row .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row .devsite-landing-row-item-description{padding:24px}}.dac-banner-card.devsite-landing-row .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.dac-banner-card.devsite-landing-row img{display:none;margin-top:32px}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row img{display:block}}.dac-banner-card.devsite-landing-row .dac-banner-card-bg-img-item{background-repeat:no-repeat;-o-background-size:cover;background-size:cover;margin-left:0}.dac-banner{text-align:center}.dac-banner .devsite-landing-row-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center}.dac-banner .devsite-landing-row-item-description{max-width:750px}.dac-banner .devsite-landing-row-item-body h3{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-banner .devsite-landing-row-item-body h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-banner .devsite-landing-row-item-description-content{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-banner-wide{background-color:transparent;background-repeat:no-repeat}.dac-banner-wide img{display:none}@media screen and (max-width:720px){.dac-banner-wide img{display:block;margin:24px auto;width:70%}}.dac-banner-wide .devsite-landing-row-item-no-media .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-banner-wide .devsite-landing-row-item{max-width:50%;padding:32px 0}@media screen and (max-width:720px){.dac-banner-wide .devsite-landing-row-item{padding:0;max-width:none}}.dac-heading-linked.devsite-landing-row-item .devsite-landing-row-item-buttons,.dac-landing-row-item-buttons-bottom .devsite-landing-row-item-buttons{padding-top:32px}.dac-button,.dac-flat-button-muted{-webkit-box-shadow:none;box-shadow:none;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:48px;letter-spacing:.5px;padding:0 24px;-webkit-transition:background-color .2s,color .2s,-webkit-box-shadow .2s;transition:background-color .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background-color .2s,box-shadow .2s,color .2s;transition:background-color .2s,box-shadow .2s,color .2s;transition:background-color .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}.dac-button{background:#4b7d2f;color:#fff}.dac-button:focus,.dac-button:hover{background:#2f4d1f}.dac-flat-button-muted{background:#f7f9fa;color:#000}.dac-flat-button-muted:focus,.dac-flat-button-muted:hover{background:#d8e1e6}.dac-button+.dac-button{margin-left:32px}.dac-outline-button{background:0;border:2px solid #4b7d2f;-webkit-border-radius:4px;border-radius:4px;color:#4b7d2f}.dac-outline-button:focus,.dac-outline-button:hover{background:#4b7d2f;color:#fff}.dac-outline-button:disabled{background:0;border-color:#d7d7d7;color:#d7d7d7}.dac-alt-flat-button,.dac-flat-button{background:0;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0}.dac-alt-flat-button:focus,.dac-alt-flat-button:hover,.dac-flat-button:focus,.dac-flat-button:hover{background:0;opacity:.4}@media screen and (max-width:720px){.dac-alt-flat-button,.dac-flat-button{font:500 14px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-flat-button{color:#414141}.devsite-product-button-row .dac-header-button{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);background:#4b7d2f}.dac-button.dac-flat-button{height:auto;line-height:inherit;white-space:inherit}.dac-alt-flat-button{color:#4b7d2f}.dac-button-group .dac-button{margin-bottom:16px}.dac-button-group .dac-button:not(:last-child){margin-right:16px}.dac-full-width-content .devsite-landing-row-item-buttons{display:block}.dac-full-width-media .devsite-landing-row-item-buttons{display:none}.dac-resource-widget-more{padding:32px 0 0;text-align:center}@media screen and (max-width:600px){.dac-full-width-content .devsite-landing-row-item-buttons{display:none}.dac-full-width-media .devsite-landing-row-item-buttons{display:block}}.dac-jetpack:before,.dac-rss:before{content:"";height:40px;width:40px}.dac-jetpack:before{background:url(../images/custom/jetpack-icon.svg) 50%/40px no-repeat}.dac-rss:before{background:url(../images/custom/rss-icon.png) 50%/24px no-repeat}.dac-jetpack a,.dac-rss a{text-decoration:underline}.dac-jetpack a:focus,.dac-jetpack a:hover,.dac-rss a:focus,.dac-rss a:hover{text-decoration:none}.dac-jetpack a:focus,.dac-rss a:focus{background:rgba(26,115,232,.1);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px}div.dac-jetpack,div.dac-rss,span.dac-jetpack{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#414141;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;font-size:14px;font-weight:700;margin:0;vertical-align:text-bottom}[dir=ltr] div.dac-jetpack:before,[dir=ltr] div.dac-rss:before,[dir=ltr] span.dac-jetpack:before{margin-right:4px}[dir=rtl] div.dac-jetpack:before,[dir=rtl] div.dac-rss:before,[dir=rtl] span.dac-jetpack:before{margin-left:4px}[dir=ltr] div.dac-jetpack,[dir=ltr] div.dac-rss{padding-left:16px}[dir=rtl] div.dac-jetpack,[dir=rtl] div.dac-rss{padding-right:16px}span.dac-jetpack{position:relative}[dir=ltr] span.dac-jetpack{padding-left:12px}[dir=rtl] span.dac-jetpack{padding-right:12px}span.dac-jetpack:after{background:#414141;content:"";display:block;height:70%;margin:0;position:absolute;top:15%;width:2px}[dir=ltr] span.dac-jetpack:after{left:0}[dir=rtl] span.dac-jetpack:after{right:0}h2 span.dac-jetpack{margin-bottom:-6px}h3 span.dac-jetpack,h4 span.dac-jetpack{margin-bottom:-8px}h4 span.dac-jetpack>span{position:relative;top:3px}aside.dac-jetpack,aside.dac-jetpack :link,aside.dac-jetpack :visited,aside.dac-rss,aside.dac-rss :link,aside.dac-rss :visited{background:#e0f2f1;color:#00796b}aside.dac-jetpack :focus,aside.dac-jetpack :hover,aside.dac-rss :focus,aside.dac-rss :hover{background:hsla(0,0%,100%,.7)}aside.dac-jetpack code,aside.dac-rss code{background:0;color:#00796b}aside.dac-jetpack:before,aside.dac-rss:before{margin:-8px 0 0}[dir=ltr] aside.dac-jetpack:before,[dir=ltr] aside.dac-rss:before{margin-left:-44px}[dir=rtl] aside.dac-jetpack:before,[dir=rtl] aside.dac-rss:before{margin-right:-44px}.dac-card-footer{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;margin-top:85px;padding:52px 32px}@media screen and (max-width:1000px){.dac-card-footer{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding:32px}}.dac-card-footer img{margin:0 10px;max-width:50%}@media screen and (max-width:1000px){.dac-card-footer img{margin-bottom:32px;max-width:240px}}.devsite-landing-row-item-description-content,.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{color:#414141;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:720px){.devsite-landing-row-item-description-content,.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{font:400 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-dynamic-content-no-img .devsite-card-image-bg{display:none}.dac-dynamic-content-stack devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 100%;flex:0 1 100%}.dac-dynamic-content-section .devsite-landing-row-item,.dac-resource-widget-section .devsite-landing-row-item{display:block}devsite-dynamic-content .resource-widget[class*=col-]{float:none!important;padding:0!important}devsite-dynamic-content .devsite-card-group{margin:-32px 0 0 -32px}devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(33.33% - 32px);flex:0 0 calc(33.33% - 32px);margin:32px 0 0 32px;padding:0}.dac-full-width-page devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px)}devsite-dynamic-content .devsite-card-content{padding:32px}devsite-dynamic-content .devsite-card{background:#f7f9fa;-webkit-box-shadow:none;box-shadow:none}.devsite-card-list-item,devsite-dynamic-content .devsite-card,devsite-dynamic-content .devsite-card-list-item{-webkit-border-radius:0;border-radius:0}devsite-dynamic-content .devsite-card h3{color:#202124}.devsite-card-list-link,devsite-dynamic-content .devsite-card-list-link,devsite-dynamic-content .devsite-card h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-card-list-link,devsite-dynamic-content .devsite-card-list-link{background:#f7f9fa;color:#80868b}.devsite-card-list-link:focus,.devsite-card-list-link:hover,devsite-dynamic-content .devsite-card-list-link:focus,devsite-dynamic-content .devsite-card-list-link:hover{background:#455a64;color:#fff}devsite-dynamic-content .devsite-card-category{font:500 14px/22px Roboto Mono,monospace;letter-spacing:1.5px}devsite-dynamic-content .devsite-pagination-less-button,devsite-dynamic-content .devsite-pagination-more-button{background:#4b7d2f;-webkit-box-shadow:none;box-shadow:none;color:#fff;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:46px;padding:0 24px}devsite-dynamic-content .devsite-pagination-less-button:focus,devsite-dynamic-content .devsite-pagination-less-button:hover,devsite-dynamic-content .devsite-pagination-more-button:focus,devsite-dynamic-content .devsite-pagination-more-button:hover{background:#2f4d1f}devsite-dynamic-content [dynamic-card-style=small] .devsite-card-content{padding:20px}devsite-dynamic-content .devsite-card[dynamic-card-type=medium]{background:rgba(0,0,0,.94)}devsite-dynamic-content .devsite-card[dynamic-card-type=medium],devsite-dynamic-content .devsite-card[dynamic-card-type=medium] h3{color:#fff}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author{border:0;padding:12px 32px 32px 80px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{background-repeat:no-repeat;content:"";height:40px;left:32px;position:absolute;width:40px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before{background-image:url(../images/custom/android-developers-logo.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before{background-image:url(../images/custom/android-blog-round-icon.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before{background-image:url(../images/custom/medium-round-icon.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{background-image:url(../images/custom/youtube-round-icon.svg)}@media screen and (max-width:1000px){.dac-full-width-page devsite-dynamic-content .devsite-card-wrapper,body devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}[has-toc] devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}}@media screen and (min-width:720px) and (max-width:1000px){[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card>*{width:50%}[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card-image-bg{height:170px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding-bottom:28.125%}}@media screen and (max-width:720px){devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author{padding-left:64px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{left:24px}[has-toc] devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}}.dac-label .devsite-landing-row-item-labels a,.dac-label .devsite-landing-row-item-labels span{margin-bottom:0;font:500 14px/22px Roboto Mono,monospace;letter-spacing:1.5px}@media screen and (max-width:720px){.dac-label .devsite-landing-row-item-labels a,.dac-label .devsite-landing-row-item-labels span{font:500 12px/22px Roboto Mono,monospace}}.dac-label.dac-landing-row-bg-blue .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-blue .devsite-landing-row-item-labels span,.dac-label.dac-landing-row-bg-green .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-green .devsite-landing-row-item-labels span,.dac-label.dac-landing-row-bg-slate .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-slate .devsite-landing-row-item-labels span{color:#fff}.dac-label.dac-success-story .devsite-landing-row-item-labels a,.dac-label.dac-success-story .devsite-landing-row-item-labels span{margin-bottom:16px}.dac-label.devsite-landing-row-item h3:first-child,.dac-label.devsite-landing-row h3:first-child,.dac-label h3{margin-top:16px}.dac-featured-cards{margin-bottom:32px}.dac-featured-cards .devsite-landing-row-column{background:#f7f9fa;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-width:0}[layout=docs] .dac-featured-cards .devsite-landing-row-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-featured-cards .devsite-landing-row-item:last-child .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-featured-cards a{color:inherit}.dac-featured-cards .devsite-landing-row-column h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-featured-cards .devsite-landing-row-column img{display:block;margin:0 auto 16px;max-width:150px}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{margin-top:0;width:60%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons,[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type .devsite-landing-row-item-buttons{display:none}[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons{display:block}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2).devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-top:0}[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{-webkit-box-flex:1;-webkit-flex:1;flex:1;width:100%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item-description{padding:24px}}[has-book-nav] .dac-featured-cards .devsite-landing-row-item-buttons .dac-button{white-space:inherit}@media screen and (max-width:1400px){.dac-featured-cards .devsite-landing-row-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{width:100%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type .devsite-landing-row-item-buttons{display:none}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons{display:block}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2).devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}.dac-featured-cards .devsite-landing-row-item:last-child .devsite-landing-row-item-buttons{padding-top:0}}.dac-get-involved.dac-center{width:auto}.dac-footer-promos{font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}.dac-footer-promos .devsite-footer-promos-list{display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:20px 0}.dac-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 1 192px;flex:0 1 192px;margin:20px 0;text-align:center}.dac-footer-promos .devsite-footer-promo:not(:first-child){margin-left:24px}.dac-footer-promos .devsite-footer-promo-icon{display:block;height:48px;margin:0 auto 8px;width:48px}.dac-footer-promos .devsite-footer-promo-title{color:rgba(0,0,0,.87);display:block;font-weight:500}.dac-footer-promos .devsite-footer-promo-title:focus,.dac-footer-promos .devsite-footer-promo-title:hover{color:#1a73e8}@media screen and (max-width:840px){.dac-footer-promos{padding:0}.dac-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;padding:12px 0}.dac-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0;padding:8px 8px 8px 0;text-align:left}.dac-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}.dac-footer-promos .devsite-footer-promo-icon{height:32px;margin:0 8px 0 0;width:32px}.dac-footer-promos .devsite-footer-promo-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font-weight:400}.dac-footer-promos .devsite-footer-promo-description{display:none}}@media screen and (max-width:600px){.dac-footer-promos .devsite-footer-promos-list{display:block}}.dac-landing-row-hero h2,.devsite-landing-row-item h2,.devsite-landing-row-item h3,.devsite-landing-row h1,.devsite-landing-row h2{color:#414141}.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-landing-row-hero h2,.devsite-landing-row-item h2,.devsite-landing-row-item h3{letter-spacing:-.5px}.dac-heading-medium.devsite-landing-row-item h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:16px;margin-top:16px}.dac-subtitle :first-child{margin-top:0}.dac-subtitle :last-child{margin-bottom:0}@media screen and (max-width:720px){.dac-subtitle{margin-bottom:32px;margin-top:32px}}@media screen and (max-width:1200px){.dac-heading-small.devsite-landing-row-item h3,.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:48px;margin-top:88px}}@media screen and (max-width:1000px){.devsite-landing-row-item .dac-subtitle h3,.devsite-landing-row-item h3{margin-bottom:26px}.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-medium.devsite-landing-row-item h3{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:32px;margin-top:48px}}.dac-heading-logo{padding:24px 0 8px}.dac-security-page [background]:not([background=grey]) :link :not(.material-icons):hover{text-decoration:none}.dac-heading-icon,.dac-heading-icon-small,.dac-heading-icon-tiny{display:block;margin-bottom:24px}.dac-heading-icon{height:80px}.dac-heading-icon.material-icons{font-size:80px}.dac-heading-icon-small{height:62px}.dac-heading-icon-small.material-icons{font-size:62px}.dac-heading-icon-tiny{height:40px}.dac-heading-icon-tiny.material-icons{font-size:40px}.dac-landing-row-hero{color:#414141;margin:84px 0;text-align:center}@media screen and (max-width:1200px){.dac-landing-row-hero{margin:60px 0}}@media screen and (max-width:1000px){.dac-landing-row-hero{margin:48px 0}}@media screen and (max-width:720px){.dac-landing-row-hero{margin:36px 0}}[layout=docs] .dac-landing-row-hero{margin:44px 0}@media screen and (max-width:1000px){[layout=docs] .dac-landing-row-hero{margin:36px 0}}.dac-landing-row-hero-screenshot{margin-top:72px}@media screen and (max-width:720px){.dac-landing-row-hero-screenshot{margin-top:48px}}.dac-landing-row-hero-screenshot img{-webkit-box-shadow:0 2px 3px 0 rgba(60,64,67,.3),0 6px 10px 4px rgba(60,64,67,.15);box-shadow:0 2px 3px 0 rgba(60,64,67,.3),0 6px 10px 4px rgba(60,64,67,.15)}.dac-landing-row-hero-description{color:#263238;font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:24px auto;max-width:800px}.dac-landing-hero{text-align:center}.dac-landing-hero .devsite-landing-row-header{display:block}.dac-landing-hero .devsite-landing-row-header-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:52px}.dac-landing-hero h1{color:inherit}.dac-sub-hero{display:-webkit-box;display:-webkit-flex;display:flex}@media screen and (max-width:1000px){.dac-sub-hero{display:block;margin-top:24px}}@media screen and (max-width:720px){.dac-sub-hero{margin-top:16px}}.dac-sub-hero h1{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:20px}@media screen and (max-width:1000px){.dac-sub-hero h1{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-sub-hero-image{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0 0 16px 16px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;text-align:center}.dac-sub-hero-image img{max-height:310px}@media screen and (max-width:1000px){.dac-sub-hero-image img{max-height:150px}}.dac-sub-hero-copy{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}@media screen and (max-width:1000px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description[icon-position=left]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-body{padding:32px}@media screen and (max-width:720px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-body{padding:24px}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child{-webkit-box-flex:1;-webkit-flex:1 0 200px;flex:1 0 200px}@media screen and (max-width:1000px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child{height:200px}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container:focus,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container:hover,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child:focus,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child:hover{opacity:.8}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-icon{height:auto;margin:0;width:95px}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-icon-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:100%;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:100%}.dac-landing-row-item-icon-container-bg-yellow .devsite-landing-row-item-icon-container{background-color:#ffd600}.dac-landing-row-item-icon-container-bg-green .devsite-landing-row-item-icon-container{background-color:#00e676}.dac-landing-row-item-icon-container-bg-red .devsite-landing-row-item-icon-container{background-color:#ff5252}.dac-landing-row-item-icon-container-bg-blue .devsite-landing-row-item-icon-container{background-color:#2196f3}.dac-chevron-right:before{content:"arrow_forward";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;vertical-align:middle}.dac-chevron-right:focus{text-decoration:none}.dac-landing-row-item-buttons-right .devsite-landing-row-item-buttons{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.dac-landing-row-item-description-bg-white .devsite-landing-row-item-description{background-color:#fff}.dac-landing-row-item-description-bg-transparent .devsite-landing-row-item-description{background-color:none}.dac-home-page .dac-gservices .dac-banner-card-bg-img-item{background:url(../images/custom/home-google-services-for-android.svg) 50%/calc(100% - 32px) no-repeat #f7f9fa}.dac-home-design-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/home-design-illustration.svg)}.dac-about-hero-illustration{background:url(../images/custom/camerax.png) 0/auto 125% no-repeat #f7f9fa}.dac-things-page .dac-banner-card.devsite-landing-row .dac-banner-card-bg-img-item.dac-banner-card-bg-img-contain{background:url(../images/custom/prototype-to-production.svg) no-repeat 0/900px #455a64}.dac-play-page .dac-play-hero-banner:after{background:url(../images/custom/google-play-policies.png) 85%/75% no-repeat}.dac-landing-row-bg-illustration-2:after{background-image:url(../images/custom/keyline-illustration-2.svg)}.dac-landing-row-bg-blob-5{background:url(../images/custom/blob-illustration-5.svg) bottom/cover no-repeat #455a64}.dac-landing-row-bg-blob-7{background:url(../images/custom/blob-illustration-7.svg) -webkit-calc(100% + 280px) -webkit-calc(100% + 150px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-7.svg) calc(100% + 280px) calc(100% + 150px)/contain no-repeat content-box content-box #f7f9fa}.dac-landing-row-bg-blob-8{background:url(../images/custom/blob-illustration-8.svg) -webkit-calc(100% + 80px) -webkit-calc(100% + 100px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-8.svg) calc(100% + 80px) calc(100% + 100px)/contain no-repeat content-box content-box #f7f9fa}.dac-illustration-block{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:16px 0;text-align:center}.dac-illustration-block img{display:inline-block;max-width:100%}.dac-landing-row-illustration-kotlin .dac-illustration-block img{margin-top:40px}.dac-illustration-block-edge{margin:0 -32px;width:-webkit-calc(100% + 64px);width:calc(100% + 64px)}.dac-illustration-block-edge-left .dac-illustration-block-edge-right{width:-webkit-calc(100% + 32px);width:calc(100% + 32px)}.dac-illustration-block-edge-left{margin-left:-32px}.dac-illustration-block-edge-right{margin-right:-32px}.dac-illustration-block-edge-left img,.dac-illustration-block-edge-right img,.dac-illustration-block-edge img{width:100%}.dac-illustration{position:relative}.dac-illustration:after{background-position:top;-o-background-size:600px auto;background-size:600px auto;content:"";display:block;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;min-height:250px;-webkit-box-ordinal-group:6;-webkit-order:5;order:5;width:100%}.devsite-landing-page-with-side-navs .dac-illustration{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-illustration .devsite-landing-row-item-description{-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0}.dac-illustration-bleed-lb:after{margin-bottom:-40px;margin-left:-40px;width:-webkit-calc(100% + 40px);width:calc(100% + 40px)}@media screen and (max-width:1000px){.dac-illustration-bleed-lb:after{margin-bottom:-40px;margin-left:-16px;width:-webkit-calc(100% + 32px);width:calc(100% + 32px)}}.dac-illustration-keyline-1:after{background-image:url(../images/custom/keyline-illustration-1.svg)}.dac-illustration-keyline-3:after{background-image:url(../images/custom/keyline-illustration-3.svg)}.dac-illustration-keyline-4:after{background-image:url(../images/custom/keyline-illustration-4.svg)}.dac-illustration-keyline-5:after{background-image:url(../images/custom/keyline-illustration-5.svg)}.dac-illustration-keyline-3:after,.dac-illustration-keyline-4:after,.dac-illustration-keyline-5:after{-o-background-size:500px auto;background-size:500px auto}.dac-illustration-play-book:after{background:url(../images/custom/playbook-illustration-2.svg) 100% 0 no-repeat content-box}.dac-illustration-auto:after{background:url(../images/custom/auto-illustration.svg) 100% 100%/auto 100% no-repeat}.dac-landing-row-illustration-kotlin{background:url(../images/custom/kotlin-blob.svg) 160px 130%/auto no-repeat content-box #455a64}.dac-illustration-kotlin-bootcamp:after{background:url(../images/custom/kotlin-bootcamp.png) 50%/cover no-repeat}.dac-illustration-guides-policies-families:after{background:url(../images/custom/google-play-guides-families-blob-2.svg) 0 30px/auto 135% no-repeat;width:35%}.dac-illustration-guides-go:after{background:url(../images/custom/google-play-guides-android-go-edition.svg) right 35px/auto 100% no-repeat}.dac-illustration-guides-games:after{background:url(../images/custom/google-play-guides-games.png) center right 15px/auto 65% no-repeat}.dac-illustration-guides-families:after{background:url(../images/custom/google-play-guides-families-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-indie:after{background:url(../images/custom/google-play-guides-indie-games-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-start:after{background:url(../images/custom/google-play-guides-start-on-android-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-subscriptions:after{background:url(../images/custom/google-play-guides-subscriptions-simplified.svg) right 35px/auto 95% no-repeat}.dac-illustration-guides:after{background:url(../images/custom/google-play-guides-android-go-edition.svg) 150% 0/auto 110% no-repeat}.dac-illustration-play-console:after{background:url(../images/custom/floating-playtime-elements-mobile.svg) left 0/cover no-repeat,url(../images/custom/playtime-hero-bg-fluid-mobile.svg) 0 50px/180% no-repeat;min-height:360px}.dac-illustration-newsletter-archive{background:url(../images/custom/newsletter-archive-banner.svg) 75%/30% no-repeat,#f7f9fa}@media screen and (max-width:1200px){.dac-illustration-newsletter-archive{background:url(../images/custom/newsletter-archive-banner.svg) 80%/40% no-repeat,#f7f9fa}}@media screen and (max-width:720px){.dac-illustration-newsletter-archive{background:#f7f9fa}}.dac-landing-row-bg-illustration-3{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-landing-row-bg-illustration-3 .devsite-landing-row-item-description-content p{max-width:100%}.dac-landing-row-bg-illustration-3:after{background:url(../images/custom/success-guide-illustration-1.svg) 0 no-repeat content-box;height:100%;margin-left:-1em;width:100%}.dac-docs-overview-page .dac-landing-row-bg-illustration-2 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after,.dac-illustration-column .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console:after,.dac-play-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-page .dac-illustration-play-console:after{width:50%}.dac-landing-row-bg-blob-3:after{background:url(../images/custom/blob-illustration-3-small.svg) bottom/cover content-box content-box no-repeat #fff;min-height:425px;padding-left:0;width:100%}.dac-illustration-column:after{height:100%;width:50%}.dac-illustration-column{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after{min-height:500px}.dac-about-hero-illustration .dac-media img{display:none}.dac-play-policies-page .dac-illustration-android-q-ready:after{background:url(../images/custom/play-policies-q-logo.png) 50%/contain no-repeat}.dac-play-policies-page .dac-illustration-play-policies-rating:after{background:url(../images/custom/play-policies-rating.svg) 50%/70% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-icon:after{background:url(../images/custom/play-policies-icon.png) 50%/70% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-target-audience:after{background:url(../images/custom/play-policies-target-audience.png) 50%/45% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-64-bit:after{background:url(../images/custom/play-policies-64-bit.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-target-api:after{background:url(../images/custom/play-policies-target-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-developer-api:after{background:url(../images/custom/play-policies-dev-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-billing-library:after{background:url(../images/custom/play-policies-billing-library.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-comply:after{background:url(../images/custom/play-policies-comply.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-monetization:after{background:url(../images/custom/play-policies-monetization.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-referrer-api:after{background:url(../images/custom/play-policies-referrer-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-workmanager:after{background:url(../images/custom/play-policies-workmanager.png) 50%/50% no-repeat}.devsite-404-header.devsite-header-billboard:before{background:url(../images/custom/404-illustration-question-mark.png) 50%/contain no-repeat}.devsite-404-header.devsite-header-billboard:after{background:url(../images/custom/404-illustration-telescope.png) 50%/contain no-repeat}@media screen and (max-width:1200px){.dac-play-page .dac-play-hero-banner:after{background-position:95%;-o-background-size:90%;background-size:90%}}@media screen and (max-width:1000px){.dac-about-hero-illustration{-o-background-size:auto 94%;background-size:auto 94%}.dac-illustration-column .devsite-landing-row-item-description,.dac-illustration-column:after{width:100%}.dac-landing-row-bg-blob-7{background:url(../images/custom/blob-illustration-7.svg) -webkit-calc(100% + 250px) -webkit-calc(100% + 119px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-7.svg) calc(100% + 250px) calc(100% + 119px)/contain no-repeat content-box content-box #f7f9fa}.dac-landing-row-bg-blob-3:after{background-image:url(../images/custom/blob-illustration-3-large.svg);background-position:0 0;padding-left:30%}}@media screen and (max-width:720px){.dac-docs-overview-page .dac-landing-row-bg-illustration-2 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after,.dac-play-console-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console:after,.dac-play-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-page .dac-illustration-play-console:after{width:auto}.dac-landing-row-illustration-kotlin{background:url(../images/custom/kotlin-blob-mobile.svg) bottom/100% no-repeat content-box #455a64}.dac-landing-row-bg-blob-3:after{background:url(../images/custom/blob-illustration-3-small.svg) bottom/cover content-box content-box no-repeat #fff;min-height:425px;padding-left:0;width:100%}.dac-landing-row-bg-blob-1{background-position:85% 100%}.dac-landing-row-bg-blob-7{background-position:-webkit-calc(100% + 417px) -webkit-calc(100% + 210px);background-position:calc(100% + 417px) calc(100% + 210px);-o-background-size:750px;background-size:750px}.dac-illustration-block img{max-width:60%}.devsite-landing-page-with-side-navs .dac-illustration{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-page-with-side-navs .dac-illustration:after{min-height:300px}.dac-illustration-auto:after{-o-background-size:cover;background-size:cover}.dac-illustration-play-console{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-illustration:after{min-height:250px}.dac-illustration-play-console:after{background-clip:content-box;background-image:url(../images/custom/floating-playtime-elements.svg),url(../images/custom/playtime-hero-bg-fluid.svg)}.dac-illustration-block-edge{margin:0;width:auto}}@media screen and (max-width:600px){.dac-play-page .dac-play-hero-banner:after{background-position:63%;-o-background-size:80%;background-size:80%}.dac-about-hero-illustration .dac-media img{display:block}.dac-about-hero-illustration{background-image:none}.dac-about-hero-illustration .dac-full-width-media{margin-top:0!important}.dac-illustration-guides-families:after,.dac-illustration-guides-go:after,.dac-illustration-guides-indie:after,.dac-illustration-guides-start:after,.dac-illustration-guides-subscriptions:after,.dac-illustration-guides:after{background-position:50%}.dac-illustration-guides-policies-families:after{background:url(../images/custom/blob-illustration-3-small.svg) -webkit-calc(100% + 150px) -webkit-calc(100% + 165px)/auto 170% no-repeat;background:url(../images/custom/blob-illustration-3-small.svg) calc(100% + 150px) calc(100% + 165px)/auto 170% no-repeat;width:100%}}ol.callouts{counter-reset:item;list-style-type:none;margin-left:30px;padding-left:0}ol.callouts>li:before{background:url(../images/custom/callout-bg_2x.png) 1px 2px/20px no-repeat;-webkit-box-sizing:content-box;box-sizing:content-box;content:counter(item);counter-increment:item;margin-left:-30px;padding-left:8px;position:absolute;width:16px}.callout{background:url(../images/custom/callout-bg_2x.png) 0 2px/20px no-repeat;display:inline-block;height:22px;text-align:center;width:20px}.callout,ol.callouts>li:before{color:#fff;font-size:12px;font-weight:500}.img-caption,figcaption{font-size:14px;margin-top:-4px}p+.img-caption{margin-top:-20px}li p+.img-caption{margin-top:-12px}.table-caption{clear:both;font-size:14px;margin:0 0 4px}.table-caption+.devsite-table-wrapper,.table-caption+table{margin-top:0}.code-caption{font:14px/1.5 monospace;margin-bottom:4px}a.external-link:after{content:"open_in_new";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;word-wrap:normal;font-size:18px;margin-left:4px;vertical-align:text-bottom}.sidebox{background:#f1f3f4;padding:12px 24px}.sidebox a.bug{padding-right:25px;background:transparent url(../images/custom/bug.png) no-repeat 100%}.sidebox a.g-plus{padding-right:25px;background:transparent url(../images/custom/g+.ico) no-repeat 100%}.sidebox a.codelab{padding-right:40px;background:transparent url(../images/custom/favicon.png) no-repeat 100%}.sidebox p{margin-top:0}.wrap{margin:0 auto;clear:both}.cols{margin-left:-10px;margin-right:-10px}.cols:after,.cols:before{content:" ";display:table}.cols:after{clear:both}[class*=col-]{-webkit-box-sizing:border-box;box-sizing:border-box;float:left;min-height:1px;padding-left:10px;padding-right:10px;position:relative}.col-1{width:6.25%}.col-2{width:12.5%}.col-3{width:18.75%}.col-4{width:25%}.col-5{width:31.25%}.col-6{width:37.5%}.col-7{width:43.75%}.col-8{width:50%}.col-9{width:56.25%}.col-10{width:62.5%}.col-11{width:68.75%}.col-12{width:75%}.col-13{width:81.25%}.col-14{width:87.5%}.col-15{width:93.75%}.col-16{width:100%}.col-13 .col-1{width:7.69230769%}.col-13 .col-2{width:15.38461538%}.col-13 .col-3{width:23.07692308%}.col-13 .col-4{width:30.76923077%}.col-13 .col-5{width:38.46153846%}.col-13 .col-6{width:46.15384615%}.col-13 .col-7{width:53.84615385%}.col-13 .col-8{width:61.53846154%}.col-13 .col-9{width:69.23076923%}.col-13 .col-10{width:76.92307692%}.col-13 .col-11{width:84.61538462%}.col-13 .col-12{width:92.30769231%}.col-13 .col-13{width:100%}.col-12 .col-1{width:8.33333333%}.col-12 .col-2{width:16.66666667%}.col-12 .col-3{width:25%}.col-12 .col-4{width:33.33333333%}.col-12 .col-5{width:41.66666667%}.col-12 .col-6{width:50%}.col-12 .col-7{width:58.33333333%}.col-12 .col-8{width:66.66666667%}.col-12 .col-9{width:75%}.col-12 .col-10{width:83.33333333%}.col-12 .col-11{width:91.66666667%}.col-1of1,.col-2of2,.col-3of3,.col-4of4,.col-5of5,.col-6of6,.col-8of8,.col-10of10,.col-12 .col-12,.col-12of12,.col-16of16{width:100%}.col-1of2,.col-2of4,.col-3of6,.col-4of8,.col-5of10,.col-6of12,.col-8of16{width:50%}.col-1of3,.col-2of6,.col-4of12{width:33.33333333%}.col-2of3,.col-4of6,.col-8of12{width:66.66666667%}.col-1of4,.col-2of8,.col-3of12,.col-4of16{width:25%}.col-3of4,.col-6of8,.col-9of12,.col-12of16{width:75%}.col-1of5,.col-2of10{width:20%}.col-2of5,.col-4of10{width:40%}.col-3of5,.col-6of10{width:60%}.col-4of5,.col-8of10{width:80%}.col-1of6,.col-2of12{width:16.66666667%}.col-5of6,.col-10of12{width:83.33333333%}.col-1of8,.col-2of16{width:12.5%}.col-3of8,.col-6of16{width:37.5%}.col-5of8,.col-10of16{width:62.5%}.col-7of8,.col-14of16{width:87.5%}.col-1of10{width:10%}.col-3of10{width:30%}.col-7of10{width:70%}.col-9of10{width:90%}.col-1of12{width:8.33333333%}.col-5of12{width:41.66666667%}.col-7of12{width:58.33333333%}.col-11of12{width:91.66666667%}.col-1of16{width:6.25%}.col-3of16{width:18.75%}.col-5of16{width:31.25%}.col-7of16{width:43.75%}.col-9of16{width:56.25%}.col-11of16{width:68.75%}.col-13of16{width:81.25%}.col-15of16{width:93.75%}.col-pull-1of1,.col-pull-2of2,.col-pull-3of3,.col-pull-4of4,.col-pull-5of5,.col-pull-6of6,.col-pull-8of8,.col-pull-10of10,.col-pull-12of12,.col-pull-16of16{left:-100%}.col-pull-1of2,.col-pull-2of4,.col-pull-3of6,.col-pull-4of8,.col-pull-5of10,.col-pull-6of12,.col-pull-8of16{left:-50%}.col-pull-1of3,.col-pull-2of6,.col-pull-4of12{left:-33.33333333%}.col-pull-2of3,.col-pull-4of6,.col-pull-8of12{left:-66.66666667%}.col-pull-1of4,.col-pull-2of8,.col-pull-3of12,.col-pull-4of16{left:-25%}.col-pull-3of4,.col-pull-6of8,.col-pull-9of12,.col-pull-12of16{left:-75%}.col-pull-1of5,.col-pull-2of10{left:-20%}.col-pull-2of5,.col-pull-4of10{left:-40%}.col-pull-3of5,.col-pull-6of10{left:-60%}.col-pull-4of5,.col-pull-8of10{left:-80%}.col-pull-1of6,.col-pull-2of12{left:-16.66666667%}.col-pull-5of6,.col-pull-10of12{left:-83.33333333%}.col-pull-1of8,.col-pull-2of16{left:-12.5%}.col-pull-3of8,.col-pull-6of16{left:-37.5%}.col-pull-5of8,.col-pull-10of16{left:-62.5%}.col-pull-7of8,.col-pull-14of16{left:-87.5%}.col-pull-1of10{left:-10%}.col-pull-3of10{left:-30%}.col-pull-7of10{left:-70%}.col-pull-9of10{left:-90%}.col-pull-1of12{left:-8.33333333%}.col-pull-5of12{left:-41.66666667%}.col-pull-7of12{left:-58.33333333%}.col-pull-11of12{left:-91.66666667%}.col-pull-1of16{left:-6.25%}.col-pull-3of16{left:-18.75%}.col-pull-5of16{left:-31.25%}.col-pull-7of16{left:-43.75%}.col-pull-9of16{left:-56.25%}.col-pull-11of16{left:-68.75%}.col-pull-13of16{left:-81.25%}.col-pull-15of16{left:-93.75%}.col-push-1of1,.col-push-2of2,.col-push-3of3,.col-push-4of4,.col-push-5of5,.col-push-6of6,.col-push-8of8,.col-push-10of10,.col-push-12of12,.col-push-16of16{left:100%}.col-push-1of2,.col-push-2of4,.col-push-3of6,.col-push-4of8,.col-push-5of10,.col-push-6of12,.col-push-8of16{left:50%}.col-push-1of3,.col-push-2of6,.col-push-4of12{left:33.33333333%}.col-push-2of3,.col-push-4of6,.col-push-8of12{left:66.66666667%}.col-push-1of4,.col-push-2of8,.col-push-3of12,.col-push-4of16{left:25%}.col-push-3of4,.col-push-6of8,.col-push-9of12,.col-push-12of16{left:75%}.col-push-1of5,.col-push-2of10{left:20%}.col-push-2of5,.col-push-4of10{left:40%}.col-push-3of5,.col-push-6of10{left:60%}.col-push-4of5,.col-push-8of10{left:80%}.col-push-1of6,.col-push-2of12{left:16.66666667%}.col-push-5of6,.col-push-10of12{left:83.33333333%}.col-push-1of8,.col-push-2of16{left:12.5%}.col-push-3of8,.col-push-6of16{left:37.5%}.col-push-5of8,.col-push-10of16{left:62.5%}.col-push-7of8,.col-push-14of16{left:87.5%}.col-push-1of10{left:10%}.col-push-3of10{left:30%}.col-push-7of10{left:70%}.col-push-9of10{left:90%}.col-push-1of12{left:8.33333333%}.col-push-5of12{left:41.66666667%}.col-push-7of12{left:58.33333333%}.col-push-11of12{left:91.66666667%}.col-push-1of16{left:6.25%}.col-push-3of16{left:18.75%}.col-push-5of16{left:31.25%}.col-push-7of16{left:43.75%}.col-push-9of16{left:56.25%}.col-push-11of16{left:68.75%}.col-push-13of16{left:81.25%}.col-push-15of16{left:93.75%}.dac-row-logos{padding:0}.dac-row-logos .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-row-logos .devsite-landing-row-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:40px 5px}.dac-row-logos .devsite-landing-row-item-description{display:none}.dac-row-logos img{width:-webkit-max-content;width:-moz-max-content;width:max-content}.dac-landing-row-media .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-full-width-media .devsite-landing-row-item-body,.dac-media{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-media{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-self:flex-end;align-self:flex-end;-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;width:100%}.dac-media,.dac-media-video{display:-webkit-box;display:-webkit-flex;display:flex}.dac-media-video{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start}.dac-media-video-toolbar{height:auto;margin-left:3%;width:15%}.dac-full-width-media[background],.dac-full-width-media[background].dac-grow-7.devsite-landing-row-item-no-media:not(:first-child){margin-left:0}@media screen and (max-width:1000px){.dac-media{margin-top:8px}.devsite-landing-row:not(.devsite-landing-row-logos) .dac-full-width-media .devsite-landing-row-item-description{padding-top:0}}[class*=dac-landing-row-bg] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row-item[background] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row[background] .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){[class*=dac-landing-row-bg] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row-item[background] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row[background] .devsite-landing-row-item-description{padding:24px}}.dac-landing-row-item-icon-container-left.devsite-landing-row-item .devsite-landing-row-item-description{padding:0}.dac-top-spacing{padding-top:120px}.dac-bottom-spacing{padding-bottom:88px}@media screen and (max-width:720px){.dac-top-spacing{padding-top:64px}.dac-bottom-spacing{padding-bottom:40px}}.dac-success-story{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content{-webkit-box-pack:end;-webkit-justify-content:end;justify-content:end}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content p{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content p strong{padding-left:4px}.dac-success-story h4{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 16px}.dac-success-story .devsite-landing-row-item-description-content{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.dac-success-story .devsite-landing-row-item-media{-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding:24px}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0}.dac-success-story img{width:128px}.dac-success-story .devsite-landing-row-item-image{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:transparent;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.dac-success-story .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0;margin:0}@media screen and (max-width:720px){.dac-success-story{display:block}.dac-success-story .devsite-landing-row-item-media{padding:24px 24px 0}.dac-success-story .devsite-landing-row-item-image{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-success-story.devsite-landing-row-item .devsite-landing-row-item-media{margin:0}}.devsite-404-header.devsite-header-billboard:after,.devsite-404-header.devsite-header-billboard:before{content:"";display:block;position:absolute}.devsite-404-header.devsite-header-billboard:before{bottom:-210px;height:428px;left:-35%;width:321px;z-index:1}.devsite-404-header.devsite-header-billboard:after{bottom:-297px;height:508px;left:86%;width:391px;z-index:3}.devsite-404-search{z-index:2}.devsite-404-search devsite-search .devsite-searchbox:before{background:0}.devsite-404-header,.devsite-offline-header{z-index:auto}@media screen and (max-width:1200px){.devsite-404-header.devsite-header-billboard:before{bottom:-180px;left:-21%;width:271px}.devsite-404-header.devsite-header-billboard:after{bottom:-350px;left:74%;width:321px;z-index:2}}@media screen and (max-width:1000px){.devsite-404-header.devsite-header-billboard:after,.devsite-404-header.devsite-header-billboard:before{background:0}}.dac-app-bundle-page h2{color:inherit}.dac-app-bundle-page .dac-logos,.dac-app-bundle-page .devsite-landing-row-header{text-align:center}.dac-app-bundle-page .dac-landing-row-hero .dac-landing-row-hero-description{margin-top:64px}.dac-app-bundle-page .dac-featured-cards .devsite-landing-row-item-description-content>p>a,.dac-app-bundle-page .dac-logos a{color:#1a73e8}.dac-app-bundle-page .dac-smaller-app img{max-width:100%}.dac-app-bundle-page .dac-list-wrapped{padding-bottom:0}.dac-app-bundle-page .dac-list-wrapped h2{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-app-bundle-page .dac-list-wrapped h2{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:720px){.dac-app-bundle-page .dac-list-wrapped h2{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-app-bundle-page .dac-list-wrapped .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;padding-top:40px}.dac-app-bundle-page .dac-app-featured-cards{margin-bottom:0}.dac-app-bundle-page .dac-logos h2{margin-top:120px}.dac-app-bundle-page .dac-logos .devsite-landing-row-item-description{padding:32px 0}.dac-app-bundle-page .dac-logos img{margin:0 auto;max-height:80px;max-width:80px}.dac-app-bundle-page .dac-logos h4,.dac-app-bundle-page .dac-logos p{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;white-space:nowrap}.dac-app-bundle-page .dac-logos.dac-bottom-logos .devsite-landing-row-group{-webkit-box-pack:space-evenly;-webkit-justify-content:space-evenly;justify-content:space-evenly}.dac-app-bundle-page .dac-logos.dac-bottom-logos .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:0 0 80px;flex:0 0 80px}.dac-app-bundle-page .dac-get-started{background:url(../images/custom/platform-app-bundle-blob.svg) left -webkit-calc(50% + 300px) bottom -250px/500px no-repeat;background:url(../images/custom/platform-app-bundle-blob.svg) left calc(50% + 300px) bottom -250px/500px no-repeat;margin-top:64px}.dac-app-bundle-page .dac-get-started .devsite-landing-row-item{padding:0 32px 32px}.devsite-landing-row-column>.dac-grow-1.devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-docs-overview-page .dac-grow-column-4-8 .devsite-landing-row-column:first-child,.dac-docs-overview-page .dac-grow-column-4-8.dac-column-switch .devsite-landing-row-column:last-child{-webkit-box-flex:4;-webkit-flex-grow:4;flex-grow:4}.dac-docs-overview-page .dac-grow-column-4-8 .devsite-landing-row-column:last-child,.dac-docs-overview-page .dac-grow-column-4-8.dac-column-switch .devsite-landing-row-column:first-child{-webkit-box-flex:8;-webkit-flex-grow:8;flex-grow:8}.dac-docs-overview-page .dac-column-switch.devsite-landing-row .devsite-landing-row-column:not(:last-child){margin-left:32px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.dac-docs-overview-page .dac-column-switch.devsite-landing-row .devsite-landing-row-column:not(:first-child){margin-left:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.dac-docs-overview-page .dac-guides-group{-webkit-column-break-inside:avoid;break-inside:avoid;-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;margin-bottom:16px}.dac-docs-overview-page .dac-guides-flex-columns .dac-guides-group{padding-left:0}.dac-docs-overview-page .dac-guides-group h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0}.dac-docs-overview-page .dac-landing-row-bg-blob-5,.dac-docs-overview-page .dac-landing-row-bg-blue{min-width:280px}.dac-docs-overview-page .dac-grow-column-4-8 .dac-guides-flex-columns-xs,.dac-docs-overview-page .dac-illustration-keyline-1,.dac-docs-overview-page .dac-illustration-keyline-3{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-docs-overview-page .dac-heading-small h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:26px}.dac-guides-flex-columns .dac-guides{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-guides-flex-columns .dac-guides-group{-webkit-box-flex:0;-webkit-flex:0 0 33%;flex:0 0 33%;padding:0 16px}.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description-content,.dac-docs-overview-page .dac-landing-row-item .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description-content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-docs-overview-page .dac-center .dac-samples-form select{margin-left:16px}.dac-docs-overview-page .dac-center .dac-samples-form .dac-button{margin:8px 16px}.dac-docs-overview-page .dac-subtitle{margin-bottom:48px;margin-top:88px}.dac-docs-overview-page .devsite-landing-row[background=grey] .devsite-landing-row-group{-webkit-flex-wrap:nowrap;flex-wrap:nowrap}@media screen and (max-width:1200px){.dac-guides-flex-columns .dac-guides-group{-webkit-flex-basis:50%;flex-basis:50%;max-width:50%}}@media screen and (max-width:1000px){.dac-guides-flex-columns .dac-guides-group{-webkit-flex-basis:100%;flex-basis:100%;max-width:100%}}@media screen and (max-width:720px){.dac-docs-overview-page .dac-subtitle{margin-bottom:16px;margin-top:16px}.dac-docs-overview-page .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-docs-overwiew-page .dac-landing-row-hero{margin-bottom:0}.dac-docs-overview-page .devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{margin-left:0;margin-top:40px;width:100%}.dac-docs-overview-page .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child),.dac-docs-overview-page .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child).dac-landing-row-bg-blob-5{margin:40px 0 0}.dac-docs-overview-page .dac-column-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:last-child){margin-left:0}}.dac-full-width-page h1{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-full-width-page h1{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-full-width-page h2{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-full-width-page h2{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-full-width-page .dac-sub-hero{margin-top:50px}@media screen and (min-width:1000px) and (max-width:1400px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image{-webkit-box-flex:2;-webkit-flex-grow:2;flex-grow:2}}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image{margin:0}}.dac-full-width-page .dac-sub-hero .dac-sub-hero-image img{margin:0 0 40px 32px;max-width:360px;width:100%}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image img{margin:0 0 40px}}@media screen and (min-width:1000px) and (max-width:1400px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-copy{-webkit-box-flex:3;-webkit-flex-grow:3;flex-grow:3}}.dac-full-width-page .dac-sub-hero .dac-sub-hero-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin-top:64px}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-buttons{display:block}}.dac-full-width-page .dac-features .devsite-landing-row-item-description{padding:0 0 0 96px}.dac-full-width-page .devsite-landing-row-item{position:relative}.dac-full-width-page .dac-features img{height:auto;left:0;max-width:64px;position:absolute;top:0}.dac-full-width-page .dac-components h4,.dac-full-width-page .dac-features h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-full-width-page .dac-components h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-full-width-page .dac-components .devsite-landing-row-item-image{background:0;max-width:320px}.dac-full-width-page .dac-components ul{list-style:none;padding-left:0}.dac-full-width-page .dac-components p{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:0}.dac-full-width-page .dac-components .dac-button{margin:16px auto 16px 0}.dac-full-width-page.devsite-landing-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px)}.dac-full-width-page .dac-success-story .devsite-landing-row-item-image{-webkit-align-self:flex-start;align-self:flex-start;-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;width:auto}@media screen and (max-width:1000px){.dac-full-width-page .dac-success-story .devsite-landing-row-item-image{-webkit-align-self:center;align-self:center}}.dac-full-width-page .devsite-landing-row-logos .devsite-landing-row-group{-webkit-box-align:center;-webkit-align-items:center;align-items:center;text-align:center}.dac-full-width-page .devsite-landing-row-logos img{max-height:100px;max-width:200px}body[theme=android-theme] devsite-header .devsite-header-billboard h1{color:#414141;font:400 44px/54px Euclid,sans-serif;margin-top:0;text-align:center}body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-search-field,body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-search-field:hover{background-color:#fff}.dac-home-page .dac-outline-button{border-color:#414141;color:#414141}.dac-home-page .dac-outline-button:focus,.dac-home-page .dac-outline-button:hover{background:#414141;color:#fff}.dac-home-page .dac-landing-row-bg-slate .dac-outline-button{border-color:#fff;color:#fff}.dac-home-page .dac-landing-row-bg-slate .dac-outline-button:focus,.dac-home-page .dac-landing-row-bg-slate .dac-outline-button:hover{background:#fff;color:#414141}.dac-home-page .dac-alt-flat-button{color:#414141}.dac-home-hero-banner .devsite-landing-row-group{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:64px 40px}.dac-home-hero-banner-text{max-width:630px}.dac-home-hero-banner-text .devsite-landing-row-item-description-content{margin-bottom:16px}.dac-home-hero-banner-image{text-align:center}.dac-home-hero-banner-image img{max-width:450px}.dac-home-hero-banner-image .devsite-landing-row-item-buttons{display:none}.dac-home-page .dac-feature-card-illustration{max-width:200px}.dac-home-page .dac-featured-cards{margin-bottom:112px}.dac-home-build-an-app{padding-top:112px}.dac-home-developer-guides{padding-bottom:112px}.dac-home-developer-guides-heading h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-no-spacing .devsite-landing-row-item-description{padding:0!important}.dac-home-developer-guides img{display:block;margin-bottom:32px;width:80px}.dac-home-design-card h3{margin-bottom:24px}.dac-home-design-card .devsite-landing-row-item-buttons{margin-top:100px}.dac-home-platforms img{display:block;margin:40px auto;max-width:312px}.dac-gservices,.dac-gservices img{margin-top:32px}.dac-gservices .devsite-landing-row-item-buttons{margin-top:200px}.dac-gservices p:last-of-type{margin:0}@media screen and (max-width:1200px){.dac-home-developer-guides .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-20px}.dac-home-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 80px)/2);flex-basis:calc((100% - 80px)/2);margin:20px}.dac-home-developer-guides-heading h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:1000px){body[theme=android-theme] devsite-header .devsite-header-billboard h1{font:400 40px/48px Euclid,sans-serif}.dac-home-build-an-app{padding-top:64px}.dac-home-developer-guides{padding-bottom:64px}.dac-home-developer-guides-heading h3{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:720px){body[theme=android-theme] devsite-header .devsite-header-billboard h1{font:400 32px/40px Euclid,sans-serif}.dac-home-page .dac-featured-cards{margin-bottom:64px}.dac-home-build-an-app{padding-top:32px}.dac-home-hero-banner .devsite-landing-row-group{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-home-hero-banner-image{margin:0!important;width:100%}.dac-home-hero-banner-image .devsite-landing-row-item-buttons{display:block;text-align:left}.dac-home-hero-banner-text .devsite-landing-row-item-buttons{display:none}.dac-home-developer-guides{padding-bottom:32px}.dac-gservices .devsite-landing-row-item-buttons,.dac-home-design-card .devsite-landing-row-item-buttons{margin-top:24px}}.dac-jetpack-page .dac-cta.dac-center .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-jetpack-page .dac-cta.dac-center .devsite-landing-row-item-buttons{display:block}.dac-jetpack-page .dac-component h3 p{font-weight:400;height:130px}.dac-jetpack-page .dac-component button{display:none}.dac-jetpack-page .devsite-landing-row-logos img{padding:12px}.dac-jetpack-page .dac-testimonial a,.dac-jetpack-page .dac-testimonial a:link,.dac-jetpack-page .dac-testimonial a:visited{color:inherit;font-weight:700}@media screen and (max-width:1200px){.dac-jetpack-page .dac-components .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-jetpack-page .dac-components .dac-component{-webkit-flex-basis:-webkit-calc((100% - 40px)/2);flex-basis:calc((100% - 40px)/2)}.dac-jetpack-page .dac-components.devsite-landing-row .devsite-landing-row-item:not(:first-child){margin:40px 0 0}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(2){margin:0 0 0 40px}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(4){margin:40px 0 0 40px}}@media screen and (max-width:1000px){.dac-jetpack-page .dac-features .devsite-landing-row-group,.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group{display:block}.dac-jetpack-page .dac-features.devsite-landing-row-3-up .devsite-landing-row-item,.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group .devsite-landing-row-item{margin:32px 0 0}.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group .devsite-landing-row-item:first-of-type{margin-top:0}.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos){padding-bottom:32px}}@media screen and (max-width:600px){.dac-jetpack-page .dac-row-logos .devsite-landing-row-group{display:block}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(2),.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(4){margin:40px 0 0}.dac-jetpack-page .dac-component h3 p{height:100px}}.dac-kotlin-page .dac-sub-hero-image img{max-height:300px}.dac-kotlin-page .dac-code-snippet .devsite-landing-row-header{margin:0 auto}.dac-kotlin-page .dac-center .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-kotlin-page .dac-components .devsite-landing-row-item-image img{height:80px;width:auto}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;flex-flow:row wrap;margin:0;text-align:center}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-description-content{margin-bottom:64px}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item-content{display:block}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item{-webkit-box-flex:1;-webkit-flex:1 0 8.33333%;flex:1 0 8.33333%;margin:8px 0;padding:8px}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-group{-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.dac-kotlin-page .dac-landing-row-item-icon-container-bg-grey .devsite-landing-row-item-icon-container{background-color:#f7f9fa}@media screen and (max-width:1000px){.dac-kotlin-page .dac-features .devsite-landing-row-group{display:block}.dac-kotlin-page .devsite-landing-row-3-up.dac-features .devsite-landing-row-item{margin:32px 0 0}}@media screen and (max-width:840px){.dac-kotlin-page .devsite-landing-row-2-up .dac-landing-row,.dac-kotlin-page .devsite-landing-row-2-up .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-kotlin-page .devsite-landing-row.dac-label .devsite-landing-row-group{display:block}}@media screen and (max-width:720px){.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item{-webkit-box-flex:1;-webkit-flex:1 0 16.66667%;flex:1 0 16.66667%}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-buttons{margin-top:0}.dac-kotlin-page .dac-media{-webkit-align-self:center;align-self:center}}.dac-news-page .dac-resource-widget-more,.dac-news-page [maxResults] .dynamic-content-paging{display:none!important}.dac-news-page h1{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-news-page .dac-sign-up .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-news-page .dac-sign-up .devsite-landing-row-item-body{-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:70px;text-align:center}.dac-news-page .dac-sign-up .devsite-landing-row-item-description-content{margin:0 0 24px}.dac-news-page .devsite-card-image-bg{padding:0;height:170px}.dac-news-page .devsite-card-wrapper{background:#f7f9fa;-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px);overflow:hidden}.dac-news-page .devsite-card,.dac-news-page .devsite-card-content,.dac-news-page .devsite-card>a{-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1)}.dac-news-page .devsite-card>a{background-color:#f7f9fa}.dac-news-page .devsite-card{height:-webkit-calc(100% + 170px);height:calc(100% + 170px);margin-bottom:-170px;-webkit-transform:translateY(-170px);-o-transform:translateY(-170px);transform:translateY(-170px)}.dac-news-page .devsite-card:hover .devsite-card-content,.dac-news-page .devsite-card:hover>a{-webkit-transform:translateY(170px);-o-transform:translateY(170px);transform:translateY(170px)}.dac-news-page .devsite-card-author{background:#f7f9fa}.dac-news-page .devsite-card-author:after{background:-webkit-gradient(linear,left top,left bottom,from(rgba(247,249,250,0)),to(#f7f9fa));background:-webkit-linear-gradient(rgba(247,249,250,0),#f7f9fa);background:-o-linear-gradient(rgba(247,249,250,0),#f7f9fa);background:linear-gradient(rgba(247,249,250,0),#f7f9fa);bottom:100%;content:"";height:25px;left:0;position:absolute;width:100%}.dac-news-page [dynamic-card-type=medium] .devsite-card-author{background:rgba(0,0,0,.94)}.dac-news-page [dynamic-card-type=medium] .devsite-card-author:after{background:-webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.94)));background:-webkit-linear-gradient(transparent,rgba(0,0,0,.94));background:-o-linear-gradient(transparent,rgba(0,0,0,.94));background:linear-gradient(transparent,rgba(0,0,0,.94))}@media screen and (max-width:1000px){.dac-news-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}.dac-news-page .devsite-landing-row-group{display:block}.dac-news-page .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:0}.dac-news-page .dac-sign-up .devsite-landing-row-item-body{padding:35px}}@media screen and (max-width:720px){.dac-news-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}}.dac-platforms-sublandings .dynamic-content-paging{display:none}.dac-platforms-sublandings .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}.dac-platforms-sublandings .dac-platforms-sublandings-basics .dac-platforms-img{display:block;margin:16px auto 0;width:480px}.dac-platforms-sublandings .dac-platforms-sublandings-basics .devsite-landing-row-group .devsite-landing-row-column{width:100%}.dac-platforms-sublandings .dac-banner-card.devsite-landing-row .dac-button{margin-top:100px}.dac-platforms-sublandings .devsite-landing-row-logos .devsite-landing-row-item{text-align:center}.dac-platforms-sublandings .devsite-landing-row-logos .devsite-landing-row-item .devsite-landing-row-item-image a{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-platforms-sublandings .dac-featured-cards .devsite-landing-row-column img{max-width:75px}.dac-platforms-sublandings .dac-grow-1 h3,.dac-platforms-sublandings .dac-grow-5 h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-platforms-sublandings .dac-grow-1 h3,.dac-platforms-sublandings .dac-grow-5 h3{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-platforms-sublandings .dac-grow-1,.dac-platforms-sublandings .dac-sublandings-hero .devsite-landing-row-item-description,.dac-things-page .dac-full-height,.dac-tv-page .dac-platforms-sublandings-basics .dac-full-height{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-item:not(:first-child){margin-left:40px;margin-top:0}.dac-platforms-sublandings-basics .devsite-landing-row-column .devsite-landing-row-item:last-child{margin-left:0}.dac-platforms-sublandings-basics .devsite-landing-row-column:not(:first-child) .devsite-landing-row-item:last-child{margin:40px 0 0}.dac-sublanding-title.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{font-weight:300}.dac-oreo-page .dac-list-wrapped a,.dac-platforms-sublandings .dac-sublanding-title.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description,.dac-platforms-sublandings .dac-sublanding-title h2,.dac-preview-page .dac-list-wrapped a,.dac-security-page .dac-list-wrapped a{color:inherit}.dac-platforms-sublandings .dac-sublanding-title .devsite-landing-row-item-description{padding-left:0;padding-top:0}.dac-chrome-page .dac-featured-cards .devsite-landing-row-item-description,.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-platforms-sublandings .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child).dac-grow-lg-5.dac-full-width-media,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-security-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-things-page .devsite-landing-row-item-description:first-of-type{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-auto-page .dac-landing-row-bg-slate,.dac-platforms-sublandings .dac-landing-row-bg-slate.dac-item-switch{margin-top:0}.dac-oreo-page .devsite-landing-row-1-up .devsite-landing-row-item:not(.dac-stack-items-block-sm) .devsite-landing-row-item-description,.dac-preview-page .devsite-landing-row-1-up .devsite-landing-row-item-description{padding-left:0}.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-list-item,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-list-item,.dac-security-page .dac-list-wrapped .devsite-landing-row-item-list-item{-webkit-flex-basis:50%;flex-basis:50%;margin:0 0 24px}.dac-oreo-page .dac-list-wrapped h4,.dac-preview-page .dac-list-wrapped h4,.dac-security-page .dac-list-wrapped h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-list,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-list{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-landing-row-bg-blob-7 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-auto-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/auto-design.svg)}.dac-platforms-sublandings.dac-auto-page .devsite-landing-row-logos .devsite-landing-row-item .devsite-landing-row-item-image img{height:100%;max-width:115px;width:100%}.dac-auto-page .devsite-landing-row-logos img{width:-webkit-max-content;width:-moz-max-content;width:max-content}.dac-chrome-page .dac-banner-card-bg-img-item{background-image:url(../images/custom/chrome-design.svg)}.dac-chrome-page .dac-featured-cards .devsite-landing-row-group{display:block}.dac-tv-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/tv-design.svg)}.dac-wear-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/wear-os-design.svg)}.dac-things-page .dac-banner-card.devsite-landing-row img{display:none;margin-top:32px}.dac-things-page .dac-banner-card-bg-img-item.devsite-landing-row-item-no-media{margin:0}.dac-things-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}.dac-platforms-sublandings .dac-item-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:last-child){margin-left:32px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.dac-platforms-sublandings .dac-item-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin-left:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.dac-platforms-sublandings .devsite-landing-row-item-list h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-column,.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-item:not(:first-child){margin-left:0;margin-top:40px}.dac-platforms-sublandings .dac-platforms-sublandings-basics .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-chrome-page .dac-featured-cards .devsite-landing-row-group{max-width:100%}.dac-things-page .dac-banner-card.devsite-landing-row img,.dac-tv-page .dac-banner-card.devsite-landing-row img{display:block}.dac-things-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item,.dac-tv-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}}@media screen and (max-width:720px){.dac-platforms-sublandings .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}.dac-oreo-page .dac-list-wrapped h4,.dac-preview-page .dac-list-wrapped h4,.dac-security-page .dac-list-wrapped h4{font:500 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-android-go-page .devsite-landing-row[background=grey] .devsite-landing-row-item-description,.dac-indie-games .dac-landing-row-bg-slate:not(.dac-stack-items-block-sm) .devsite-landing-row-item-description,.dac-indie-games .devsite-landing-row-item[background=grey] .devsite-landing-row-item-description,.dac-play-guides .dac-landing-row-bg-slate .devsite-landing-row-item-description,.dac-play-guides .dac-stack-items-block-sm .devsite-landing-row-item-description:first-of-type,.dac-subscriptions .dac-card-image--bottom .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-play-guides h3{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;margin-bottom:12px}.dac-play-guides h3 img{-webkit-align-self:baseline;align-self:baseline;margin-bottom:24px}.dac-play-guides .devsite-steps{padding:24px 40px 40px}.dac-play-guides .dac-card-image--bottom .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-guides .dac-illustration-block{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end}.dac-play-guides .dac-illustration-block img{max-width:100%}.dac-android-go-page .devsite-landing-row-3-up h2,.dac-subscriptions .devsite-landing-row-1-up:not(.dac-heading-large) h2{margin-top:40px}.dac-android-go-page .devsite-landing-row[background=grey],.dac-play-guides .devsite-landing-row-1-up.dac-landing-row-bg-slate{background-clip:content-box}.dac-best-practices h3 img{max-height:90px}.dac-best-practices h3 button{display:none}.dac-play-academy .dac-landing-hero{background:url(../images/custom/play-academy-banner.svg) bottom/35% no-repeat;margin-bottom:0;padding-bottom:180px}.dac-play-academy .dac-cards-row .devsite-landing-row-group{background-color:#fff;margin:0 32px;padding:32px}.dac-play-academy .dac-cards-row .devsite-landing-row-item:last-of-type{-webkit-flex-basis:58%;flex-basis:58%}.dac-play-academy .dac-cards-row img{min-width:120px}.dac-play-academy .dac-play-academy-footer{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-play-academy .dac-play-academy-footer p{text-align:center}.dac-indie-games .dac-landing-row-item-indie-corner .devsite-landing-row-item-description{background:url(../images/custom/indie-games-indie-corner-background.svg) 100% 100%/auto 70% no-repeat;margin-right:0}.dac-indie-games .dac-landing-row-item-device .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse}.dac-indie-games .dac-landing-row-item-device-image{-webkit-box-flex:0;-webkit-flex:0 0 30%;flex:0 0 30%}.dac-indie-games .dac-landing-row-item-device-image img{max-width:100%}.dac-startups-page .dac-sublanding-title-normal{margin-top:64px}.dac-startups-page .dac-illustration-block{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:64px}.dac-android-go-page .devsite-landing-row[background=grey] .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:1400px){.dac-play-guides:not(.dac-play-academy):not(.dac-best-practices) .devsite-landing-row-3-up .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-guides .devsite-landing-row-3-up .devsite-landing-row-item.dac-landing-row-bg-slate{margin:32px 0 0}.dac-indie-games .devsite-landing-row-3-up:last-of-type .devsite-landing-row-item:first-of-type,.dac-startups-page .devsite-landing-row-3-up:not(.force-spacing) .devsite-landing-row-group div:first-of-type{margin-top:0}.dac-subscriptions .devsite-landing-row-3-up{padding-bottom:0}}@media screen and (max-width:1200px){.dac-indie-games .dac-landing-row-item-indie-corner .devsite-landing-row-item-description{background-image:none}.dac-indie-games .dac-landing-row-item-device .devsite-landing-row-item-description-content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}@media screen and (max-width:1000px){.dac-families-page .devsite-landing-row-2-up .devsite-landing-row-group,.dac-indie-games .devsite-landing-row-2-up .devsite-landing-row-group,.dac-indie-games .devsite-landing-row-3-up .devsite-landing-row-group,.dac-startups-page .devsite-landing-row-2-up .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-families-page .devsite-landing-row-2-up .devsite-landing-row-item.dac-card-image--bottom,.dac-play-guides .devsite-landing-row-2-up .devsite-landing-row-item.dac-stack-items-block-sm{margin:32px 0 0}.dac-play-academy .dac-cards-row img{width:85px}.dac-play-academy .dac-cards-row .devsite-landing-row-item:last-of-type{margin:0}.dac-play-academy .dac-cards-row .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-academy .dac-landing-hero{-o-background-size:50%;background-size:50%}.dac-indie-games .dac-landing-row-item-device-image img{max-width:30%}}.dac-gpi-page h2{color:#414141}.dac-gpi-page .dac-landing-row-hero img{margin-bottom:40px;max-width:380px}.dac-gpi-page .video-wrapper{float:none;margin:auto auto 40px;width:-webkit-calc(100% - 40px);width:calc(100% - 40px)}.dac-gpi-page .dac-landing-row-bg-slate{background-clip:content-box}.dac-gpi-page .dac-center .devsite-landing-row-item-description,.dac-gpi-page .dac-landing-row-bg-slate .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-gpi-page .dac-landing-row-bg-slate .devsite-landing-row-item-list{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:55px 0 7px}.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding-left:32px}@media screen and (max-width:1200px){.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:1000px){.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-gpi-page .dac-landing-row-bg-slate h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-gpi-page .dac-landing-row-bg-slate li{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0 0 24px;padding-left:32px}.dac-play-page .dynamic-content-paging{display:none}.dac-play-sublandings .dac-sublandings-hero{padding:86px 0}.dac-play-sublandings .dac-banner-img .devsite-landing-row-item-no-description{height:288px;padding-bottom:0;padding-top:0}.dac-play-sublandings .dac-sublandings-hero-copy img{margin-bottom:16px;width:54px}.dac-play-sublandings .dac-sublandings-hero h1{color:inherit;font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-play-sublandings .dac-sublandings-section-header .dac-sublandings-section-header-img img{display:block;margin:0 auto;max-width:500px}.dac-play-billing-page .dac-sublandings-section-header img,.dac-play-store-page .dac-sublandings-section-header .dac-sublandings-section-header-img .devsite-landing-row-item-description-content{min-height:230px}.dac-play-sublandings .dac-success-story{margin-left:40px}.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 19px);flex:0 0 calc(50% - 19px)}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item,.dac-play-console-page .dac-bottom-spacing.dac-bg-img-in-column .devsite-landing-row-item,.dac-play-console-page .dac-cta .devsite-landing-row-item-description,.dac-play-store-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:nth-of-type(3)),.dac-play-sublandings .dac-resources-heading .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-billing-page .dac-sublandings-hero-image img,.dac-play-services-page .dac-sublandings-hero-image img{max-width:580px}.dac-play-page .dac-illustration-play-console h3{margin-bottom:26px}.dac-play-page .dac-grow-5 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-play-page .dac-landing-row-bg-illustration-3:after{min-height:300px}.dac-play-page .dac-landing-row-bg-blob-3 .devsite-landing-row-item-buttons,.dac-play-page .dac-landing-row-bg-illustration-3 .devsite-landing-row-item-buttons{bottom:32px;left:32px;position:absolute}.dac-play-page .dac-basis-9.devsite-landing-row-item{-webkit-flex-basis:68.5%;flex-basis:68.5%;margin-bottom:0}.dac-play-console-page .dac-bg-img-in-column .devsite-landing-row-group{background:url(../images/custom/google-play-console-grow-business.svg) 0 100%/700px no-repeat}.dac-play-console-page .dac-sublandings-section-header .devsite-landing-row-item-description{padding:32px}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-group{background:url(../images/custom/google-play-console-release-confidence.svg) 100% 100%/700px no-repeat}.dac-play-console-page .dac-banner-img .devsite-landing-row-item-no-description{background:url(../images/custom/google-play-console-focus-quality.svg) 0/cover no-repeat}.dac-play-console-page .dac-sublandings-hero .devsite-landing-row-item-media{-webkit-align-self:center;align-self:center;-webkit-box-flex:1;-webkit-flex:auto;flex:auto;margin-left:32px;width:-webkit-calc(73% - 16px);width:calc(73% - 16px)}.dac-play-console-page .dac-bottom-spacing:not(.dac-bg-img-in-column) .dac-heading-medium{margin:32px 0 0 20px}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:first-of-type{margin:0}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:last-of-type{margin:0 0 0 32px}.dac-play-console-page .dac-landing-row-bg-mint:hover{background-color:#55ffb5;color:#414141}.dac-play-console-page .dac-landing-row-bg-yellow:hover{background-color:#ffd600}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-play-console-page .dac-success-story .devsite-landing-row-item-image img{display:inline-block;float:right;width:120px}.dac-play-console-page .dac-bottom-spacing.dac-bg-img-in-column .devsite-landing-row-item{margin-right:0}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:first-child){margin-left:20px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item{max-width:33%}.dac-play-console-page .dac-cta .devsite-landing-row-item-buttons{margin:auto}.dac-play-console-page .dac-chevron-right{color:#fff}.dac-play-store-page .dac-sublandings-hero{padding-bottom:0}.dac-play-store-page .dac-banner-img .devsite-landing-row-item-no-description{background:url(../images/custom/google-play-store-get-discovered.svg) 0/cover no-repeat}.dac-play-store-page .dac-sublandings-hero-image img{display:block;margin:auto}.dac-play-store-page .dac-resources-heading .devsite-landing-row-item{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-play-store-page .dac-cards-2-1-1 .devsite-landing-row-item-description-content a,.dac-play-store-page .dac-cards-2-1-1 [background]:not([background=grey]) p>a:not(.button){color:#1a73e8;text-decoration:none}.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:32px 0 0 20px}.dac-play-services-page .dac-sublandings-hero-copy .devsite-landing-row-item-buttons,.dac-play-services-page .devsite-landing-row-3-up.dac-bottom-spacing .devsite-landing-row-item.dac-heading-medium{margin-top:32px}.dac-play-services-page .dac-sublandings-section-header img{min-height:240px}.dac-resources-page .devsite-landing-row-3-up .devsite-landing-row-item-description{padding:0}.dac-resources-page h3:first-child{color:#1a73e8;font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:32px}.dac-resources-page .devsite-landing-row-3-up .devsite-landing-row-item-media{margin-bottom:0}.dac-play-policies-page .dac-code-snippet img{margin:0 auto}@media screen and (max-width:1400px){.dac-play-sublandings .dac-success-story{margin-left:0}.dac-play-sublandings .dac-sublandings-section-header .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-console-page .devsite-landing-row-4-up .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}}@media screen and (max-width:1200px){.dac-play-sublandings .dac-sublandings-hero h1{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-play-billing-page .dac-cards-2-1-1 .devsite-landing-row-item,.dac-play-services-page .devsite-landing-row-4-up.dac-bottom-spacing .devsite-landing-row-item,.dac-play-sublandings .dac-cards-2-1-1.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child),.dac-play-sublandings:not(.dac-play-console-page) .dac-cards-1-1-2.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:32px 0 0}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-group,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4{-webkit-box-flex:1;-webkit-flex:1 1 40%;flex:1 1 40%}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:last-of-type,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4:nth-of-type(3){margin-left:0;margin-top:32px}.dac-play-console-page .dac-bottom-spacing .dac-heading-medium,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4:last-of-type{margin-top:32px}.dac-play-console-page .dac-bottom-spacing .dac-heading-medium,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0 -webkit-calc((100% - 40px)/2);flex:1 0 calc((100% - 40px)/2)}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column,.dac-play-services-page .devsite-landing-row-4-up .devsite-landing-row-group,.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-group,.dac-play-sublandings:not(.dac-play-console-page) .dac-cards-1-1-2 .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}@media screen and (max-width:1000px){.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-flex-basis:auto;flex-basis:auto}.dac-play-sublandings .dac-sublandings-hero-copy,.dac-play-sublandings .dac-sublandings-hero .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-billing-page .dac-sublandings-hero-image img,.dac-play-services-page .dac-sublandings-hero-image img{min-height:400px}.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:first-of-type,.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:last-of-type,.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:last-of-type,.dac-play-console-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),.dac-play-console-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin-top:32px}.dac-play-page .dac-grow-5,.dac-play-page .dac-grow-7{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-page .devsite-landing-row:not(.devsite-landing-row-4-up) .dac-grow-4{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;margin-left:0;margin-top:32px}.dac-play-page .dac-basis-9.devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%}.dac-play-page .devsite-landing-row.dac-landing-row-bg-grey .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-play-console-page .dac-sublandings-hero .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:none;flex:none;margin-left:0;-webkit-box-ordinal-group:4;-webkit-order:3;order:3;padding-top:20px;width:100%}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:last-of-type,.dac-play-console-page .devsite-landing-row-4-up.dac-bottom-spacing .dac-heading-medium,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%;margin-left:0}.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item:first-child{margin-top:0}.dac-play-store-page .devsite-landing-row.dac-cards-1-1-1-1+.dac-cards-1-1-1-1{padding-top:20px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;max-width:100%}.dac-play-billing-page.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{margin-bottom:0}.dac-play-policies-page .dac-sublandings-hero-copy{margin-bottom:32px}}@media screen and (max-width:720px){.dac-play-sublandings .dac-landing-row-item-icon-container-left{margin-top:0}.dac-play-page .dac-landing-row-bg-blob-3 .devsite-landing-row-item-buttons,.dac-play-page .dac-landing-row-bg-illustration-3 .devsite-landing-row-item-buttons{bottom:24px;left:24px}.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:last-of-type{margin-top:32px}}@media screen and (max-width:600px){.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column:last-of-type{height:300px}.dac-play-console-page .dac-bg-img-in-column .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;height:300px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:first-child){margin-left:0}.dac-play-services-page .devsite-landing-row-3-up.dac-bottom-spacing .devsite-landing-row-item.dac-heading-medium:first-of-type{margin-top:0}}.dac-security-page .dac-slate{color:#455a64}.dac-subscribe-page .dac-subscribe-heading{color:#414141;margin:24px 0 40px}.dac-subscribe-page .dac-field,.dac-subscribe-page .dac-field-group{margin:0 4px 32px}.dac-subscribe-page .dac-names-field{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;width:60%}.dac-subscribe-page .dac-names-field .dac-field{-webkit-box-flex:1;-webkit-flex:1;flex:1;min-width:260px}.dac-subscribe-page label,.dac-subscribe-page legend{color:#202124;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400}.dac-subscribe-page .dac-label-description{color:#5f6368;font:500 12px/18px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400;line-height:24px;margin:0 32px}.dac-subscribe-page .dac-language-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center}[dir=ltr] .dac-subscribe-page .dac-language-container label{margin-right:12px}.dac-subscribe-page .dac-form-info{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subscribe-page .dac-form-after-submit h1{color:#414141;font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:0}.dac-subscribe-page .dac-form input[type=text]{padding:10px}.dac-subscribe-page .dac-form input[type=email]:focus{border-bottom:2px solid #4285f4}.dac-subscribe-page .dac-form .dac-email{margin-bottom:0;min-width:260px;width:60%}.dac-subscribe-page devsite-framebox{margin-left:20px}.dac-subscribe-page.dac-subscribe-container{background-color:#f7f9fa;margin:0 auto;max-width:1000px;padding:50px;position:relative}.dac-subscribe-page iframe{width:100%}.dac-subscribe-page .dac-form .dac-email{-webkit-border-radius:2px;border-radius:2px;margin-bottom:40px;min-width:225px;padding:10px;width:70%}.dac-subscribe-page .dac-language-container{display:-webkit-box;display:-webkit-flex;display:flex;margin:45px 0 30px}.dac-subscribe-page .dac-language-container h3,.dac-subscribe-page .dac-row{margin:0}.dac-subscribe-page .dac-form-icon{height:230px;position:absolute;right:80px;top:495px}@media screen and (max-width:1024px){.dac-subscribe-page .dac-form-icon .dac-subscribe-page .dac-form-icon{display:none}}.dac-subscribe-page .dac-outline-button{margin-top:45px}@media screen and (max-width:820px){.dac-subscribe-page .dac-form-icon{display:none}body[layout=full][type=landing].dac-subscribe-page .devsite-main-content{padding:0}.dac-subscribe-page .dac-subscribe-container{padding:25px}.dac-subscribe-page .dac-form .dac-email{width:85%}}@media screen and (max-width:470px){.dac-subscribe-page .dac-language-container{display:block}}@font-face{font-family:Euclid;src:url(../fonts/custom/euclid.ttf) format("truetype"),url(../fonts/custom/euclid.otf) format("opentype"),url(../fonts/custom/euclid.woff) format("woff")}.dac-studio-page .download td{vertical-align:middle}.dac-studio-page #studio-downloaded-dialog{height:auto}.dac-studio-page #studio-downloaded-dialog .devsite-dialog-contents{height:-webkit-calc(100% - 36px);height:calc(100% - 36px);margin-bottom:24px}.dac-studio-page .studio-downloaded-links{padding:16px 0}.dac-studio-page .studio-downloaded-links img{background:#d7d7d7;margin:0 0 16px;width:100%}.dac-studio-page .devsite-landing-row-item-description ul{padding-left:16px}.dac-studio-page .dac-info-size{font-size:smaller;margin-top:4px;padding:0 8px}.dac-studio-page .dac-sizes{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-studio-page .dac-feature-last{margin-bottom:40px}.dac-studio-page .dac-btn-block{margin-top:32px;padding:0 32px}.dac-studio-page .dac-landing-row-hero{margin-bottom:32px}.dac-studio-page .dac-download-options{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:100%}.dac-studio-page .dac-download-info{border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;height:100%;padding:24px 12px;text-align:center}.dac-studio-page .dac-download-info h2{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:500}.dac-studio-page .dac-download-info.show{-webkit-box-flex:6;-webkit-flex:6;flex:6;margin:0 32px 0 0}.dac-studio-page .dac-download-info.solo{-webkit-box-flex:4;-webkit-flex:4;flex:4;max-width:500px}.dac-studio-page .dac-landing-links{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:24px 0;width:100%}.dac-studio-page .dac-landing-links .button{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:12px 40px;min-width:240px;text-align:center}.dac-studio-page .dac-dropshadow{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.dac-studio-page .dac-video-card h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-studio-page .devsite-landing-row-item-media{background:#455a64;display:-webkit-box;display:-webkit-flex;display:flex}.dac-studio-page .devsite-landing-row-item-image{-webkit-align-self:center;align-self:center;background:#455a64;width:100%}.dac-terms-page .sdk-terms{white-space:pre-wrap;word-wrap:break-word}.dac-archive-page.tos-wall{height:450px;overflow:auto}.dac-archive-page .sdk-terms{color:#757575;border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;font:400 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:300px;margin-bottom:16px;overflow:scroll;padding:12px;white-space:pre-wrap;word-wrap:break-word}.dac-archive-page.tos-wall h2,.dac-archive-page.tos-wall h3{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding:0}.dac-releases-page.updates-box{border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;margin:12px 0;padding:12px 24px 0}.dac-test-page .jd-sumtable th{color:inherit}@media screen and (max-width:1000px){.dac-full-width-content .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n),.dac-full-width-content .devsite-landing-row-item-no-media:not(:first-child){margin:0}.dac-full-width-content .devsite-landing-row-group .devsite-landing-row-item-no-media,.dac-full-width-content .devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-studio-page .dac-btn-block{padding:0}.dac-studio-page .dac-landing-links .button{margin:12px 16px}.dac-studio-page .dac-download-options{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-studio-page .dac-download-info.show,.dac-studio-page .dac-download-info.solo{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-studio-page .dac-download-info.solo{max-width:100%}.dac-studio-page .dac-download-info.show{margin:0 0 16px}}@media screen and (max-width:720px){.dac-full-width-content .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n),.dac-full-width-content .devsite-landing-row-item-no-media:not(:first-child){-webkit-box-flex:0;-webkit-flex:0;flex:0}}devsite-book-nav{max-height:100vh;overflow-x:hidden;overflow-y:auto;position:relative;z-index:1004}body[pending] devsite-book-nav{background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);height:100vh}body[ready] devsite-book-nav[fixed]{-webkit-box-shadow:none;box-shadow:none;contain:content;max-height:100%;position:fixed;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:top,max-height,transform}.devsite-book-nav-bg{background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}body[ready] .devsite-book-nav-bg[fixed]{bottom:0;display:block;position:fixed;top:0;z-index:1}.devsite-book-nav-bg:after{bottom:-10px;content:"";display:block;height:10px;left:0;position:fixed;width:278px}[dir=rtl] .devsite-book-nav-bg:after{left:auto;right:0}devsite-book-nav .devsite-nav{-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-book-nav .devsite-nav-list{padding-bottom:36px}devsite-book-nav .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{border-top:0;margin-top:20px;padding-top:0}devsite-book-nav li .devsite-nav-title{padding-left:24px}[dir=rtl] devsite-book-nav li .devsite-nav-title{padding-left:8px;padding-right:24px}devsite-book-nav devsite-expandable-nav li .devsite-nav-title{padding-left:40px}[dir=rtl] devsite-book-nav devsite-expandable-nav li .devsite-nav-title{padding-left:8px;padding-right:40px}devsite-book-nav devsite-expandable-nav li li .devsite-nav-title{padding-left:56px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li .devsite-nav-title{padding-left:8px;padding-right:56px}devsite-book-nav devsite-expandable-nav li li li .devsite-nav-title{padding-left:72px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li .devsite-nav-title{padding-left:8px;padding-right:72px}devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-title{padding-left:88px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-title{padding-left:8px;padding-right:88px}devsite-book-nav devsite-expandable-nav li li li li li .devsite-nav-title{padding-left:104px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li li .devsite-nav-title{padding-left:8px;padding-right:104px}devsite-book-nav li.devsite-nav-divider .devsite-nav-title{padding-left:0}[dir=rtl] devsite-book-nav li.devsite-nav-divider .devsite-nav-title{padding-right:0}devsite-book-nav .devsite-nav-title{padding-right:8px}[dir=rtl] devsite-book-nav .devsite-nav-title{padding-left:8px;padding-right:0}devsite-book-nav .devsite-nav-list>.devsite-nav-heading:not(.devsite-nav-divider){border-top:1px solid #dadce0;padding-top:11px}devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider){margin-top:12px}devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:24px}[dir=rtl] devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:24px}devsite-book-nav devsite-expandable-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:40px}[dir=rtl] devsite-book-nav devsite-expandable-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:40px}devsite-book-nav devsite-expandable-nav li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:56px}[dir=rtl] devsite-book-nav devsite-expandable-nav li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:56px}devsite-book-nav devsite-expandable-nav li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:72px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:72px}devsite-book-nav devsite-expandable-nav li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:88px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:88px}devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:104px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:104px}devsite-book-nav .devsite-nav-heading.devsite-nav-divider{background:#eceff1;border-bottom:1px solid #dadce0;border-top:1px solid #dadce0;padding:4px 24px 2px}devsite-book-nav .devsite-nav-heading.devsite-nav-divider:first-child{padding-top:4px}devsite-book-nav .devsite-nav-divider>.devsite-nav-title{font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;text-transform:uppercase}devsite-book-nav .devsite-nav-accordion+.devsite-nav-accordion,devsite-book-nav .devsite-nav-divider+.devsite-nav-accordion{border-top:0;padding-top:12px}devsite-book-nav .devsite-nav-accordion+.devsite-nav-divider{border-top:0;padding-top:4px}devsite-book-nav .devsite-nav-accordion+.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider),devsite-book-nav .devsite-nav-divider+.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider),devsite-book-nav .devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider)+.devsite-nav-accordion,devsite-book-nav .devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider)+.devsite-nav-divider{margin-top:12px}devsite-book-nav .devsite-nav-break{height:24px}#devsite-hamburger-menu,#devsite-hamburger-menu[visually-hidden],devsite-book-nav .devsite-mobile-header,devsite-book-nav .devsite-mobile-nav-top{display:none}#devsite-hamburger-menu:before{content:"menu"}devsite-book-nav #devsite-close-nav:before{content:"arrow_back"}[dir=rtl] devsite-book-nav #devsite-close-nav:before{content:"arrow_forward"}devsite-book-nav[top-level-nav] #devsite-close-nav:before{content:"close"}@media screen and (max-width:840px){devsite-book-nav{display:none;height:100vh;max-height:100vh!important;top:0!important;-webkit-transform:translate3d(-280px,0,0)!important;transform:translate3d(-280px,0,0)!important;-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1);z-index:1013}[dir=rtl] devsite-book-nav{-webkit-transform:translate3d(280px,0,0)!important;transform:translate3d(280px,0,0)!important}devsite-book-nav:not([animatable]){-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}body[ready] .devsite-book-nav-bg[fixed]{display:none}body[ready] devsite-book-nav[fixed]{background:#fff;display:block!important;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-book-nav[visually-hidden]{opacity:1!important;pointer-events:auto!important;visibility:visible!important}#devsite-hamburger-menu{display:inline-block}devsite-book-nav #devsite-close-nav{color:#5f6368;-webkit-flex-shrink:0;flex-shrink:0}#devsite-hamburger-menu,devsite-book-nav #devsite-close-nav{height:auto;padding:8px;position:relative;width:auto;z-index:20}[dir=ltr] #devsite-hamburger-menu,[dir=ltr] devsite-book-nav #devsite-close-nav{margin:0 8px 0 -4px}[dir=rtl] #devsite-hamburger-menu,[dir=rtl] devsite-book-nav #devsite-close-nav{margin:0 -4px 0 8px}devsite-book-nav .devsite-mobile-nav-top{display:block}devsite-book-nav .devsite-book-nav-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-transform:translate3d(-268px,0,0)!important;transform:translate3d(-268px,0,0)!important;-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1)}[dir=rtl] devsite-book-nav .devsite-book-nav-wrapper{-webkit-transform:translate3d(268px,0,0)!important;transform:translate3d(268px,0,0)!important}devsite-book-nav:not([animatable]) .devsite-book-nav-wrapper{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}devsite-book-nav .devsite-nav-list{padding-bottom:120px}devsite-book-nav .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{margin-top:0}devsite-book-nav .devsite-mobile-nav-bottom .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{margin-top:13px}devsite-book-nav .devsite-mobile-nav-top .devsite-nav-text{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-book-nav[top-level-nav] .devsite-book-nav-wrapper{-webkit-transform:translateZ(0)!important;transform:translateZ(0)!important}devsite-book-nav:not([top-level-nav]) .devsite-mobile-nav-top,devsite-book-nav[top-level-nav] .devsite-mobile-nav-bottom{height:-webkit-calc(100vh - 64px);height:calc(100vh - 64px);overflow:hidden}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item{border-bottom:1px solid #dadce0}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item>.devsite-nav-title{font-weight:700;padding-bottom:15px;padding-top:16px}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item>.devsite-nav-title:not(.devsite-nav-active){color:#5f6368}devsite-book-nav .devsite-mobile-nav-bottom,devsite-book-nav .devsite-mobile-nav-top{-webkit-flex-shrink:0;flex-shrink:0;width:268px}devsite-book-nav .devsite-mobile-header{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:48px;padding:0 16px;position:relative}devsite-book-nav .devsite-mobile-header .devsite-nav-active{font-weight:400}devsite-book-nav .devsite-nav-responsive-tabs{margin-bottom:12px;margin-top:-11px}devsite-book-nav .devsite-lower-tab-item{margin:0}devsite-book-nav .devsite-nav-responsive-tabs>.devsite-nav-item:last-child{margin-bottom:8px}}@media screen and (max-width:600px){#devsite-hamburger-menu,devsite-book-nav #devsite-close-nav{margin:0 4px 0 -12px}}devsite-book-nav .devsite-product-id-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:56px;padding:20px 24px 2px}devsite-book-nav .devsite-header-no-lower-tabs .devsite-product-id-row{min-height:72px;padding:20px 24px}devsite-book-nav .devsite-product-description-row{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-book-nav .devsite-breadcrumb-list+.devsite-product-description:not(:empty){margin-top:8px}devsite-book-nav .devsite-product-description{font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 180px 0 0}[dir=rtl] devsite-book-nav .devsite-product-description{margin:0 0 0 180px}devsite-book-nav .devsite-product-button-row{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 0 0 24px;z-index:1}[dir=rtl] devsite-book-nav .devsite-product-button-row{margin:0 24px 0 0}@media screen and (max-width:840px){devsite-book-nav .devsite-product-id-row{min-height:72px;padding:20px 24px}[dir=rtl] devsite-book-nav .devsite-product-description,devsite-book-nav .devsite-product-description{margin:0}}@media screen and (max-width:600px){devsite-book-nav .devsite-header-no-lower-tabs .devsite-product-id-row,devsite-book-nav .devsite-product-id-row{-webkit-flex-wrap:wrap;flex-wrap:wrap;padding:20px 16px}devsite-book-nav .devsite-product-button-row{-webkit-flex-basis:100%;flex-basis:100%;margin:16px 0 0}}devsite-book-nav .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}devsite-book-nav .devsite-product-name-link,devsite-book-nav .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}devsite-book-nav .devsite-product-name-link:focus,devsite-book-nav .devsite-product-name-link:hover,devsite-book-nav .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}devsite-book-nav .devsite-site-logo{height:32px}devsite-book-nav .devsite-has-google-wordmark>.devsite-breadcrumb-link,devsite-book-nav .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}devsite-book-nav .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}devsite-book-nav .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}devsite-book-nav .devsite-site-logo-link canvas{height:auto!important}devsite-book-nav .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] devsite-book-nav .devsite-product-logo-container{margin-right:4px}[dir=rtl] devsite-book-nav .devsite-product-logo-container{margin-left:4px}devsite-book-nav .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}devsite-book-nav .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}devsite-book-nav .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-book-nav .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}devsite-book-nav .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){devsite-book-nav .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),devsite-book-nav .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-item,devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-link,devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-list,devsite-book-nav .devsite-product-name-wrapper .devsite-product-name{width:100%}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}devsite-expandable-nav{cursor:pointer;display:block;position:relative}devsite-expandable-nav>.devsite-nav-section{max-width:100%;overflow-y:hidden;-webkit-transition:height .2s;-o-transition:height .2s;transition:height .2s;width:100%;will-change:height}devsite-expandable-nav:not([animatable])>.devsite-nav-section{-webkit-transition:height 1ms;-o-transition:height 1ms;transition:height 1ms}devsite-expandable-nav[collapsed]:not([animating])>.devsite-nav-section{display:none}devsite-expandable-nav[collapsed]:not([connected])>.devsite-nav-section{height:0}devsite-expandable-nav>.devsite-nav-title-no-path{cursor:pointer;outline:0}devsite-expandable-nav>.devsite-nav-title{padding-left:24px}[dir=rtl] devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:24px}devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:40px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:40px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:56px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:56px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:72px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:72px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:88px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:88px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:104px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:104px}devsite-expandable-nav>.devsite-nav-toggle{color:#bdc1c6;cursor:pointer;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav>.devsite-nav-toggle{font-size:18px;position:absolute;top:2px;-webkit-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;-o-transition:-o-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease,-o-transform .2s ease;will-change:transform}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav:not([animatable])>.devsite-nav-toggle{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}devsite-expandable-nav>.devsite-nav-toggle{left:4px}[dir=rtl] devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:4px}devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:20px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:20px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:36px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:36px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:52px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:52px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:68px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:68px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:84px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:84px}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}[dir=rtl] .devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}devsite-expandable-nav>.devsite-nav-toggle:before{content:"arrow_drop_down"}.devsite-nav-accordion{border-bottom:1px solid #dadce0;border-top:1px solid #dadce0;padding:11px 0}.devsite-nav-accordion>devsite-expandable-nav{-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-nav-accordion>devsite-expandable-nav,.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title{color:rgba(0,0,0,.65);-webkit-box-flex:1;-webkit-flex:1 0 196px;flex:1 0 196px;font-weight:700;overflow:hidden}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle{font-size:24px;margin:0 8px 0 0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;-o-transition:-o-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s,-o-transform .5s}[dir=rtl] .devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle{margin:0 0 0 8px}.devsite-nav-accordion>devsite-expandable-nav:not([animatable])>.devsite-nav-toggle{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}.devsite-nav-accordion>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotateX(180deg);transform:rotateX(180deg)}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle:before{content:"expand_less"}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-section{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}devsite-footer-linkboxes{background:#fff;display:block;font:400 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-linkboxes .devsite-footer-linkboxes-list{border-bottom:1px solid #dadce0;display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;padding:0}devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:24px 0}devsite-footer-linkboxes .devsite-footer-linkbox:not(:first-child){margin-left:24px}devsite-footer-linkboxes .devsite-footer-linkbox-heading{font:500 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 8px}devsite-footer-linkboxes .devsite-footer-linkbox-list .devsite-footer-linkbox-heading{margin-top:40px}devsite-footer-linkboxes .devsite-footer-linkbox-list{list-style-type:none;padding:0}devsite-footer-linkboxes .devsite-footer-linkbox-item{margin:0}devsite-footer-linkboxes .devsite-footer-linkbox-link{color:#202124;display:block;padding:8px 0}devsite-footer-linkboxes .devsite-footer-linkbox-link:focus,devsite-footer-linkboxes .devsite-footer-linkbox-link:hover{color:#1a73e8;text-decoration:none}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}}@media screen and (max-width:600px){devsite-footer-linkboxes{padding:0 16px}devsite-footer-linkboxes .devsite-footer-linkboxes-list{display:block}devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-footer-linkboxes .devsite-footer-linkbox:not(:first-child){margin-left:0}}devsite-footer-promos{border-top:1px solid #dadce0;background:#fff;display:block;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-promos .devsite-footer-promos-list{border-bottom:1px solid #dadce0;display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;-webkit-justify-content:space-around;justify-content:space-around;padding:18px 0}devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 1 192px;flex:0 1 192px;margin:20px 0;text-align:center}devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:24px}devsite-footer-promos .devsite-footer-promo-icon{color:rgba(0,0,0,.87);display:block;font-size:48px;height:48px;margin:0 auto 8px;width:48px}devsite-footer-promos .devsite-footer-promo-title{color:rgba(0,0,0,.87);display:block;font-weight:500}devsite-footer-promos .devsite-footer-promo-title:focus,devsite-footer-promos .devsite-footer-promo-title:hover{color:#1a73e8;text-decoration:none}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;padding:0 20px}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}}@media screen and (max-width:840px){.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promos-list,devsite-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;padding:12px 0}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo,devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0;padding:8px 8px 8px 0;text-align:left}[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo,[dir=rtl] devsite-footer-promos .devsite-footer-promo{text-align:right}devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}devsite-footer-promos .devsite-footer-promo-icon{height:32px;margin:0 8px 0 0;width:32px}devsite-footer-promos .devsite-footer-promo-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font-weight:400}devsite-footer-promos .devsite-footer-promo-description{display:none}}@media screen and (max-width:600px){devsite-footer-promos{padding:0 16px}devsite-footer-promos .devsite-footer-promos-list{display:block}}devsite-footer-utility{background:#fff;display:block;font:400 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-utility nav{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;padding:24px 0}devsite-footer-utility .devsite-footer-sites{-webkit-box-align:center;-webkit-align-items:center;align-items:center;border-bottom:1px solid #dadce0;padding:24px 0 23px}devsite-footer-utility .devsite-footer-sites-list{display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;padding:0}devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}[dir=rtl] devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}devsite-footer-utility .devsite-footer-sites-link{color:#202124;display:block;padding:8px 0}devsite-footer-utility .devsite-footer-sites-link:focus,devsite-footer-utility .devsite-footer-sites-link:hover{color:#1a73e8;text-decoration:none}devsite-footer-utility .devsite-footer-sites-logo-link{display:-webkit-box;display:-webkit-flex;display:flex}devsite-footer-utility .devsite-footer-sites-logo{height:32px;margin-top:-4px;width:185px}devsite-footer-utility .devsite-footer-utility-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;list-style:none;min-height:36px;padding:0}devsite-footer-utility .devsite-footer-utility-link{color:#202124}devsite-footer-utility .devsite-footer-utility-link:focus,devsite-footer-utility .devsite-footer-utility-link:hover{color:#1a73e8}devsite-footer-utility .devsite-footer-utility-item{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 8px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item{margin:0 0 0 8px}devsite-footer-utility .devsite-footer-utility-item:last-child{margin-right:0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item:last-child{margin-left:0}devsite-footer-utility .devsite-footer-utility-item:not(:first-child):before{content:"|";margin:0 8px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item:not(:first-child):before{margin:0 0 0 8px}devsite-footer-utility .devsite-footer-utility-item.devsite-footer-utility-button:before{content:"";margin:0}devsite-footer-utility .devsite-footer-utility-button{-webkit-box-align:center;-webkit-align-items:center;align-items:center;line-height:20px;margin-left:auto;padding-left:16px}devsite-footer-utility .devsite-footer-utility-button>a{-webkit-flex-shrink:0;flex-shrink:0;margin:0 0 0 16px}[dir=rtl] devsite-footer-utility .devsite-footer-utility-button>a{margin:0 16px 0 0}devsite-footer-utility .devsite-footer-utility-button>a:focus{text-decoration:none}devsite-footer-utility devsite-language-selector{margin:0 0 0 16px}[dir=rtl] devsite-footer-utility devsite-language-selector{margin:0 16px 0 0}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites{display:block}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-logo{margin-bottom:16px}}@media screen and (max-width:840px){devsite-footer-utility .devsite-footer-sites{display:block}devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}devsite-footer-utility .devsite-footer-sites-logo{margin-bottom:16px;margin-top:0}}@media screen and (max-width:600px){devsite-footer-utility{padding:0 16px}devsite-footer-utility .devsite-footer-sites,devsite-footer-utility .devsite-footer-sites-list,devsite-footer-utility nav{display:block}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item,devsite-footer-utility .devsite-footer-sites-item{margin:0}devsite-footer-utility devsite-language-selector{display:block;margin:16px 0 0}devsite-footer-utility .devsite-footer-utility-list{-webkit-flex-wrap:wrap;flex-wrap:wrap}devsite-footer-utility .devsite-footer-utility-button{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:16px 0 0;padding:0}devsite-footer-utility .devsite-footer-utility-button>a{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto}}devsite-header{display:block;position:relative;z-index:1006}body[ready] devsite-header[fixed]{contain:layout;pointer-events:none;position:fixed;top:0;width:100%}devsite-header .devsite-top-logo-row-wrapper-wrapper{position:relative;z-index:1}body[ready] devsite-header[fixed] .devsite-top-logo-row-wrapper-wrapper:before{content:"";height:400px;position:absolute;-webkit-transform:translateY(-400px);-o-transform:translateY(-400px);transform:translateY(-400px);width:100%}devsite-header[fixed] .devsite-top-logo-row-wrapper-wrapper{pointer-events:all}devsite-header .devsite-collapsible-section{position:relative}devsite-header .devsite-collapsible-section,devsite-header[no-lower-row][fixed]{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-header[fixed] .devsite-collapsible-section{contain:style;pointer-events:all;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:transform}devsite-header .devsite-top-logo-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:48px;padding:0 24px;position:relative}devsite-header .devsite-top-button{background:0;padding:0 8px;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}devsite-header .devsite-top-button,devsite-header .devsite-top-button:active,devsite-header .devsite-top-button:focus,devsite-header .devsite-top-button:hover{border:0}devsite-header .devsite-header-icon-button{display:none;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:24px;min-width:24px;padding:0;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;width:24px}devsite-header .devsite-top-logo-row-middle{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative}@media screen and (max-width:840px){devsite-header{-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-header .devsite-top-logo-row{padding:0 16px}devsite-header .devsite-header-upper-tabs devsite-tabs{margin:0 0 0 16px}[dir=rtl] devsite-header .devsite-header-upper-tabs devsite-tabs{margin:0 16px 0 0}devsite-header .devsite-header-upper-tabs .devsite-doc-set-nav{display:none}}devsite-header .devsite-header-billboard{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:40px 24px 20px;position:relative;z-index:100}devsite-header .devsite-header-billboard h1{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;line-height:1;margin:14px 0;overflow:visible;padding:0}devsite-header .devsite-header-billboard-logo{max-height:64px}devsite-header .devsite-header-billboard-search{margin:0 auto;max-width:816px;padding-bottom:48px}devsite-header .devsite-header-billboard-search devsite-search{width:100%}devsite-header .devsite-header-billboard-search devsite-search .devsite-popout-result{max-height:50vh}@media screen and (max-width:840px){devsite-header .devsite-header-billboard-search{margin:0 24px}}devsite-header .devsite-doc-set-nav-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:48px;padding:0 24px 0 0}[dir=rtl] devsite-header .devsite-doc-set-nav-row{padding:0 0 0 24px}[dir=ltr] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-list{padding-left:24px}[dir=rtl] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-list{padding-right:24px}@media screen and (max-width:840px){devsite-header .devsite-doc-set-nav-row{display:none}}devsite-header devsite-language-selector{margin:0 0 0 16px}[dir=rtl] devsite-header devsite-language-selector{margin:0 16px 0 0}@media screen and (max-width:840px){devsite-header devsite-language-selector{margin:0 0 0 8px}[dir=rtl] devsite-header devsite-language-selector{margin:0 8px 0 0}}@media screen and (max-width:600px){devsite-header devsite-language-selector{display:none}}devsite-header .devsite-header-link{margin:0 -8px 0 16px;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}[dir=rtl] devsite-header .devsite-header-link{margin:0 16px 0 -8px}@media screen and (max-width:840px){devsite-header .devsite-header-link{display:none}}devsite-header .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}devsite-header .devsite-product-name-link,devsite-header .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}devsite-header .devsite-product-name-link:focus,devsite-header .devsite-product-name-link:hover,devsite-header .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}devsite-header .devsite-site-logo{height:32px}devsite-header .devsite-has-google-wordmark>.devsite-breadcrumb-link,devsite-header .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}devsite-header .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}devsite-header .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}devsite-header .devsite-site-logo-link canvas{height:auto!important}devsite-header .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] devsite-header .devsite-product-logo-container{margin-right:4px}[dir=rtl] devsite-header .devsite-product-logo-container{margin-left:4px}devsite-header .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}devsite-header .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}devsite-header .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-header .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}devsite-header .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){devsite-header .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),devsite-header .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-item,devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-link,devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-list,devsite-header .devsite-product-name-wrapper .devsite-product-name{width:100%}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}devsite-header .devsite-product-id-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:56px;padding:20px 24px 2px}devsite-header .devsite-header-no-lower-tabs .devsite-product-id-row{min-height:72px;padding:20px 24px}devsite-header .devsite-product-description-row{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-header .devsite-breadcrumb-list+.devsite-product-description:not(:empty){margin-top:8px}devsite-header .devsite-product-description{font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 180px 0 0}[dir=rtl] devsite-header .devsite-product-description{margin:0 0 0 180px}devsite-header .devsite-product-button-row{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 0 0 24px;z-index:1}[dir=rtl] devsite-header .devsite-product-button-row{margin:0 24px 0 0}@media screen and (max-width:840px){devsite-header .devsite-product-id-row{min-height:72px;padding:20px 24px}[dir=rtl] devsite-header .devsite-product-description,devsite-header .devsite-product-description{margin:0}}@media screen and (max-width:600px){devsite-header .devsite-header-no-lower-tabs .devsite-product-id-row,devsite-header .devsite-product-id-row{-webkit-flex-wrap:wrap;flex-wrap:wrap;padding:20px 16px}devsite-header .devsite-product-button-row{-webkit-flex-basis:100%;flex-basis:100%;margin:16px 0 0}}devsite-header[search-expanded] .devsite-header-upper-tabs{display:none}devsite-header[search-expanded] devsite-search{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}devsite-header [transition]{-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s}@media screen and (max-width:840px){devsite-header[search-active] .devsite-product-name-wrapper,devsite-header[search-active] devsite-language-selector,devsite-header[search-active] devsite-user{display:none}devsite-header[search-active] devsite-search{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-transform:none!important;-o-transform:none!important;transform:none!important}devsite-header[search-active] .devsite-header-upper-tabs{-webkit-box-flex:0;-webkit-flex:0 1;flex:0 1;overflow:hidden}devsite-header[search-active] .devsite-top-logo-row devsite-search{margin:6px 0}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-searchbox{width:100%}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-searchbox .devsite-search-image{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox:before,devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-popout{left:-60px;width:-webkit-calc(100vw + 16px);width:calc(100vw + 16px)}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox:before,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-popout{left:auto;right:-60px}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button{margin:0 0 0 16px}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button{margin:0 16px 0 0}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button[search-open]{display:none}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button[search-close]{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header [transition]{-webkit-transition:none;-o-transition:none;transition:none}}devsite-header .devsite-search-background{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-sizing:content-box;box-sizing:content-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:stretch;-webkit-justify-content:stretch;justify-content:stretch;margin:0 0 0 24px;padding:6px 0;pointer-events:none;position:absolute;right:0;-webkit-transform-origin:right center;-o-transform-origin:right center;transform-origin:right center;-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;will-change:transition;z-index:9}[dir=rtl] devsite-header .devsite-search-background{left:0;margin:0 24px 0 0;right:auto;-webkit-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}devsite-header .devsite-search-background:after{background:#f1f3f4;content:"";-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:100%;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s}devsite-header[billboard] .devsite-search-background{display:none}devsite-header[billboard][bottom-row--hidden] .devsite-search-background{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header[billboard] .devsite-top-logo-row devsite-search .devsite-search-form{opacity:1;-webkit-transition:opacity .2s,-webkit-transform .2s;transition:opacity .2s,-webkit-transform .2s;-o-transition:opacity .2s,-o-transform .2s;transition:opacity .2s,transform .2s;transition:opacity .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}devsite-header[billboard][bottom-row--hidden] .devsite-top-logo-row devsite-search .devsite-search-form{-webkit-transform:translateZ(0);transform:translateZ(0)}body[type=error] devsite-header .devsite-top-logo-row .devsite-search-form,devsite-header[billboard]:not([bottom-row--hidden]) .devsite-top-logo-row devsite-search .devsite-search-form{opacity:0;-webkit-transform:translate3d(200px,0,0);transform:translate3d(200px,0,0)}devsite-header[billboard][bottom-row--hidden] .devsite-header-billboard-search devsite-search{opacity:0}devsite-header[billboard] .devsite-header-billboard-search devsite-search{margin-left:0}[dir=rtl] devsite-header[billboard] .devsite-header-billboard-search devsite-search{margin-right:0}devsite-header[billboard] .devsite-header-billboard-search devsite-search .devsite-popout{max-height:-webkit-calc(100vh - 255px);max-height:calc(100vh - 255px)}@media screen and (max-width:840px){devsite-header .devsite-top-logo-row devsite-search{width:auto}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox{width:0}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox .devsite-search-image{display:none}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#5f6368;display:-webkit-box;display:-webkit-flex;display:flex;z-index:1}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-open]{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-close]{display:none}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-open]:before{content:"search"}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-close]:before{content:"cancel"}devsite-header .devsite-top-logo-row devsite-search .devsite-result-item a,devsite-header .devsite-top-logo-row devsite-search .devsite-result-label,devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-footer,devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-header{padding-left:60px;padding-right:8px}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-result-item a,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-result-label,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-footer,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-header{padding-left:8px;padding-right:60px}}devsite-header .devsite-header-upper-tabs{-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0;margin:0 0 0 48px;position:relative;z-index:8}[dir=rtl] devsite-header .devsite-header-upper-tabs{margin:0 48px 0 0}devsite-header devsite-tabs tab a:focus,devsite-header devsite-tabs tab a:hover{text-decoration:none}@media screen and (max-width:840px){devsite-header .devsite-header-upper-tabs{margin-left:0}[dir=rtl] devsite-header .devsite-header-upper-tabs{margin-right:0}devsite-header devsite-tabs.lower-tabs,devsite-header devsite-tabs.upper-tabs{display:none}}devsite-language-selector>devsite-select .devsite-select-toggle{color:#3c4043;max-width:124px;padding:0 31px 0 15px}devsite-progress{pointer-events:none;-webkit-transform-origin:50% 0;-o-transform-origin:50% 0;transform-origin:50% 0;-webkit-transform:scaleY(0);-o-transform:scaleY(0);transform:scaleY(0);-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;-o-transition:-o-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease,-o-transform .2s ease}devsite-progress[type=indeterminate]{-webkit-transform:scaleY(1);-o-transform:scaleY(1);transform:scaleY(1)}devsite-progress .devsite-progress--indeterminate{position:relative;height:2px}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-2,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-4{background:#fff;bottom:0;left:0;position:absolute;right:0;top:0;-webkit-transform-origin:0 0;-o-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1{-webkit-animation:progress-indeterminate-1 2.5s linear infinite;-o-animation:progress-indeterminate-1 2.5s linear infinite;animation:progress-indeterminate-1 2.5s linear infinite;z-index:1}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-2{-webkit-animation:progress-indeterminate-2 2.5s ease-in infinite;-o-animation:progress-indeterminate-2 2.5s ease-in infinite;animation:progress-indeterminate-2 2.5s ease-in infinite;z-index:2}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3{-webkit-animation:progress-indeterminate-3 2.5s ease-out infinite;-o-animation:progress-indeterminate-3 2.5s ease-out infinite;animation:progress-indeterminate-3 2.5s ease-out infinite;z-index:3}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-4{-webkit-animation:progress-indeterminate-4 2.5s ease-out infinite;-o-animation:progress-indeterminate-4 2.5s ease-out infinite;animation:progress-indeterminate-4 2.5s ease-out infinite;z-index:4}@-webkit-keyframes progress-indeterminate-1{0%{-webkit-transform:scaleX(0);transform:scaleX(0)}50%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-1{0%{-o-transform:scaleX(0);transform:scaleX(0)}50%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-1{0%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}50%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-2{0%,20%{-webkit-transform:scaleX(0);transform:scaleX(0)}70%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-2{0%,20%{-o-transform:scaleX(0);transform:scaleX(0)}70%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-2{0%,20%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}70%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-3{0%,60%{-webkit-transform:scaleX(0);transform:scaleX(0)}90%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-3{0%,60%{-o-transform:scaleX(0);transform:scaleX(0)}90%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-3{0%,60%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}90%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-4{0%,75%{-webkit-transform:scaleX(0);transform:scaleX(0)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-4{0%,75%{-o-transform:scaleX(0);transform:scaleX(0)}to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-4{0%,75%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}devsite-search{-webkit-border-radius:2px;border-radius:2px;display:inline-block;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0 6px 24px;overflow:hidden;position:relative;text-align:left;-webkit-transform:translateZ(0);transform:translateZ(0);vertical-align:top;width:200px;will-change:transition;z-index:10}[dir=rtl] devsite-search{margin:6px 24px 6px 0;text-align:right}body[pending] devsite-search{visibility:hidden!important}devsite-search .devsite-search-image{color:#5f6368;left:8px;position:absolute;top:6px;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}[dir=rtl] devsite-search .devsite-search-image{left:auto;right:8px}devsite-search .devsite-search-image:before{content:"search"}devsite-search .devsite-search-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex}devsite-search .devsite-suggest-results-container{border-top:1px solid #dadce0}devsite-search input.devsite-search-field{background:none;border:0;color:#5f6368;height:36px;outline:0;padding:8px 8px 8px 40px;-webkit-transition:background .2s,color .2s;-o-transition:background .2s,color .2s;transition:background .2s,color .2s;width:100%}[dir=rtl] devsite-search input.devsite-search-field{padding:8px 40px 8px 8px}devsite-search input.devsite-search-field::-ms-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::-webkit-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::-moz-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field:-ms-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field:focus{border:0;padding-bottom:8px}devsite-search .devsite-searchbox{width:100%}devsite-search .devsite-searchbox:before{background:#fff;content:"";height:500px;left:-6px;opacity:0;pointer-events:none;position:absolute;top:-458px;-webkit-transition:opacity 1ms .2s;-o-transition:opacity 1ms .2s;transition:opacity 1ms .2s;width:-webkit-calc(100% + 12px);width:calc(100% + 12px);will-change:opacity;z-index:-1}[dir=rtl] devsite-search .devsite-searchbox:before{left:auto;right:-6px}devsite-search[search-active]{overflow:visible}devsite-search[search-active] .devsite-searchbox:before{opacity:1}devsite-search[search-active] .devsite-searchbox:hover{background:#f1f3f4}devsite-search[search-active] .devsite-search-field{color:#202124}devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-image{color:#5f6368}devsite-search .devsite-popout{display:block;margin-top:6px;position:absolute;-webkit-transform:translateY(-100vh);-o-transform:translateY(-100vh);transform:translateY(-100vh);-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;visibility:hidden;width:100%;z-index:-2}devsite-search[search-active] .devsite-popout{display:block;-webkit-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0);-webkit-transition-delay:.2s;-o-transition-delay:.2s;transition-delay:.2s;visibility:visible;will-change:transform}devsite-search .devsite-popout-result{max-height:-webkit-calc(100vh - 56px);max-height:calc(100vh - 56px);overflow-y:auto;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-search .devsite-popout-result:empty,devsite-search[search-active][no-suggest] .devsite-popout{display:none}devsite-search .devsite-suggest-wrapper{padding:16px 0 0;font-size:14px}devsite-search .devsite-result-item,devsite-search .devsite-result-label{font:13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0}devsite-search .devsite-result-label{padding-left:40px}[dir=rtl] devsite-search .devsite-result-label{padding-left:0;padding-right:40px}devsite-search .devsite-result-item a{color:#202124;display:block;outline:0;padding:8px;text-decoration:none;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s;will-change:transition}[dir=ltr] devsite-search .devsite-result-item a{padding-left:40px}[dir=rtl] devsite-search .devsite-result-item a{padding-right:40px}devsite-search .devsite-result-item.highlight a,devsite-search .devsite-result-item a:focus,devsite-search .devsite-result-item a:hover{background:#f1f3f4}devsite-search .devsite-result-item b{font-weight:500}devsite-search .devsite-suggest-footer{border-top:1px solid #dadce0;margin:8px 0 0;padding:7px 0 8px 40px}[dir=rtl] devsite-search .devsite-suggest-footer{padding:7px 40px 8px 0}devsite-search .devsite-suggest-footer>.button{display:inline-block;margin:6px 0;max-width:-webkit-calc(100% - 16px);max-width:calc(100% - 16px)}[dir=ltr] devsite-search .devsite-suggest-footer>.button{margin-right:16px}[dir=rtl] devsite-search .devsite-suggest-footer>.button{margin-left:16px}devsite-search .devsite-suggest-footer>.button-white{max-width:100%}[dir=ltr] devsite-search .devsite-suggest-footer>.button-white{margin-left:-8px}[dir=rtl] devsite-search .devsite-suggest-footer>.button-white{margin-right:-8px}devsite-search .devsite-suggest-header{font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin:12px 0;padding-left:40px;text-transform:uppercase}[dir=rtl] devsite-search .devsite-suggest-header{padding-left:0;padding-right:40px}devsite-search hr+.devsite-suggest-header{margin-top:24px}devsite-search .devsite-suggest-header .devsite-suggest-project:before{content:"|";margin:0 8px}devsite-search hr{background:#ddd;margin:8px 0}devsite-search .devsite-suggestion-fragment+.devsite-suggestion-fragment:before{content:"|";margin:0 8px}devsite-search .devsite-search-disabled{padding-bottom:16px}devsite-search[compact]{width:auto}devsite-search[compact] input.devsite-search-field{width:0}devsite-search[compact] .devsite-search-image{left:-webkit-calc(50% - 12px);left:calc(50% - 12px);pointer-events:none}[dir=ltr] devsite-search[compact][search-active] .devsite-search-image{left:8px;right:auto}[dir=rtl] devsite-search[compact][search-active] .devsite-search-image{right:8px;left:auto}devsite-search[compact][search-active] input.devsite-search-field{width:100%}body[theme] devsite-search[compact] .devsite-search-field,body[theme] devsite-search[compact] .devsite-searchbox{background-color:transparent}@media screen and (max-width:840px){devsite-search input.devsite-search-field{padding-left:40px}[dir=rtl] devsite-search input.devsite-search-field{padding-left:0;padding-right:40px}.devsite-search-background,.devsite-search-background:after,[search-active] .devsite-search-background:after,devsite-search .devsite-search-field,devsite-search .devsite-search-field:hover{-webkit-transition:none;-o-transition:none;transition:none}devsite-search .devsite-search-image{left:8px}[dir=rtl] devsite-search .devsite-search-image{left:auto;right:8px}devsite-header devsite-search{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin-left:8px;overflow:visible}[dir=rtl] devsite-header devsite-search{margin-left:0;margin-right:8px}devsite-header devsite-search .devsite-search-form{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-header .devsite-top-logo-row devsite-search:not([search-active]) input.devsite-search-field{padding:0}}devsite-select{display:inline-block;position:relative}devsite-select+devsite-select{margin:0 0 0 16px}devsite-select select{display:none!important;pointer-events:none!important;position:absolute;z-index:-1}devsite-select .devsite-select{position:relative}devsite-select .devsite-select-toggle{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;padding:0 27px 0 7px;-moz-appearance:none;-webkit-appearance:none;background:#fff url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23212121'/></svg>") no-repeat 100%;-webkit-box-shadow:none;box-shadow:none;color:#202124;cursor:pointer;display:inline-block;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:36px;line-height:34px;max-width:256px;min-width:72px;outline:0;overflow:hidden;text-align:left;text-indent:.01px;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s;-o-transition:background-color .2s;transition:background-color .2s;vertical-align:middle;white-space:nowrap}devsite-select .devsite-select-toggle:focus,devsite-select .devsite-select-toggle:hover{background-color:#f1f3f4}devsite-select .devsite-select-toggle:active{background-color:#e8eaed}devsite-select .devsite-select-toggle:disabled{background:#f1f3f4 url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23bdbdbd'/></svg>") no-repeat 100%;border-color:transparent;color:#bdc1c6;cursor:default}devsite-select .devsite-select-list{background:#fff;border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;display:none;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);font:400 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-height:304px;opacity:0;outline:0;overflow-y:auto;padding:8px 0;pointer-events:none;position:absolute;-webkit-transition:opacity .2s,visibility .2s;-o-transition:opacity .2s,visibility .2s;transition:opacity .2s,visibility .2s;z-index:1015}devsite-select[menu--open] .devsite-select-list{display:block;pointer-events:auto}devsite-select[menu--show] .devsite-select-list{opacity:1}devsite-select[menu-position=above] .devsite-select-list{bottom:36px}devsite-select[menu-position=below] .devsite-select-list{top:36px}devsite-select .devsite-select-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0;min-height:48px;min-width:100%;padding:8px 16px;white-space:nowrap}devsite-select .devsite-select-item.devsite-focused,devsite-select .devsite-select-item:focus,devsite-select .devsite-select-item:hover{background-color:#f1f3f4;cursor:pointer}devsite-select .devsite-select-item[data-selected]{background-color:#f1f3f4;font-weight:500}devsite-select.devsite-select--multiple .devsite-select-item{padding-left:48px;position:relative}devsite-select.devsite-select--multiple .devsite-select-item:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;color:#80868b;content:"check_box_outline_blank";display:block;font-size:24px;left:16px;position:absolute;top:50%;-webkit-transform:translateY(-50%);-o-transform:translateY(-50%);transform:translateY(-50%)}devsite-select.devsite-select--multiple .devsite-select-item[data-selected]:before{color:#1976d2;content:"check_box"}@media screen and (max-width:600px){devsite-select{display:block}devsite-select+devsite-select{margin:16px 0 0}}devsite-sitemask{background:rgba(0,0,0,.4);bottom:-200px;cursor:pointer;left:-200px;opacity:0;pointer-events:none;position:fixed;right:-200px;top:-200px;-webkit-transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;-o-transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;visibility:hidden;z-index:1012;-webkit-tap-highlight-color:transparent}devsite-sitemask[visible]{opacity:1;pointer-events:auto;-webkit-transition:opacity .2s ease;-o-transition:opacity .2s ease;transition:opacity .2s ease;visibility:visible}devsite-tabs{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:48px;max-width:-webkit-calc(100% - 208px);max-width:calc(100% - 208px);position:relative}devsite-tabs[connected]{max-width:none}devsite-tabs .devsite-tabs-wrapper{bottom:0;display:-webkit-box;display:-webkit-flex;display:flex;left:0;overflow:hidden;position:absolute;right:0;top:0}devsite-tabs[no-overflow] .devsite-tabs-wrapper{overflow:auto}devsite-tabs[dropdown--open] .devsite-tabs-wrapper,devsite-tabs[overflow-menu--open] .devsite-tabs-wrapper{overflow:visible}devsite-tabs tab{-webkit-flex-shrink:0;flex-shrink:0;position:relative}devsite-tabs tab,devsite-tabs tab>a{display:-webkit-box;display:-webkit-flex;display:flex}devsite-tabs tab>a{-webkit-box-align:center;-webkit-align-items:center;align-items:center;font:500 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding:0 24px;text-transform:uppercase;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-tabs tab>a,devsite-tabs tab>a:focus,devsite-tabs tab>a:hover{text-decoration:none}devsite-tabs.upper-tabs tab a{font-weight:400;text-transform:none}devsite-tabs.upper-tabs tab[active]>a{font-weight:500}devsite-tabs tab[active] a:after,devsite-tabs tab a:focus:after,devsite-tabs tab a:hover:after{bottom:0;content:"";display:block;height:2px;left:0;position:absolute;right:0}devsite-tabs tab[dropdown]>a{padding:0 0 0 24px;position:relative;z-index:2}[dir=rtl] devsite-tabs tab[dropdown]>a{padding:0 24px 0 0}devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle{-webkit-box-align:center;-webkit-align-items:center;align-items:center;cursor:pointer;display:-webkit-box;display:-webkit-flex;display:flex}[dir=rtl] devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle,devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle{padding:0}devsite-tabs.upper-tabs .devsite-icon-arrow-drop-down:before,devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle:before{-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg);-webkit-transition:color .2s,-webkit-transform .2s;transition:color .2s,-webkit-transform .2s;-o-transition:color .2s,-o-transform .2s;transition:color .2s,transform .2s;transition:color .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}devsite-tabs.upper-tabs[overflow-menu--open] tab:hover .devsite-icon-arrow-drop-down:before,devsite-tabs tab[dropdown--open] .devsite-tabs-dropdown-toggle:before{-webkit-transform:rotate(-180deg);-o-transform:rotate(-180deg);transform:rotate(-180deg)}devsite-tabs tab[overflow-tab]{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative}devsite-tabs tab[overflow-tab][collapsed]{-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0}devsite-tabs.upper-tabs tab[overflow-tab]:after{content:"";height:48px;position:absolute;z-index:-1}[dir=ltr] devsite-tabs.upper-tabs tab[overflow-tab]:after{left:-6px;right:-100%}[dir=rtl] devsite-tabs.upper-tabs tab[overflow-tab]:after{left:-100%;right:-6px}devsite-tabs tab[overflow-tab] tab>a{padding:0 24px}devsite-tabs tab[overflow-tab] a{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse;position:relative}devsite-tabs tab[overflow-tab] tab .devsite-tabs-dropdown,devsite-tabs tab[overflow-tab] tab .devsite-tabs-dropdown-toggle{display:none}devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{background:#fff;-webkit-border-radius:2px;border-radius:2px;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;max-height:320px;overflow-y:auto;padding:16px 0;position:absolute;top:-16px;z-index:1005}devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);top:48px;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;z-index:-1}devsite-tabs.upper-tabs .devsite-tabs-overflow-menu .devsite-tabs-dropdown-toggle{display:none!important}devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu[hidden]{display:block!important;pointer-events:none;-webkit-transform:translate3d(0,-150%,0);transform:translate3d(0,-150%,0)}body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a{background:#fff;color:#5f6368;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:48px}body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a:focus,body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a:hover{background:#f1f3f4;color:#202124}devsite-tabs .devsite-tabs-dropdown{display:block;font-size:13px;left:-6px;min-width:-webkit-calc(100% + 12px);min-width:calc(100% + 12px);outline:0;overflow:hidden;padding:0 6px 6px;pointer-events:none;position:absolute;top:100%;z-index:-1}[dir=rtl] devsite-tabs .devsite-tabs-dropdown{right:-6px;left:auto}devsite-tabs [dropdown-full] .devsite-tabs-dropdown{left:0;padding:0 0 6px;width:100vw}devsite-tabs .devsite-tabs-dropdown-content{background-color:#fff;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);overflow:auto;max-height:600px;max-width:100vw;padding:0 12px;pointer-events:none;-webkit-transform:translate3d(0,-150%,0);transform:translate3d(0,-150%,0);-webkit-transition:-webkit-transform 0s;transition:-webkit-transform 0s;-o-transition:-o-transform 0s;transition:transform 0s;transition:transform 0s,-webkit-transform 0s,-o-transform 0s;white-space:nowrap}devsite-tabs .devsite-tabs-dropdown[dropdown-transition] .devsite-tabs-dropdown-content{-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;-o-transition:-o-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s,-o-transform .5s}devsite-tabs tab[dropdown--open] .devsite-tabs-dropdown-content{pointer-events:all;-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-tabs [dropdown-full] .devsite-tabs-dropdown-column{-webkit-box-flex:1;-webkit-flex:1;flex:1;min-width:0}devsite-tabs .devsite-tabs-dropdown-section{list-style:none;padding:0 12px}devsite-tabs .devsite-tabs-dropdown-section:first-child{margin-top:18px}devsite-tabs .devsite-tabs-dropdown-section:not(:first-child){margin-top:54px}devsite-tabs tab[dropdown] .devsite-nav-item,devsite-tabs tab[dropdown] .devsite-nav-title{line-height:18px;margin:0 0 18px}devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368;font-weight:700;padding:0}devsite-tabs [dropdown-full] .devsite-nav-item>a,devsite-tabs [dropdown-full] .devsite-nav-title{display:block;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:normal}devsite-tabs tab[dropdown] .devsite-nav-item-description{color:#5f6368;white-space:normal}body[theme] devsite-tabs .devsite-tabs-dropdown a,body[theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124;display:block;font-weight:400}body[theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}devsite-tabs[render-hidden]{width:100%}devsite-tabs[render-hidden] tab[overflow-tab],devsite-tabs tab[overflow-tab][render-hidden]{-webkit-box-flex:0;-webkit-flex:none;flex:none}devsite-tabs tab[dropdown] .devsite-tabs-close-button{color:#202124;cursor:pointer;position:absolute;right:24px;top:24px;visibility:hidden;z-index:1}devsite-tabs tab[dropdown] .devsite-tabs-close-button:focus,devsite-tabs tab[dropdown] .devsite-tabs-close-button:hover{color:#1a73e8}devsite-tabs tab[dropdown--open] .devsite-tabs-close-button{visibility:visible}devsite-toc.devsite-toc{float:right;width:160px}[dir=rtl] devsite-toc.devsite-toc{float:left}devsite-toc>.devsite-nav-list{border-left:4px solid #5f6368;width:160px}[dir=rtl] devsite-toc>.devsite-nav-list{border-left:0;border-right:4px solid #5f6368}devsite-toc[fixed]>.devsite-nav-list{contain:content;overflow-x:hidden;overflow-y:auto;position:fixed;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:max-height,transform}[dir=ltr] devsite-toc[fixed]>.devsite-nav-list{padding-right:8px}[dir=rtl] devsite-toc[fixed]>.devsite-nav-list{padding-left:8px}devsite-toc>.devsite-nav-list>:first-child>.devsite-nav-title{padding-top:0}devsite-toc>.devsite-nav-list>:last-child>.devsite-nav-list>:last-child>.devsite-nav-title:last-child,devsite-toc>.devsite-nav-list>:last-child>.devsite-nav-title:only-child{padding-bottom:0}devsite-toc.devsite-toc-embedded{display:none}devsite-toc.devsite-toc-embedded>.devsite-nav-list{width:auto}devsite-toc .devsite-nav-list{padding:0 0 0 12px}[dir=rtl] devsite-toc .devsite-nav-list{padding:0 12px 0 0}devsite-toc .devsite-nav-more-items,devsite-toc .devsite-nav-show-all{display:none}devsite-toc[expandable] .devsite-nav-more-items,devsite-toc[expandable] .devsite-nav-show-all{color:#5f6368;display:block;height:24px;padding:0}devsite-toc .devsite-nav-show-all{margin:-4px 0 0 4px;min-width:20px}devsite-toc .devsite-nav-show-all:before{content:"expand_more"}devsite-toc .devsite-nav-more-items{margin-bottom:-8px;min-width:0}devsite-toc .devsite-nav-more-items:before{content:"more_horiz"}devsite-toc[expanded] .devsite-nav-more-items:before,devsite-toc[expanded] .devsite-nav-show-all:before{content:"expand_less"}devsite-toc .devsite-toc-toggle{display:-webkit-box;display:-webkit-flex;display:flex;margin:0}devsite-toc .devsite-show-apix{margin-top:12px}@media screen and (max-width:1252px){devsite-toc.devsite-toc,devsite-toc[visible].devsite-toc{display:none}devsite-toc.devsite-toc-embedded:not(:empty){display:block;margin:20px 0 24px}body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 40px 24px}}@media screen and (max-width:840px){body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 24px 24px}}@media screen and (max-width:600px){body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 16px 24px}}@charset "UTF-8";devsite-user{display:block;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto}devsite-user:not(:empty){margin:0 -8px 0 0;min-width:60px}[dir=rtl] devsite-user:not(:empty){margin:0 0 0 -8px}devsite-user #devsite-signin-btn{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-user devsite-spinner{margin:4px 8px 4px 20px}devsite-user devsite-spinner.hide{opacity:0;-webkit-transition:opacity .45s ease;-o-transition:opacity .45s ease;transition:opacity .45s ease;-webkit-animation-delay:.45s;-o-animation-delay:.45s;animation-delay:.45s}devsite-user .ogb-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s cubic-bezier(.4,0,.2,1);-o-transition:opacity .2s cubic-bezier(.4,0,.2,1);transition:opacity .2s cubic-bezier(.4,0,.2,1)}devsite-user .ogb-pending{opacity:0}devsite-user .ogb-si{margin:0 0 0 16px}[dir=rtl] devsite-user .ogb-si{margin:0 16px 0 0}devsite-user .ogb-so{margin:0 0 0 12px}[dir=rtl] devsite-user .ogb-so{margin:0 12px 0 0}devsite-user .gb_Sb>.gb_Rb{-webkit-box-sizing:content-box;box-sizing:content-box}devsite-user button.devsite-user-change-account,devsite-user button.devsite-user-signout{height:auto;color:#3c4043}devsite-user button.devsite-user-change-account .material-icons,devsite-user button.devsite-user-signout .material-icons{margin:0;height:auto;width:auto;top:auto}devsite-user button.devsite-user-change-account{border:0}@media (-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-resolution:1.25dppx),not all{[dir=rtl] devsite-user .gb_xa:before{-webkit-transform-origin:right 0;-o-transform-origin:right 0;transform-origin:right 0}}devsite-user .devsite-user-dialog{display:none}devsite-user .devsite-user-dialog a:link,devsite-user .devsite-user-dialog a:visited{text-decoration:none}devsite-user[dialog--open] .devsite-user-dialog{background:#fff;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:8px;border-radius:8px;-webkit-box-shadow:0 2px 10px rgba(0,0,0,.2);box-shadow:0 2px 10px rgba(0,0,0,.2);color:#000;display:block;max-height:-webkit-calc(100vh - 86px);max-height:calc(100vh - 86px);outline:none;overflow:auto;position:absolute;right:24px;top:62px;width:354px}[dir=rtl] devsite-user[dialog--open] .devsite-user-dialog{left:24px;right:auto}devsite-user .devsite-user-dialog-photo,devsite-user .devsite-user-dialog-toggle,devsite-user .devsite-user-dialog-toggle .devsite-user-dialog-letter{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:0;overflow:hidden;padding:0}devsite-user .devsite-user-dialog-toggle{border:4px solid transparent;-webkit-box-sizing:content-box;box-sizing:content-box;height:32px;margin:0 4px 0 0;overflow:hidden;text-decoration:none;width:32px}[dir=rtl] devsite-user .devsite-user-dialog-toggle{margin:0 0 0 4px}devsite-user .devsite-user-dialog-toggle:focus{border-color:rgba(0,0,0,.2)!important}devsite-user .devsite-user-dialog-photo-thumbnail{height:32px;width:32px}devsite-user[js-signin] button{-webkit-box-shadow:none;box-shadow:none}devsite-user[js-signin] .devsite-user-dialog-toggle{opacity:0;-webkit-transition:opacity .45s ease;-o-transition:opacity .45s ease;transition:opacity .45s ease}devsite-user[js-signin] .devsite-user-dialog-toggle.show{opacity:1}devsite-user .devsite-user-dialog-toggle .devsite-user-dialog-letter{-webkit-box-flex:0;-webkit-flex:0 0 32px;flex:0 0 32px;font-size:17px;height:32px}devsite-user .devsite-user-dialog-learn-more{background-color:#e8f0fe;-webkit-border-radius:4px;border-radius:4px;color:#5f6368;font:12px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:4px 4px 0;padding:4px 29px;text-align:center}devsite-user .devsite-user-dialog-learn-more a,devsite-user .devsite-user-dialog-learn-more span{font-weight:500}devsite-user .devsite-user-dialog-learn-more a{color:#1a73e8}devsite-user .devsite-user-dialog-learn-more a:focus,devsite-user .devsite-user-dialog-learn-more a:hover{text-decoration:underline}devsite-user .devsite-user-dialog-user{padding:20px 33px 23px;text-align:center}devsite-user .devsite-user-dialog-photo{margin:0 auto 16px;position:relative;left:-2px}devsite-user .devsite-user-dialog-photo,devsite-user .devsite-user-dialog-photo-portrait{height:80px;width:80px}devsite-user .devsite-user-dialog-letter{text-transform:uppercase}devsite-user .devsite-user-dialog-photo .devsite-user-dialog-letter{font-size:52px}devsite-user .devsite-user-dialog-email,devsite-user .devsite-user-dialog-name{-o-text-overflow:ellipsis;text-overflow:ellipsis;overflow:hidden}devsite-user .devsite-user-dialog-name{color:#202124;font:500 16px/22px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.29px}devsite-user .devsite-user-dialog-email{color:#5f6368;font:400 14px/19px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-user .devsite-user-manage{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:0;border:1px solid #dadce0;-webkit-border-radius:17px;border-radius:17px;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;font:500 14px/20px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:34px;letter-spacing:.25px;margin:16px 0 0;padding:0 16px;white-space:nowrap}devsite-user .devsite-user-manage:link,devsite-user .devsite-user-manage:visited{color:#3c4043}devsite-user .devsite-user-manage:focus,devsite-user .devsite-user-manage:hover{background-color:#f8f9fa}devsite-user .devsite-user-manage:active{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background-color:#e8eaed;border-color:transparent}devsite-user .devsite-user-dialog-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}devsite-user .devsite-user-dialog .developer-profile:link,devsite-user .devsite-user-dialog .developer-profile:visited,devsite-user .devsite-user-dialog .devsite-user-developer-profile:link,devsite-user .devsite-user-dialog .devsite-user-developer-profile:visited,devsite-user .devsite-user-dialog .devsite-user-signin:link,devsite-user .devsite-user-dialog .devsite-user-signin:visited,devsite-user .devsite-user-signout:link,devsite-user .devsite-user-signout:visited{color:#3c4043}devsite-user .devsite-user-dialog .developer-profile:focus,devsite-user .devsite-user-dialog .developer-profile:hover,devsite-user .devsite-user-dialog .devsite-user-developer-profile:focus,devsite-user .devsite-user-dialog .devsite-user-developer-profile:hover,devsite-user .devsite-user-dialog .devsite-user-signin:focus,devsite-user .devsite-user-dialog .devsite-user-signin:hover,devsite-user .devsite-user-signout:focus,devsite-user .devsite-user-signout:hover{background-color:#f8f9fa}devsite-user .devsite-user-dialog .developer-profile:active,devsite-user .devsite-user-dialog .devsite-user-developer-profile:active,devsite-user .devsite-user-dialog .devsite-user-signin:active,devsite-user .devsite-user-signout:active{background-color:#e8eaed}devsite-user .devsite-user-dialog .devsite-user-developer-profile,devsite-user .devsite-user-dialog .devsite-user-signin{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font:500 14px/16px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.25px;padding:15px 39px 16px;width:100%}devsite-user .devsite-user-dialog-buttons>:first-child{border-top:1px solid #e8eaed}devsite-user .devsite-user-dialog .devsite-user-signin{border-bottom:1px solid #e8eaed}devsite-user .devsite-user-dialog .new-notification{background:#1967d2;-webkit-border-radius:10px;border-radius:10px;color:#fff;font-weight:700;font-size:12px;letter-spacing:.3px;padding:2px 8px}[dir=ltr] devsite-user .devsite-user-dialog .new-notification{margin-left:12px}[dir=ltr] devsite-user .devsite-user-signin .devsite-switch-account-icon,[dir=rtl] devsite-user .devsite-user-dialog .new-notification{margin-right:12px}[dir=rtl] devsite-user .devsite-user-signin .devsite-switch-account-icon{margin-left:12px}devsite-user .devsite-user-developer-profile .google-dev-icon{width:28px;position:relative}[dir=ltr] devsite-user .devsite-user-developer-profile .google-dev-icon{margin-left:-4px;margin-right:8px}[dir=rtl] devsite-user .devsite-user-developer-profile .google-dev-icon{margin-left:8px;margin-right:-4px}devsite-user .devsite-user-signout{border:1px solid #dadce0;-webkit-border-radius:4px;border-radius:4px;display:inline-block;font:500 14px/16px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.15px;margin:16px auto;padding:10px 24px}devsite-user .devsite-user-signout:active{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background-color:#e8eaed;border-color:transparent}devsite-user .devsite-user-dialog-footer{border-top:1px solid #e8eaed;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:14px 20px}devsite-user .devsite-user-dialog-footer-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;margin:0}devsite-user .devsite-user-dialog-footer-link:not(:first-child):before{color:#5f6368;content:"•";font-size:13px}devsite-user .devsite-user-dialog-footer-link>a{-webkit-border-radius:4px;border-radius:4px;display:inline-block;font:400 12px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:4px 8px}devsite-user .devsite-user-dialog-footer-link>a:link,devsite-user .devsite-user-dialog-footer-link>a:visited{color:#5f6368}devsite-user .devsite-user-dialog-footer-link>a:focus,devsite-user .devsite-user-dialog-footer-link>a:hover{background-color:#f8f9fa}devsite-user .devsite-user-dialog-footer-link>a:active{background-color:#e8eaed}@media screen and (max-width:840px){devsite-user .ogb-si{margin:0 0 0 4px}[dir=rtl] devsite-user .ogb-si{margin:0 4px 0 0}devsite-user[dialog--open] .devsite-user-dialog{right:16px}[dir=rtl] devsite-user[dialog--open] .devsite-user-dialog{left:16px;right:auto}}devsite-content-footer{clear:both;color:rgba(0,0,0,.65);display:block;font:13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}google-codelab-step{line-height:24px;display:block}google-codelab-step:focus{outline:none}google-codelab-step code,google-codelab-step pre{font-family:Source Code Pro,Helvetica,Arial;font-size:inherit;-webkit-border-radius:4px;border-radius:4px;overflow-x:auto;overflow-y:visible}google-codelab-step code{background-color:#e8eaed;padding:.1em .3em}google-codelab-step pre{display:block;color:#fff;background-color:#28323f;padding:14px;-webkit-text-size-adjust:none;line-height:1.4}google-codelab-step pre>code{padding:0;background-color:transparent}google-codelab-step code em{color:#97c8f2}google-codelab-step code .str,google-codelab-step pre .str{color:#34a853}google-codelab-step code .kwd,google-codelab-step pre .kwd{color:#f538a0}google-codelab-step code .com,google-codelab-step pre .com{color:#bdc1c6;font-style:italic}google-codelab-step code .typ,google-codelab-step pre .typ{color:#24c1e0}google-codelab-step code .lit,google-codelab-step pre .lit{color:#4285f4}google-codelab-step code .pln,google-codelab-step code .pun,google-codelab-step pre .pln,google-codelab-step pre .pun{color:#f8f9fa}google-codelab-step code .tag,google-codelab-step pre .tag{color:#24c1e0}google-codelab-step code .atn,google-codelab-step pre .atn{color:#eda912}google-codelab-step code .atv,google-codelab-step pre .atv{color:#34a853}google-codelab-step code .dec,google-codelab-step pre .dec{color:#5195ea}google-codelab-step paper-button{display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;min-width:5.14em;margin:0 .29em;background:transparent;-webkit-tap-highlight-color:transparent;font:inherit;text-transform:uppercase;outline-width:0;-webkit-border-radius:3px;border-radius:3px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;z-index:0;padding:.7em .57em;font-family:Roboto,Noto,sans-serif;-webkit-font-smoothing:antialiased;-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)}google-codelab-step h2.step-title{font-family:Google Sans,Arial,sans-serif!important;font-size:28px!important;font-weight:400!important;line-height:1em!important;margin:0 0 30px!important}google-codelab:not([theme=minimal]) google-codelab-step .instructions{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background:#fff;max-width:800px;font-size:14px;margin:0 auto 90px;-webkit-border-radius:4px;border-radius:4px}google-codelab-step .instructions .inner{padding:24px}google-codelab[theme=minimal] google-codelab-step .instructions .inner{padding:0 24px}@media (max-width:800px){google-codelab .instructions{margin:0 0 16px}}google-codelab:not([theme=minimal]) google-codelab-step .instructions a,google-codelab:not([theme=minimal]) google-codelab-step .instructions a:visited{color:#1a73e8}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2,google-codelab:not([theme=minimal]) google-codelab-step .instructions h3,google-codelab:not([theme=minimal]) google-codelab-step .instructions h4{font-weight:400;margin:0}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2{font-weight:300;line-height:1em;font-size:22px}google-codelab:not([theme=minimal]) google-codelab-step .instructions{line-height:24px}google-codelab:not([theme=minimal]) google-codelab-step .instructions li{margin:.5em 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2{font-weight:500;margin:20px 0 0;font-size:20px}google-codelab:not([theme=minimal]) google-codelab-step .instructions h3{font-weight:500;margin:20px 0 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside{padding:.5em 1em;margin:2em 0;border-left:4px solid;-webkit-border-radius:4px;border-radius:4px}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside p{margin:.5em 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.note,google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.notice{border-color:#ea8600;background:#fef7e0;color:#212124}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.special,google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.tip{border-color:#137333;background:#e6f4ea;color:#212124}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.warning{border-color:#ea8600;background:#fef7e0;color:#212124}google-codelab-step .instructions aside.callout{background-color:#e8f0fe;margin:20px 0;padding:15px;border-left:3px solid #185abc;-webkit-border-radius:4px;border-radius:4px;color:#212124;font-size:14px;line-height:1.5}google-codelab-step aside.callout b{color:#185abc}google-codelab-step .instructions ul.checklist{list-style:none;padding:0 0 0 1em}google-codelab-step .instructions ::content ul.checklist li,google-codelab-step .instructions ul.checklist li{padding-left:24px;-o-background-size:20px;background-size:20px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAWlBMVEUAAAAxokwwoks1pFAxokwxokwxokwxokwxokwnnkQnnkQnnkRou3y84cTS69cxokwonkQxokwnnkRqvH1VsmtluXlVsmsnnkRdtnLw+PIxokwqn0YinEAfmj3goh/UAAAAGnRSTlMA2CcEo+6AQT7+2IOBJxPl27alhoBnX15SCCe258UAAAB+SURBVEjH7dA5EoAgEERR3BcQ923Q+1/T0SqKlNbMouP3gxkRFvZpyQb64VSQT4mOcYc8mU5DnqIG8zXoozj4d34tML+YrET8XBFx4e2F4oAL4N7J3EUB/EfSUwD/zG3hvFdROu9XtL31vgXguQA9F6DnAvM8WbOHpkXYD3cBBCcPjtASYjwAAAAASUVORK5CYII=")}google-codelab-step .instructions h2 code,google-codelab-step .instructions table code{background:#fff}google-codelab-step .instructions .indented{margin-left:40px}google-codelab-step .instructions strong{font-weight:600}google-codelab-step .instructions :link paper-button{text-decoration:none!important}google-codelab-step .instructions paper-button{display:inline-block;-webkit-border-radius:4px;border-radius:4px;color:#fff;font-family:Google Sans,Arial,sans-serif;font-size:14px;font-weight:600;letter-spacing:.6px;padding:6px 16px 6px 12px;text-transform:none}google-codelab-step .instructions paper-button a{text-decoration:none;color:inherit!important}google-codelab-step a paper-button{display:inline-block}google-codelab-step .instructions paper-button.colored{background-color:#1e8e3e}google-codelab-step .instructions paper-button.red{background-color:#d93025}google-codelab-step .instructions iron-icon{vertical-align:sub;margin-right:7px;margin-left:3px;font-size:16px;top:-1px;position:relative}google-codelab-step .instructions img{max-width:100%;vertical-align:bottom}google-codelab-step .instructions .image-container{text-align:center}google-codelab-step .instructions table{border-spacing:0}google-codelab-step .instructions td{vertical-align:top;border-bottom:1px solid #ccc;padding:8px}google-codelab-step .instructions table p{margin:0}google-codelab:not([theme=minimal]) .instructions h3.faq{border-bottom:1px solid #ddd}google-codelab:not([theme=minimal]) .instructions ul.faq{list-style:none;padding-left:1em}google-codelab:not([theme=minimal]) .instructions .faq li{font-size:1.1em;margin-bottom:.8em}google-codelab:not([theme=minimal]) .instructions .faq a{color:inherit;text-decoration:none}google-codelab:not([theme=minimal]) .instructions .faq a:hover{text-decoration:underline}google-codelab-step .instructions .faq a[href*=cloud\.google\.com]{padding-left:22px;-o-background-size:20px;background-size:20px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC9FBMVEX////u7u7v7+/ZRDf/zEH/zkPbRTlJifT/z0XYRDhRj/XWRDdPjvVOjfX/zkH/z0fXQzdGiPXUQzdMi/VKi/Xr6+3t7e1SkPXbRDdLjPW70fPaQjVOjPXx8PD9/f1nnff19fXRQzdHifVJhOxEh/Tu7/JLifH9y0DUQjVtofhOi/NKh+9HgelGf+b+7saaWXX/zDz+yTrZRzrRQjT7+/vxx8PeqkTsuj32uzrOQzdqn/j39/fr6Oj9yT/3vjvaQDPUPTBJivVKifPq7O/z0Mz968T4wj7zwD3NPTHGPDD0+P9Bifrv8/Xx8vJJhu5EfOPo3Nv/0Ez8y0rxvT3ZPjBDi/tBhPT55ePkysj+3Hr/0VLHVErcpkPcTUH7xD35wTzztznpsTjKRTjo8P3U4/z6+vr//PmQt/mBrff//fayy/ZUkfZjmfSXufP78/L18+//+Oj66Of+9eBCdt3t59f/89TvxcPsurXasKztqKL+4Y7jjIT0033ShH3hc2ngaV7/1F3/0ljjv1fbWU37xkDaoEDCSj/xujvptjvutjrxtjn8xDflrDb5vjXJQjX2tzP4+v+bwf3I3Pyiw/t0pvhel/VZlPW90fP/+/DV3/Cdu+/e4uv33Nk+btP+8Mz+8Mniwb7+7L3u371Vdrv+6bDusq3+56j+5aTx2Z+Gjpj+4pbolI3RjYehnIT+2XPedW370mbHaGDMZl3bY1jQVUvbU0f/00PYTkLKSj/tsjjwpjfeXjfnfjbEQjbXPjTSPS/w9vu50vs7hfr5+flypPf89vb89fTF1fHM2vBsnfB3ou/67+6swOpekert6uNdi+M5deLq4OA/dt83bt80a9xmjdby19VKc8jx5cf12cP/7r/gwr+pnr/v4b7xwLtffbf+6rHx3KnorKjYqKTXpqHXo57aop3ZkovTwIboh3+jX3f30XDgenCWUm3Kc2zIsGnMcGj4y1zgYFXgX1TfrEXWlz7mhjfsrTbkljbabDbcVTbsmTBDSJ02AAAFFklEQVRYw+2WV1jTUABGkyZpi6W0tKUWC0hBsAporbMWrCgqKA6WCooK7r333nvvvffee++99957b33x3uTetIEGnnzx83y89Tunf25SWuI/f4Oqk6cU4ijLkr9sfp5Ckyfk6h8o5JkH4umpVPr5+fnqdLro6GiZTBYkk3l71z5SNbdA8zyt8wLYhKefUunr66vV6WQsMDExF3/StLytka/0VPopfbUgoEWBoCBv76lNcj6AFnnQ+6MB0BcsKNA8x2M4iPbjAThQgVsAA545TWg8RQlc5MMBflrg8wHgg3NsIX6OAa10Lr6SDQgHgAXetSeJD5iqRTrytfAKdHwgiAu0EZ3QvEBhYLLAu6+LDoIOtCApKSnVUqoBDondyiahoTggazt9ej4hd94Wxbya4z5wskZoYVgA0/NXlGSlVr3iJpOpOMsbt36r4BphsACQHSezBfSP4zQQs8Zs3n7P3QmeDQ8OAwVwdkEt0ADhhI3jFCxms7lfu+yBthHhwcFhoaCgnXZC4obqPYbZPABqgHVpNj/yak1QABdRQatriwcIiXy312alrBRFqff175jFb9lrZecIVGgz09+NTurjbw/LNFmtUqnVw8PRM0AY6JowaHFN7iIKz2rvtCIjfbBPkvF992aapABKakzvKByQymxbO5e7iDb8BfjoA5uWsPv7kJxPSi4Oy7RpgA/+kl8KArdolUq1uDMshLXCA8jD6NXqpEQPAmDCszibTSFlccS4+DMGMCrVtvdzO0eEh59CPulfgsDYoQ/Rb4q1mbiCsX8zZ2AVrQLIV16uGRGMTxD6PIEkh2TBOPA4og09nSeYJoe+fND1SxdmSRCBgg+6PypE9o01mTRq6Bs+1yEQqcCHJK69cn62xDnA3YS6W4qD51kBA9LV6MX5aXKuIB90/wyJnztCQFMJKuif2zQANfANn9CnchlYzyWYF7OFV8ATgAPV624BAzQKCgTSSxIsDxIYOQR00m7kFgjpsQP6CgUlNWxFd7LDAEaOoPvoc74ESa1NsUBXK9QelLHfafTqclZmKwXvsjpJihyi/mkc9j3Su/P7BngxeMJHPXoru9vbCP4vYZ9aT/DMpxkAW7CsCCFZ3D5I5BowAPnp1wgnqRaGQ84kduEKemchwC5BJ1jX6Tt6Ey50SvBiMH1AgCvYA1i9BP8UkRtjsW8c3E5wjcstfCBxUQgW/APt9sBAfxIR/2ScBvnS5O6EgA4baL7w+hzpRELyhNSqZ1Jgf/0xQsgSuXPCQzxBSPwaG/YpawyRhRnrvBh0DvSGLkITn2A9M/Kljt5HiawsTGAwxVbohS7+JGPfOBgNEJDK3wg60d2EBTvM2KdWE27opPLCWPqEZP9W+BCLfIoa3JFwxyoLX4haVNFHSMUecQrsGx8RbmmZxgfoIV9KIeoDhg4d+jXTzPubCREWWmhOB0SV5hgxYkRGxpgxY36i+2cEgRixQLN1FpqnYJWkKklJScNLjxy5MyPjhwf0Kegn9yZEmZdAuxQqVwEkDR8+auTO77uhD3XKsbWOeKBZL4ugULlyuXJlyjQaNeqbFfhG6BuSwdeqOF0H0q6FBmyhUaNdu6FPQd/RHw4Qpxfj4heMatCwIZywh1JTLAYD+mkhSoeBLoGoqCENYGDXL943oB834iyhLdjHgfF79mHfsb0bkRvLBhZDjB49euzYsePH/96/vzyHeXN3Inc6zSuCqcRys1tJjm4xdYj//JP8AXE5S/JuAn7MAAAAAElFTkSuQmCC")}google-codelab-step .instructions .faq a[href*=stackoverflow\.com]{padding-left:22px;-o-background-size:24px;background-size:24px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAATwAAAE8CAMAAABq2/00AAAA4VBMVEUAAACCg4aCg4aCg4bIj0P1fx/2hh+ojHCojHCojHD2ih/UjCn0eiD0eiD2hB/2hR/RjjDUjCnKkkPUjCnUjCn2ih/UjCnCllPCllP2ih/CllPCllPCllP2ih/CllPCllPCllP2ih/2ih/CllPCllP2ih/2ih/CllPUjCnCllPUjCn0eiD0eiD2ih+ojHD0eiDUjCn0eiD0eiCojHDUjCn0eiDUjCn0eiDUjCnUjCn0eiCojHD0eiCojHD2ih+ojHD2ih+ojHCojHCdiXaViHuCg4aojHD0eiDCllPUjCn2ih8XYwy7AAAARXRSTlMAv0CAEEAQv4BA7++/gCBQQN8wIIDPv2Dfv5+PUDDPryCAcO+/r49wn4Bg79/frJ9QcGDPr6+Pj3DPz2AwIJ9wYOaPVDAbIL/gAAAGhElEQVR42uzbsYrCQBDG8WmyxaJJlcIixQmxMSIRgiBB7Bbm/R/oPAnH3e0SxwSvyf/3DLt8zDeMAAAAAAAAAAAAAAAAAAAAAAAAAAAA4F9k7Xm1Xwlek53WqyY87AVWfbHumvCTE5hkIZYJTFyIrQU2+xA5CmxWIULcWq1DTGBzCrFcMDluC4ENcTtDEyKdwKYLkYuAuH27ggFtuj7ETgLiNo0B7Z3c1YnNMUQaWbTC6/W1uKUPHbha7wr60GnP7ot3YpGHWCsL5Wod1JP70LMsU+H12424nfDsBj4XgzMD2kN20F82YtDSh965nf61JW7Nzy7iKwY067OLlfJcw/qx1KQPQ9zSh1aaVtGHGnxoUummrB97WZhSk3byRM/6UST3mpQRtwZbTTo4GXdhQLvbaNJVxnX0oSMftyBuDQpN8o4BzaDWpFrG5KwfH5zXpJuMCfShox83pw812GnSxrx+XPI5hjvowF7ttawfB5km+Yq4nf5xS84x5lR7nGPMqfaI2znVHgPajGqPc4w51R7nGJ/sm29P2zAQh49h1kJgJHGWlrbQItAIebNO67owadpgwOzv/4Wm8S8NLsXxGSTu/HyEk/O78+McRu0FH4pQe2EdA6P2wjoGRu2FdotQe2EdA6P2wjoGRu2FdotQe+GChlF7DNcxKulN7bFbx4h1lPlSe+x8aKK1Tqae1B6zdYxM35BLP2qP1TqGjPQdmfCi9ji125l+ICp9qD1G6xixXiSPPag9PusYiW5SCLza49JuK/2YKEOrPSYXNBFpkyTGqj0e6xiFXkoukWqPwzpGrJ8iEyi1x8GHnuknSUqU2qO/jlHpVeQpwhB8oL6OISK9mkK4qz3q7bbQzxFV7mqP9jpGqi1IYme1R3sdI060BTPpaAiot9ss0hZkwk3tUb+gyUJbkEzd1B55HxqfaQvy1Ent0V/HKCNtwVy0NwSfGPhQMdcWRJWD2uOwjiFzbcFZ3F7tsVjHmDqMLTZqj3S7bTm2RJloZwh2maxjyJnd2NJS7XFZx7AcW2QrtUe93dZUDmOLjdrbaTTa7e9fNgj1i7ZjS9lK7dU+dPvzzgaRZ8elpPZji73a2/j749vOLuWytRtbCmmv9jghLMcWuw936zfwwnZssTAEexw+VidTmstn1N4Wr2/2gcrBlO6HY3eHKNqPLYfh2FmPLeZ/aXvh2D1Q2o0tYkHt8ZxQliIybTm2NNXex30IWI8tcePDPYSAywPv+61w7NwfeHfJHrvJS44tSQmUGan+oCfAgTi3fOCly1D952A4eqkH3gTocqDuOT5NX2JsmQJZhFrkaHwy8fzAmwNdeuoRRgQiTakEugyUSR2B+LFlDoTpK4M6AvH/pUUC6JIqAyMCMQ+8pMe8E2XgGIFlxKxbAIyVgUUEWj/wUh6QAZQBIgJlzqlbQKwMUBE4Tdh0C4ChMsBFoMgiJt2ivpvVoCNQznh0CxDKAB2B9dgSA2l6ygAbgfUDbwG0GSgDPxEo5tS7xYq7GT4CU8Im6pbRsNkxMBFIex5ezqQ3qM8fPgL5kZ6Mj1QNLgKpR90y4tNjpbxFID+ExwjsAUN8ReAYmOIjAlmePF8RyLHv+orAPgScI3AIb531tQZ/XjECR3DL5VoTeDOsXTVYf8UIFHDLu6sm8GZAFA8ZgccQimcgLCPwNBRvOROLCExD8Z4mXR2BRxCK5+wCx6F47hHYC8Vzj8BJKJ5zBPYhFM85Agf0i3dx3ulcdLubm5e+I3BEv3id65r7Ov70EoGCV/EW+NrpdLu/cHWkX7zz65pn6hiK9689O8hJKIaiMCxK0nYzTBgSYxDs/pekiTohEq/v0eRKv28J/+zkXMbrF2IdxQvEC3ScOF7r6+3LrtZDa1vxVinfHaeId+qjTBCv9kGO4i1XJoi365/EWxCv9EF24i1XJ4jXv4iXKd7p/uNt+yjt/uO1PspZvOVmmGcPrZ1q3ZUi3qoDaNva4WYdj5PFu2nHMm+8nzqKF4y3vuOreL923PcrqngRz6291FrKXrxbdWzifXB6iydehHh5iDco3maQpxnivQ2yEU888cQLEi8R8cSLES8R8cSLES8R8cSLES8R8cSLES8R8QbFexzkPEO86xxA4on3F+LlIZ54QeLlIZ54QeLlIZ54QeLlId6qeOk8/BviiRciXibiiRciXibiiRciXibiiRciXibiiQcAAAAAAAAAAAAAAAAAwHTeAe21evvWi2VXAAAAAElFTkSuQmCC")}google-codelab-step .instructions .faq a[href*=support\.google\.com\/webmasters\/]{padding-left:24px;-o-background-size:24px;background-size:24px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAolBMVEUAAADW1tbW1tZ6enrQ0dJ6enrMzMzq6+zq6+x6enru7+/m5+ju7+/m5+j////S09REi/XW1tbm5+ju7+9PT0/Q0dK8vLxGjfVZWVlNTU16enrMzMzIyMj09PS/0uuSkpJpaWmbm5tim/e0tLSEhIStx+3e3+GsrKxwcHBZk/dPk/R1p/KPtu/U3eqmpqZlZWVfmO5woeqHreWvwN6nu9qLi4vZSE73AAAADnRSTlMAEcxmzO7MzDMz7u6IiHn/rpYAAALMSURBVHja7dXpjtowFIZhUkqZlSV1ncQdEsK+zD7T+7+1xjTiwxVOAmeORyP5+2UspPchEtDy8/Pz8/Pz8/M7cZeduHKdyxbL0FfVAMUraHdUHUB12i22tZWqBRSDgKFfC4CApV8PgIClXw/gE6hyTd/lAR7gAV8fEHwbWtc/ul/WfQ8IfQKAIECfCoCA3sdq+nQB+nQABIQ+CQABtY9Z+lQB+gwACBj6AFAE6DM9AL0fAalPB/yGgNAfntwHAAJKnwCAwNLneQAAVAvM/ttzVLlB4z2/A6AFVoDRf43IfewVgGKNAG/Uvrl39BsCXqh9cy8nA/51tmk63+jDZp6m9wTA4DzANiyW6lOqTxAMHAD22XASReuwpKDvELCNookBGJwx9E8B3OvsQp8e9GmOviNANA/Dh4k+TApBukHfFSBa7/I7whqf3xWA/gOAoU8DDJwBCHk+wIAw9CsAPw/W++CFB/MAD/hCgL59PXPS8toRYKHEndkTauEQIIUQ0hToG2eAXOicKdBXmSPAo9CTpkDo5U4Ad6IEGILy0gFgFJctCACIh/yATABwIChvVyE3IBR7AAQAiOmIGZADAMEBQMkRL+AJAAgOALGUI07AQgBgCPb3uRbwAaYAGAIAZlIL2AA5AIYAgJXUAjaAAsAQADCWO4EjAAQGQAvYAUsDsDQAeuwAMZvu89PZ/jZhBmQCi2dl60+MPiMAf8UgZEUpK/LoJ4oX0C9LIIyF0U8yXkBPVCzRWzIDZjX9sWQG9Kv7ScYN6KnKfizZAf2qfpLzA3p5RV9JboDeytp/kk4Ao5WtP+UFQKCO9sdTSQQ03+OR/kxKdgAWZv/11VI6AWBmP5HSNQD5zwQknwxIqIBu468hBgDqNYCuFXBDASSNAbdWQHB1PiBpDLgOWtZdXPEDri/QswjYAOjbF9x0OQHd26Dl5+fn5+fn5+dn7i/3LEaKJNV/0wAAAABJRU5ErkJggg==")}google-codelab-step .instructions .faq a[href*=android-developer],google-codelab-step .instructions .faq a[href*=developer\.android\.com]{padding-left:20px;background-repeat:no-repeat;-o-background-size:20px;background-size:20px;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAq1BMVEX///+lyjmfxyGhyCqkyjakyTP+/vuiyS7z+OWlyjKgxyTX57ChyCfa6bTx9uLy9+Sz0l6szk/D24Tt9NrN4ZnK4JSiyCyexh34+/D6/PTp8dPh7cjf7L7k78vc6rnW5qzU5ajS5KPP4p+21GWnzD3F3Yi813Kx0ViqzUWexhr8/vjv9d7Z6LKy0lyZxADh7cLA2n2rzkr1+erJ35C61m6uz1KcxRHn8M6/2XgEePWtAAACr0lEQVRo3u3Z2XKiQBiG4Y9ebNO2aCCyzOC+7zH7/V/ZRJZQZaJpWnKS4TlJUItXbX4pFJXK71DTusvccI6z5oMSAutdhDP2uzpK4CxwxsRBGYJdA19qLAOU4s4LkyUN7G4Ude2glmzuRihHKDqAPZhNuJRKSckms37rvctrKMnG728l5cxKMU7JuCF7KEvf87h1gnu0iXK4B2p9Sc3dUgZNMusMLvu42r20LiBXT8JMWRepN1xlGu+fK8/6xFPxwsvH697/eP/j7uDTOvB+dxwX/AaM2SQ+6icrYHDyGmgfWE3irNeCqTaPAwcArydroboAbuMAb8NQRNJn20SYtHK8HaKZzoffhZl5tlNxsD5PsnUQ2b9jwwnO35WvRo3lN/o2TDwKS5M3gokF0w2wZxgIlpY2FpgsgdQPkLXJQar0A/LVIDB6Idp2GxQQrJI/rbq2Voijlc5S9LZs8uYCqBVyXLTZhLW/fSn3hDMmZAQ05I020gT2PmeM+w4uGpLksFs+oEEtbaqJVnpUkz4uyebf6xQK0CY6XjoSC1xQ9/PPr4KB52zuvTrOa5EssC0auM0C0tYKtI0DqgpUgSpQBapAFfifAj90Tn5YZoGnooH8eutB56KDDosGhlTrYiTyk0epVdFAKJLn9rLHRSNfcE4tF0UDcAXlXPhDfGPtjGejEMUDCEdPY8eFHoOADvPAxiDQLBBQkUHAJfoB34UBS/9CfAETe6lZYKZft/QIFe94vicRy7tcvKOkB0PhYOo4zlNWYNvjpjOdZwU+O24OQlynK62EGCF2J9IbpIsy/P0I3CHW+Qj8qQJVoAr8VOCx3EB+YvDSwIDmXzaW4oZZseU6Lfr5aaAUPT8uyPuT33VeIpRkI5RHyRQfHEKFUj2UJuh1GjaQsxudKECl8jv8A6GtQkKSkMLrAAAAAElFTkSuQmCC")}google-codelab-step .instructions h3>a[href*=github],google-codelab-step .instructions h3>a[href*=github]:visited{color:#000;text-decoration:none;padding-left:24px;background-repeat:no-repeat;-o-background-size:18px;background-size:18px;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAMAAAAOusbgAAAAflBMVEUAAACXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZbf2s+YAAAAKXRSTlMA+SAC7QrIBPXROWMX17rCSmusXEaAD9zNeXFWJqOLMx3mtJJAK5xP4Jw4LyAAAAT6SURBVGje1NbdcqJAEAXg0zJAQEFA0QhojJHE8/4vuBfrJlvQGHSGbO13CzVT09M/g//HIssPRZAYIUkxSVAc8myBSfnrMjVUmbRc+5jEsUqFN0lazeDYogo4SlC5jPq6EI4mxRpOhKuId4pWof228y0fsJ1bbp0lfFCS4XFNSgtpg8eEO6EV2YV4QBvRWtTiXl4udEByD3fxCzpS+LhDk9CZpMFosaFDJsZItdApqTHKnM7NMULOCeQW5534zDUnUuOmWDgRiXFDYzgZ02CQn3BCiY8BXsFJFd7YQjLPkeGDTPRsRhZVK+zIABxX+yfe6Wm/OgL4YIe0UIQRO8x1jm/yiHeI8s11RdP7FKJvx649PmUBRwoyfNqza6dUktzsNt7HliNsa+9m95UGXSl7XvQn2OV0qOpzO1tsNotZe66rw+lyXbb08bcX9qToyNgjYTcoS3PaxT4UflydzLJ7nFDYk3V+SdhzQZfnQTP89VW5jPDbmRTAWvDdnAq1zFnC2pLfHHnFn9uYK3yJ+HOhZoRPa2peYe2VmjX+KKjawNKGqgJXC6HqDEtnqmSB3yrqSlgqqau+UkAVwVLEKz1tZxxQw1LNATObSNvHOqUqCGEtDKhKAcAXqlo40Ag14g92D77BiTcO9pByqNbsDfeIcvCK3+HI++AlG2piOBJTY4AjNVs4s6XmiMwitSzSK0Nu0bQs2leOAzUzODOj5qDPYvHgTCj6TA703HLoog+oxGIgWgzHBMbigWnx1DSQyTcOqBDw34Sav9q1sx1HYSAKoNcYE4cl0AlhIB22rF3//4PTI43ULbrMYjzKy5zHROiGyNhlyqAXDC5z8Acc6mcFu3+OpeCD+Y87ONMRR1isik6qet+wah3gzMEwfIPXLIsB9sQp4UxJnL2pDFRwRBHrauoEZHDkaeoV5PSKKpNydMTSCk4oTawO8O07Nva9JN+8Z0skHNgk5l3bkf7hLUcjW+CceH2B1YqeeDmzTXU5sB/EE4rZmDucsA9ksBtvJuoQq4SaDE5sqc8n2+eaNyqBOTmHtcycG8xo3B4lrMgjmZ0GrwtYaQsLbUBmImZeojKuHRbyHjRmz5ZFQRUdByfGxCPEfPJW07gbVyVoiU9xIwYz97GVs1LD5oMmbPknvcKntLmXNNDvT2GBEV1WXXyadjA0Q64bIMygLsTo00aCsbmWfCbfDOEnt6QF8EzjlBi6BasVdscU5HYwbdz9LO65y9ae4tjKwTzDTJUZfxlPJjRLhoELfdPHgNrl9ZIFK6I5LhjyxOB75QdFsqAEVIKmCW+qcZ0B9xvOPft7eReaVnFPRPmj1lPvbcJdx6toUrkB46zpmyeAnFLViMGDZvSkKfo8Y3ikAORjB8RVqonIT4+hxJjW/qRRzezNvd1VoegKoG1uGOPRhBomaktfAok/PJ9a3PykQEk1xsQ0bqtg5Pk/+04qBtryotDlG4wpaJTvzS7Q3sCwDdbhgu1sw9yi7V/9XFSEb3MwbAbXYWn5H7zF3+5J2gZHNhuPX5frsaqaOvWpsAw+rC3FY6tg/cRMYU88zya4DzGbV7oL3npYQNWugmuFZSLtIlhHWOwcrA8OzrAgT3pdsD5J2PH2a4L3HuzdStvgMscq8j2hL/e5wcm7BLA2OuBbFebVKWBirYR/D6OWc16JizqEO0WUCrHzMMrbCZFGBf5j/QYa/td1VlNoIAAAAABJRU5ErkJggg==")}google-codelab-step .embedded-iframe,google-codelab-step .youtube-video{display:-webkit-box;display:-webkit-flex;display:flex;margin:auto;width:560px;height:315px;border:none;max-width:100%;max-height:51vw}google-codelab google-codelab-step .note:before,google-codelab google-codelab-step .special:before,google-codelab google-codelab-step aside:before{content:unset!important}google-codelab google-codelab-step .instructions aside.special,google-codelab google-codelab-step .instructions aside.warning{margin:10px 0!important;padding:15px 20px!important}iron-icon{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;font-size:inherit}iron-icon[icon=file-download]:after{content:"cloud_download"}google-codelab .warning,google-codelab .warning :link,google-codelab .warning :visited,google-codelab .warning code{background:#fef7e0}google-codelab .special :link,google-codelab .special :visited,google-codelab .special code,google-codelab aside :link,google-codelab aside :visited,google-codelab aside code{background:#e6f4ea}google-codelab-step td{background:transparent}google-codelab-step .instructions h3>a[href*=github],google-codelab-step .instructions h3>a[href*=github]:visited{background-position:0 3px}google-codelab-step code .pln,google-codelab-step code .pun,google-codelab-step pre .pln,google-codelab-step pre .pun{color:inherit}google-codelab-step code{font:500 90%/1 Roboto Mono,monospace}google-codelab-step pre{background:#f1f3f4;-webkit-border-radius:0;border-radius:0;color:inherit;margin:16px 0;overflow-x:auto;padding:8px 80px 8px 8px;position:relative}google-codelab-step pre,google-codelab-step pre code{font:14px/20px Roboto Mono,monospace}.button-blue,.button-green,.button-primary,.button-red,body devsite-footer-utility .devsite-footer-utility-button>a{color:#fff!important}google-codelab{width:100%;height:100%;padding-top:64px}google-codelab,google-codelab #main{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #main{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative;background:#f8f9fa}google-codelab #codelab-title{position:fixed;top:0;left:0;width:100%;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);color:#3c4043;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;height:64px;padding:0 36px 0 16px;-webkit-font-smoothing:antialiased;z-index:1000}google-codelab #codelab-title h1{font-size:20px;font-weight:400;margin:0 8px;font-family:Roboto,Noto,sans-serif;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-shrink:1;flex-shrink:1;white-space:nowrap;-o-text-overflow:ellipsis;text-overflow:ellipsis;overflow:hidden;width:0;display:inline-block}google-codelab #codelab-title .time-remaining{-webkit-flex-shrink:0;flex-shrink:0;-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;font-size:16px;font-weight:400;white-space:nowrap}google-codelab #codelab-title .time-remaining i{margin-right:3px}google-codelab #codelab-nav-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0;-webkit-flex-shrink:0;flex-shrink:0}google-codelab #codelab-nav-buttons #arrow-back,google-codelab #codelab-nav-buttons #menu{text-decoration:none;color:#3c4043;width:40px;height:40px;-webkit-box-align:center;-webkit-align-items:center;align-items:center}google-codelab #codelab-nav-buttons #arrow-back,google-codelab #codelab-nav-buttons #menu,google-codelab #controls{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}google-codelab #controls{position:absolute;bottom:32px;left:0;right:0;padding:0 32px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;z-index:1001}google-codelab #fabs{display:-webkit-box;display:-webkit-flex;display:flex;max-width:1025px;width:100%;margin:0 auto}google-codelab #fabs,google-codelab #fabs .spacer{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}#done,#next-step,#previous-step{-webkit-border-radius:4px;border-radius:4px;font-family:Google Sans,Arial,sans-serif;font-size:14px;font-weight:600;letter-spacing:.6px;line-height:24px;padding:6px 24px;pointer-events:auto;text-transform:none;background:#fff;color:#1a73e8;-webkit-transform:scale(1);-o-transform:scale(1);transform:scale(1);-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out,-o-transform .3s ease-in-out;-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);text-decoration:none;-webkit-font-smoothing:antialiased}#next-step{color:#fff;background:#1a73e8}#done{background:#1e8e3e;color:#fff}google-codelab #fabs a[disappear]{-webkit-transform:scale(0);-o-transform:scale(0);transform:scale(0)}#done{background:#0f9d58}google-codelab #drawer .codelab-time-container{display:none}@media (max-width:768px){google-codelab #codelab-title .codelab-time-container{display:none}google-codelab #drawer .codelab-time-container{display:block;padding:20px 10px 10px 23px}google-codelab #drawer .time-remaining i{margin-right:9px}}google-codelab #drawer{background:#fff;width:256px;-webkit-flex-shrink:0;flex-shrink:0;position:relative;z-index:100;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;background:#f8f9fa}google-codelab #drawer,google-codelab #drawer .steps{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #drawer .steps{-webkit-flex-shrink:1;flex-shrink:1;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;overflow-x:visible;max-height:-webkit-calc(100% - 54px);max-height:calc(100% - 54px)}google-codelab #drawer .steps:only-child{max-height:100%}google-codelab #drawer .metadata .material-icons{top:6px;position:relative}google-codelab #drawer ol{margin:0;padding:16px 12px;counter-reset:li-count;list-style:none;overflow-x:visible;overflow-y:auto;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}google-codelab #drawer ol li{display:block;counter-increment:li-count}google-codelab #drawer ol li a{text-decoration:none;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;font-size:14px;color:#80868b;padding:3px 10px;min-height:48px;font-weight:400;line-height:20px;-webkit-box-sizing:content-box;box-sizing:content-box;position:relative;font-family:Roboto,Noto,sans-serif;-webkit-font-smoothing:antialiased;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out;border:1px solid #dadce0;-webkit-border-radius:5px;border-radius:5px;margin:6px 0;background:#fff}google-codelab #drawer ol li a:active,google-codelab #drawer ol li a:focus{background:#c6c6c6;-webkit-tap-highlight-color:transparent;outline:0;border-color:#c6c6c6!important}google-codelab #drawer ol li a .step{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center}google-codelab #drawer ol li .step:before{content:counter(li-count);display:inline-block;font-style:normal;width:26px;min-width:26px;color:#fff;background:#80868b;-webkit-border-radius:50%;border-radius:50%;text-align:center;height:26px;vertical-align:middle;line-height:26px;margin-right:8px;font-weight:400;position:relative;z-index:2;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}google-codelab #drawer ol li[selected] a,google-codelab #drawer ol li a:focus{color:#212121;font-weight:600;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}google-codelab #drawer ol li[selected] a{border-color:#fff}google-codelab #drawer ol li[selected] .step:before{font-weight:600}google-codelab #drawer ol li[completed] a{color:#212121}google-codelab #drawer ol li[completed] .step:before{background-color:#1a73e8;color:#fff}google-codelab #drawer .metadata{color:#777;font-size:14px;padding:16px;-webkit-flex-shrink:0;flex-shrink:0}google-codelab #drawer .metadata a{color:currentcolor;margin-left:4px}google-codelab #codelab-nav-buttons #menu{display:none}google-codelab #drawer ol ::-webkit-scrollbar{-webkit-appearance:none;width:7px}google-codelab #drawer ol ::-webkit-scrollbar-thumb{-webkit-border-radius:4px;border-radius:4px;background-color:rgba(0,0,0,.5);-webkit-box-shadow:0 0 1px hsla(0,0%,100%,.5)}@media (max-width:768px){google-codelab{display:block;position:relative}google-codelab #main{height:100%}google-codelab #codelab-nav-buttons #arrow-back{display:none}google-codelab #codelab-nav-buttons #menu{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #drawer{width:256px;position:absolute;left:0;top:0;bottom:0;z-index:10000;will-change:transform;-webkit-box-shadow:2px 2px 4px transparent;box-shadow:2px 2px 4px transparent;pointer-events:none;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);-webkit-transition:-webkit-transform .3s ease-in-out,-webkit-box-shadow .3s;transition:-webkit-transform .3s ease-in-out,-webkit-box-shadow .3s;-o-transition:box-shadow .3s,-o-transform ease-in-out .3s;transition:transform .3s ease-in-out,box-shadow .3s;transition:transform .3s ease-in-out,box-shadow .3s,-webkit-transform .3s ease-in-out,-o-transform .3s ease-in-out,-webkit-box-shadow .3s}google-codelab[drawer--open] #drawer{-webkit-box-shadow:2px 2px 4px rgba(0,0,0,.15);box-shadow:2px 2px 4px rgba(0,0,0,.15);-webkit-transform:translateZ(0);transform:translateZ(0);pointer-events:all}google-codelab #main:before{content:"";top:0;left:0;right:0;bottom:0;position:absolute;-webkit-transition:opacity .38s ease-in-out;-o-transition:opacity ease-in-out .38s;transition:opacity .38s ease-in-out;background-color:rgba(0,0,0,.3);z-index:10;pointer-events:none;opacity:0}google-codelab[drawer--open] #main:before{opacity:1;pointer-events:all}}google-codelab #steps{overflow:hidden;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;position:relative;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}google-codelab google-codelab-step{display:none;width:100%;-webkit-transform:translateZ(0);transform:translateZ(0);position:absolute;top:0;left:0;right:0;bottom:0;padding-top:32px;overflow-y:auto;overflow-x:hidden}google-codelab google-codelab-step[animating],google-codelab google-codelab-step[selected]{display:block;-webkit-transform-origin:0 50% 0;-o-transform-origin:0 50% 0;transform-origin:0 50% 0;-webkit-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both}google-codelab google-codelab-step[animating]{pointer-events:none;position:absolute;overflow:hidden}google-codelab #drawer ol li{padding:0;margin:0}google-codelab{height:100vh;left:0;position:fixed;top:0}google-codelab #codelab-title h1{width:auto;color:#3c4043;top:0}google-codelab .title{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;margin-left:10px;overflow-x:hidden}google-codelab #drawer .metadata .material-icons,google-codelab-about .about-card .material-icons{top:0!important}body[type=codelab]{color:#5c5c5c;font-family:Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;overflow:hidden}body[type=codelab] google-codelab{opacity:0;display:-webkit-box;display:-webkit-flex;display:flex}body[type=codelab] devsite-googler-buttons{bottom:95px}body[type=codelab][ready] google-codelab{opacity:1}body[type=codelab] .devsite-main-content{max-width:100%!important;padding:0!important}body[type=codelab] a:focus{text-decoration:none}body[type=codelab] .devsite-badger-award{left:256px}body[type=codelab] .devsite-back-to-top-link,body[type=codelab] .devsite-banner,body[type=codelab] .devsite-footer,body[type=codelab] .devsite-heading-link,body[type=codelab] devsite-code:after,body[type=codelab] devsite-header{display:none}@media screen and (max-width:840px){body[type=codelab] .devsite-badger-award{left:0}}body.google-samples{background:#f1f3f4}body.google-samples .devsite-article{margin:0;width:auto}android-samples{display:block;--mdc-theme-primary:$WHITE}android-samples .mdc-button:not(:disabled){color:#fff}android-samples .sample-grid{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-24px 0 16px -24px}android-samples .resource-card{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 33.3333%;flex:0 0 33.3333%;min-width:0;padding:24px 0 0 24px}android-samples .resource-card:active,android-samples .resource-card:focus{text-decoration:none}android-samples .sample-card{background:#fff;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.21);box-shadow:0 1px 3px 0 rgba(0,0,0,.21);-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;max-height:165px;min-width:0;overflow:hidden;padding:10px 20px 20px;position:relative}android-samples .card-featured .card-info:before{color:#1a73e8;content:"FEATURED";font-size:10px;position:absolute;right:10px;text-align:right;top:8px}android-samples .card-info .section{color:#9aa0a6;font:700 11px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.3px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}android-samples .card-info .title{color:#3c4043;font:500 18px/23px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:7px 0;max-height:46px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:normal}android-samples .sample-description-area .text{color:#5f6368;font-size:14px;line-height:20px;margin:0 0 100px;max-height:60px;overflow:hidden}android-samples .no-display,android-samples .sample-description-area :not(.text){display:none}android-samples .block-display{display:-webkit-box;display:-webkit-flex;display:flex}@media screen and (min-width:1440px){android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 25%;flex:0 0 25%}}@media screen and (max-width:840px){android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}}@media screen and (max-width:600px){android-samples .sample-grid{margin:-16px 0 8px -16px}android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:16px 0 0 16px}}android-samples .navrow{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:#546e7a;display:-webkit-box;display:-webkit-flex;display:flex;max-height:68px;padding:16px;visibility:hidden}@media screen and (max-width:840px){android-samples .navrow{max-height:112px}}@media screen and (max-width:600px){android-samples .navrow{max-height:184px}}body[mdc--ready] android-samples .navrow{max-height:none;visibility:visible}android-samples .mdc-menu-surface[open]{display:block;opacity:1}android-samples .navrow .buttongroup{display:-webkit-box;display:-webkit-flex;display:flex}android-samples .sample-menu{padding:0 4px 0 16px}android-samples .sample-button,android-samples .sample-menu{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:0;-webkit-box-shadow:none;box-shadow:none;color:#fff;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;opacity:.87}android-samples .sample-button:hover,android-samples .sample-menu:hover{background-color:hsla(0,0%,62%,.2)}android-samples .sample-button:active,android-samples .sample-button:focus,android-samples .sample-menu:active,android-samples .sample-menu:focus{background-color:hsla(0,0%,62%,.4);-webkit-box-shadow:none;box-shadow:none}android-samples .mdc-button{letter-spacing:0}android-samples .mdc-list-item__text{font-size:14px}android-samples .googlesamples-filter-menu{padding-left:0}android-samples .menu-indicator{font-size:20px;visibility:hidden}android-samples .menu-indicator.yes-visible{visibility:visible}android-samples .navrow .searchfield{font-size:16px;margin:0 0 0 auto;width:300px}android-samples .navrow input#search{background:#fff;border:0;-webkit-border-radius:2px;border-radius:2px;margin:0;padding:4px 0 4px 8px;width:100%}android-samples .navrow .searchfield label{display:none}@media screen and (max-width:840px){android-samples .navrow{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding:16px 16px 24px}android-samples .navrow .searchfield{margin:8px 0 0}}@media screen and (max-width:600px){android-samples .navrow{margin:0 -16px}android-samples .navrow .buttongroup{-webkit-align-self:flex-start;align-self:flex-start;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}android-samples .navrow .searchfield{margin:8px 16px 0;width:100%}}android-samples .controls-grid{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;padding:16px 24px}android-samples .matching-samples-count,android-samples .per-page-selector{-webkit-box-flex:1;-webkit-flex:1 1 20%;flex:1 1 20%;white-space:nowrap}android-samples .page-number-selector{-webkit-box-flex:1;-webkit-flex:1 1 60%;flex:1 1 60%;text-align:center}android-samples .matching-samples-count{text-align:right}android-samples .page-number-selector-button,android-samples .per-page-item{background:0;border:0;-webkit-border-radius:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none;color:#1a73e8;cursor:pointer;font-size:16px;font-weight:400;height:auto;margin:0;min-width:auto;padding:0 8px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}android-samples .page-number-selector-button:active,android-samples .page-number-selector-button:focus,android-samples .page-number-selector-button:hover,android-samples .per-page-item:active,android-samples .per-page-item:focus,android-samples .per-page-item:hover{background:0}android-samples .page-number-selector-button:hover,android-samples .per-page-item:hover{color:#000}android-samples .page-number-selector-button:active,android-samples .page-number-selector-button:focus,android-samples .per-page-item:active,android-samples .per-page-item:focus{-webkit-box-shadow:none;box-shadow:none}android-samples .page-number-selector-button .material-icons{margin-bottom:4px;vertical-align:middle}android-samples .page-number-selector-inactive,android-samples .per-page-selected-item{color:#000;cursor:default}@media screen and (max-width:840px){android-samples .controls-grid{display:block;padding:16px 0;text-align:center}android-samples .matching-samples-count,android-samples .per-page-selector{margin:8px auto;text-align:center}}@font-face{font-family:Euclid;src:url(../fonts/custom/euclid.ttf) format("truetype"),url(../fonts/custom/euclid.otf) format("opentype"),url(../fonts/custom/euclid.woff) format("woff")}android-sticky-toc ul,android-sticky-toc ul ul{list-style:none;padding-left:0}android-sticky-toc a:focus,android-sticky-toc a:hover,android-sticky-toc a:link,android-sticky-toc a:visited{color:#414141}android-sticky-toc[sticky]{padding-left:32px;position:-webkit-sticky;position:sticky;top:164px;width:240px}android-sticky-toc[embedded]{display:none;margin-top:120px}android-sticky-toc .dac-nav-item.dac-nav-heading{line-height:16px;margin-bottom:16px;max-width:160px}android-sticky-toc .dac-nav-subtitle{font:500 12px/22px Roboto Mono,monospace;color:#414141;text-transform:uppercase}android-sticky-toc .dac-nav-title{font-size:13px}android-sticky-toc .dac-nav-item{margin:0;padding:5px 0}android-sticky-toc div>ul{border-left:4px solid #f5f5f5;padding-left:24px}android-sticky-toc div>ul .dac-nav-title{border-left:4px solid transparent;display:inline-block;margin-left:-28px}android-sticky-toc ul ul .dac-nav-title{padding-left:24px}android-sticky-toc div>ul>.dac-nav-item:first-child{padding-top:0}android-sticky-toc div>ul>.dac-nav-item:last-child,android-sticky-toc div>ul>.dac-nav-item:last-child .dac-nav-item:last-child{padding-bottom:0}android-sticky-toc .dac-nav-title.dac-nav-active{border-left:4px solid #3ddc84;font-weight:500}android-sticky-toc .dac-nav-title .dac-nav-arrow:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"arrow_right";vertical-align:middle}android-sticky-toc .dac-nav-title .dac-nav-arrow.open:before{content:"arrow_drop_down"}android-sticky-toc .dac-nav-list.nested{height:0;visibility:hidden}android-sticky-toc .dac-nav-list.nested.collapse{height:auto;padding-top:5px;visibility:visible}@media screen and (max-width:1200px){android-sticky-toc[sticky]{display:none}android-sticky-toc[embedded]{display:block}}@media screen and (max-width:1000px){android-sticky-toc[embedded]{display:none}}.dac-tutorial-page[ready] .devsite-wrapper{overflow:inherit}.dac-tutorial-page .dac-preview-wrapper{height:-webkit-calc(100vh - 96px);height:calc(100vh - 96px);position:-webkit-sticky;position:sticky;top:64px;width:100%}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview{opacity:0;position:absolute;visibility:hidden;width:100%}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview.active{opacity:1;visibility:visible}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview.screenshot{border:0;-webkit-transition:visibility 0s .3s,opacity .3s linear;-o-transition:visibility 0s .3s,opacity .3s linear;transition:visibility 0s .3s,opacity .3s linear}.dac-tutorial-page .dac-step-wrapper section.active:before{content:"";border-left:4px solid #3ddc84;-webkit-border-radius:4px;border-radius:4px;margin-left:-20px;padding-left:16px}.dac-tutorial-page .dac-preview-toggle-wrapper{color:#fff;position:absolute;right:0;top:27px;z-index:1}.dac-tutorial-page devsite-code .devsite-icon-copy:before{z-index:2}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper .hide,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .show{display:block}.dac-tutorial-page .dac-preview-toggle-wrapper .show,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide{display:none}@media screen and (max-width:1000px){.dac-tutorial-page .dac-step-wrapper section.active:before{content:none}.dac-tutorial-page .dac-preview-toggle-wrapper,.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle{color:#414141;left:0;position:relative;right:0;top:0}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle+.dac-preview-phone{display:none;padding-top:40px;margin:auto;width:200px}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle.toggle+.dac-preview-phone{display:block}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper .hide,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .show{display:none!important}.dac-tutorial-page .dac-preview-toggle-wrapper .show,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide{display:block!important}.dac-tutorial-page .dac-preview-toggle-wrapper .show:after,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide:after{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;height:23px;position:absolute;width:50px}.dac-tutorial-page .dac-preview-toggle-wrapper .show:after{content:"expand_more"}.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide:after{content:"expand_less"}}
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 7f809f1..cbb96f5 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -252,6 +252,9 @@
     docs(project(":privacysandbox:tools:tools-apicompiler"))
     docs(project(":privacysandbox:tools:tools-apigenerator"))
     docs(project(":privacysandbox:tools:tools-core"))
+    docs(project(":privacysandbox:ui:ui-client"))
+    docs(project(":privacysandbox:ui:ui-core"))
+    docs(project(":privacysandbox:ui:ui-provider"))
     docs(project(":profileinstaller:profileinstaller"))
     docs(project(":recommendation:recommendation"))
     docs(project(":recyclerview:recyclerview"))
diff --git a/docs/onboarding.md b/docs/onboarding.md
index 7673b4e..56fd9f3 100644
--- a/docs/onboarding.md
+++ b/docs/onboarding.md
@@ -184,11 +184,11 @@
 allows you to explore all of the source code in the repository. Links to this
 URL may be shared on the public issue tracked and other external sites.
 
+### Custom search engine for `androidx-main` {#custom-search-engine}
+
 We recommend setting up a custom search engine in Chrome as a faster (and
 publicly-accessible) alternative to `cs/`.
 
-### Custom search engine for `androidx-main` {#custom-search-engine}
-
 1.  Open `chrome://settings/searchEngines`
 1.  Click the `Add` button
 1.  Enter a name for your search engine, ex. "AndroidX Code Search"
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index 53a6ddb..9156e71 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -28,12 +28,22 @@
     implementation project(path: ':emoji2:emoji2')
     implementation project(path: ':core:core')
 
-    androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation project(path: ':test:screenshot:screenshot')
 }
 
 android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+    sourceSets {
+        androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/emoji2/emoji2-emojipicker"
+    }
+
     namespace "androidx.emoji2.emojipicker"
+    testOptions.unitTests.includeAndroidResources = true
 }
 
 androidx {
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml b/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..45dad90
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name="androidx.emoji2.emojipicker.EmojiViewTestActivity" />
+    </application>
+</manifest>
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt
new file mode 100644
index 0000000..dd4ff10
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.app.Activity
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import androidx.core.graphics.applyCanvas
+import androidx.core.text.toSpanned
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.assertAgainstGolden
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+class EmojiViewTestActivity : Activity()
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EmojiViewTest {
+    companion object {
+        private const val GRINNING_FACE = "\uD83D\uDE00"
+    }
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule("emoji2/emoji2-emojipicker")
+
+    @get:Rule
+    val activityRule = ActivityScenarioRule(EmojiViewTestActivity::class.java)
+
+    private lateinit var emojiView: EmojiView
+
+    @Before
+    fun setUp() {
+        activityRule.scenario.onActivity {
+            emojiView = EmojiView(it)
+            it.setContentView(emojiView)
+        }
+    }
+
+    private fun setAndWait(cs: CharSequence?) {
+        emojiView.emoji = cs
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+    }
+
+    private fun dumpAndAssertAgainstGolden(golden: String) {
+        Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888).applyCanvas {
+            emojiView.draw(this)
+        }.assertAgainstGolden(screenshotRule, golden)
+    }
+
+    @Test
+    fun testDrawEmoji() {
+        setAndWait(GRINNING_FACE)
+        dumpAndAssertAgainstGolden("draw_grinning_face")
+    }
+
+    @Test
+    fun testDrawSpannedString() {
+        setAndWait(SpannableString("0").apply {
+            setSpan(ForegroundColorSpan(Color.RED), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+        }.toSpanned())
+
+        dumpAndAssertAgainstGolden("draw_red_zero")
+    }
+
+    @Test
+    fun testMultipleDraw() {
+        setAndWait(GRINNING_FACE)
+        setAndWait("M")
+
+        dumpAndAssertAgainstGolden("multiple_draw")
+    }
+
+    @Test
+    fun testClear() {
+        setAndWait(GRINNING_FACE)
+        setAndWait(null)
+
+        dumpAndAssertAgainstGolden("draw_and_clear")
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/AndroidManifest.xml b/emoji2/emoji2-emojipicker/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..280a186
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+</manifest>
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
new file mode 100644
index 0000000..abd602c
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.os.Build
+import android.text.Layout
+import android.text.Spanned
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.util.TypedValue
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.core.graphics.applyCanvas
+
+/**
+ * A customized view to support drawing emojis asynchronously.
+ */
+internal class EmojiView(context: Context) : View(context) {
+    companion object {
+        private const val EMOJI_DRAW_TEXT_SIZE_SP = 30
+    }
+
+    private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {
+        textSize = TypedValue.applyDimension(
+            TypedValue.COMPLEX_UNIT_SP,
+            EMOJI_DRAW_TEXT_SIZE_SP.toFloat(),
+            context.resources.displayMetrics
+        )
+    }
+
+    private val offscreenCanvasBitmap: Bitmap = with(textPaint.fontMetricsInt) {
+        val size = bottom - top
+        Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        val size = MeasureSpec.getSize(widthMeasureSpec)
+        setMeasuredDimension(size, size)
+    }
+
+    override fun draw(canvas: Canvas?) {
+        super.draw(canvas)
+        canvas?.run {
+            save()
+            scale(
+                width.toFloat() / offscreenCanvasBitmap.width,
+                height.toFloat() / offscreenCanvasBitmap.height
+            )
+            drawBitmap(offscreenCanvasBitmap, 0f, 0f, null)
+            restore()
+        }
+    }
+
+    var emoji: CharSequence? = null
+        set(value) {
+            field = value
+            offscreenCanvasBitmap.eraseColor(Color.TRANSPARENT)
+            if (value != null) {
+                post {
+                    drawEmoji(value)
+                    invalidate()
+                }
+            }
+        }
+
+    private fun drawEmoji(emoji: CharSequence) {
+        if (emoji == this.emoji) {
+            offscreenCanvasBitmap.applyCanvas {
+                if (emoji is Spanned) {
+                    createStaticLayout(emoji, width).draw(this)
+                } else {
+                    val textWidth = textPaint.measureText(emoji, 0, emoji.length)
+                    drawText(
+                        emoji,
+                        /* start = */ 0,
+                        /* end = */ emoji.length,
+                        /* x = */ (width - textWidth) / 2,
+                        /* y = */ -textPaint.fontMetrics.top,
+                        textPaint,
+                    )
+                }
+            }
+        }
+    }
+
+    @RequiresApi(23)
+    internal object Api23Impl {
+        fun createStaticLayout(emoji: Spanned, textPaint: TextPaint, width: Int): StaticLayout =
+            StaticLayout.Builder.obtain(
+                emoji, 0, emoji.length, textPaint, width
+            ).apply {
+                setAlignment(Layout.Alignment.ALIGN_CENTER)
+                setLineSpacing(/* spacingAdd = */ 0f, /* spacingMult = */ 1f)
+                setIncludePad(false)
+            }.build()
+    }
+
+    private fun createStaticLayout(emoji: Spanned, width: Int): StaticLayout {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return Api23Impl.createStaticLayout(emoji, textPaint, width)
+        } else {
+            @Suppress("DEPRECATION")
+            return StaticLayout(
+                emoji,
+                textPaint,
+                width,
+                Layout.Alignment.ALIGN_CENTER,
+                /* spacingmult = */ 1f,
+                /* spacingadd = */ 0f,
+                /* includepad = */ false,
+            )
+        }
+    }
+}
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 1b19f8d..317485e 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -1452,9 +1452,17 @@
      * Asserts that {@code expectedImageFile} and {@code actualImageFile} can be decoded by
      * {@link BitmapFactory} and the results have the same width, height and MIME type.
      *
+     * <p>The assertion is skipped if the test is running on an API level where
+     * {@link BitmapFactory} is known not to support the image format of {@code expectedImageFile}
+     * (as determined by file extension).
+     *
      * <p>This does not check the image itself for similarity/equality.
      */
     private void assertBitmapsEquivalent(File expectedImageFile, File actualImageFile) {
+        if (Build.VERSION.SDK_INT < 16 && expectedImageFile.getName().endsWith("webp")) {
+            // BitmapFactory can't parse WebP files on API levels before 16: b/254571189
+            return;
+        }
         BitmapFactory.Options expectedOptions = new BitmapFactory.Options();
         Bitmap expectedBitmap = Objects.requireNonNull(
                 decodeBitmap(expectedImageFile, expectedOptions));
diff --git a/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt b/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
index 0a38bcc..f32df12 100644
--- a/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
+++ b/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
@@ -94,8 +94,8 @@
  *
  * @param fragmentArgs a bundle to passed into fragment
  * @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState the initial [Lifecycle.State]. This must be one of
- * [Lifecycle.State.CREATED], [Lifecycle.State.STARTED], or [Lifecycle.State.RESUMED].
+ * @param initialState the initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
  * @param factory a fragment factory to use or null to use default factory
  */
 public inline fun <reified F : Fragment> launchFragment(
@@ -116,8 +116,8 @@
  *
  * @param fragmentArgs a bundle to passed into fragment
  * @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState the initial [Lifecycle.State]. This must be one of
- * [Lifecycle.State.CREATED], [Lifecycle.State.STARTED], or [Lifecycle.State.RESUMED].
+ * @param initialState the initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
  * @param instantiate method which will be used to instantiate the Fragment.
  */
 public inline fun <reified F : Fragment> launchFragment(
@@ -146,8 +146,8 @@
  *
  * @param fragmentArgs a bundle to passed into fragment
  * @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState the initial [Lifecycle.State]. This must be one of
- * [Lifecycle.State.CREATED], [Lifecycle.State.STARTED], or [Lifecycle.State.RESUMED].
+ * @param initialState the initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
  * @param factory a fragment factory to use or null to use default factory
  */
 public inline fun <reified F : Fragment> launchFragmentInContainer(
@@ -169,8 +169,8 @@
  *
  * @param fragmentArgs a bundle to passed into fragment
  * @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState the initial [Lifecycle.State]. This must be one of
- * [Lifecycle.State.CREATED], [Lifecycle.State.STARTED], or [Lifecycle.State.RESUMED].
+ * @param initialState the initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
  * @param instantiate method which will be used to instantiate the Fragment. This is a
  * simplification of the [FragmentFactory] interface for cases where only a single class
  * needs a custom constructor called.
@@ -304,9 +304,7 @@
      * Moves Fragment state to a new state.
      *
      *  If a new state and current state are the same, this method does nothing. It accepts
-     * [CREATED][Lifecycle.State.CREATED], [STARTED][Lifecycle.State.STARTED],
-     * [RESUMED][Lifecycle.State.RESUMED], and [DESTROYED][Lifecycle.State.DESTROYED].
-     * [DESTROYED][Lifecycle.State.DESTROYED] is a terminal state.
+     * all [Lifecycle.State]s. [DESTROYED][Lifecycle.State.DESTROYED] is a terminal state.
      * You cannot move to any other state after the Fragment reaches that state.
      *
      * This method cannot be called from the main thread.
@@ -463,9 +461,8 @@
          * @param fragmentClass a fragment class to instantiate
          * @param fragmentArgs a bundle to passed into fragment
          * @param themeResId a style resource id to be set to the host activity's theme
-         * @param initialState The initial [Lifecycle.State]. This must be one of
-         * [CREATED][Lifecycle.State.CREATED], [STARTED][Lifecycle.State.STARTED], and
-         * [RESUMED][Lifecycle.State.RESUMED].
+         * @param initialState The initial [Lifecycle.State]. Passing in
+         * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
          * @param factory a fragment factory to use or null to use default factory
          */
         @JvmOverloads
@@ -545,9 +542,8 @@
          * @param fragmentClass a fragment class to instantiate
          * @param fragmentArgs a bundle to passed into fragment
          * @param themeResId a style resource id to be set to the host activity's theme
-         * @param initialState The initial [Lifecycle.State]. This must be one of
-         * [CREATED][Lifecycle.State.CREATED], [STARTED][Lifecycle.State.STARTED], and
-         * [RESUMED][Lifecycle.State.RESUMED].
+         * @param initialState The initial [Lifecycle.State]. Passing in
+         * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
          * @param factory a fragment factory to use or null to use default factory
          */
         @JvmOverloads
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index e067485..d07f061 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -19,7 +19,6 @@
 
     defaultConfig {
         multiDexEnabled true
-        testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"
     }
     namespace "androidx.fragment"
 }
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ActivityLeakTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ActivityLeakTest.kt
index ea1aa11..0235efe 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ActivityLeakTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ActivityLeakTest.kt
@@ -23,11 +23,13 @@
 import androidx.testutils.runOnUiThreadRethrow
 import androidx.testutils.waitForExecution
 import com.google.common.truth.Truth.assertWithMessage
+import java.lang.ref.WeakReference
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
-import java.lang.ref.WeakReference
 
 /**
  * Class representing the different configurations for testing leaks
@@ -83,9 +85,13 @@
     }
 
     @Suppress("DEPRECATION")
-    @get:Rule
     val activityRule = androidx.test.rule.ActivityTestRule(ActivityLeakActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun testActivityDoesNotLeak() {
         // Restart the activity because activityRule keeps a strong reference to the
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordStateTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordStateTest.kt
index ce2a35a..bac35d4 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordStateTest.kt
@@ -24,20 +24,27 @@
 import androidx.test.filters.LargeTest
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class BackStackRecordStateTest {
 
-    @get:Rule
+    @Suppress("DEPRECATION")
     val activityRule = ActivityScenarioRule(EmptyFragmentTestActivity::class.java)
     private val fragmentManager get() = activityRule.withActivity {
         supportFragmentManager
     }
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun testParcel() {
         val fragment = StrictFragment()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordTest.kt
index ce059cb..de1a619 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordTest.kt
@@ -24,9 +24,11 @@
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
@@ -34,8 +36,12 @@
 class BackStackRecordTest {
 
     @Suppress("DEPRECATION")
+    val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
     @Test
     @UiThreadTest
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 c3025c4..25584e9 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
@@ -25,20 +25,27 @@
 import androidx.test.filters.LargeTest
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class BackStackStateTest {
 
-    @get:Rule
+    @Suppress("DEPRECATION")
     val activityRule = ActivityScenarioRule(EmptyFragmentTestActivity::class.java)
     private val fragmentManager get() = activityRule.withActivity {
         supportFragmentManager
     }
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun testRestoreFromPending() {
         val fragment = StrictFragment()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ChildFragmentStateTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ChildFragmentStateTest.kt
index 0346592..f9ed241 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ChildFragmentStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ChildFragmentStateTest.kt
@@ -22,17 +22,24 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class ChildFragmentStateTest {
+
     @Suppress("DEPRECATION")
+    val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
     @Test
     @UiThreadTest
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ContentViewTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ContentViewTest.kt
index c7314e8..a69f121 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ContentViewTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ContentViewTest.kt
@@ -23,6 +23,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -30,6 +32,9 @@
 @MediumTest
 class ContentViewTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testContentViewWithInflatedFragment() {
         // Test basic lifecycle when the fragment view is inflated with <fragment> tag
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DefaultSpecialEffectsControllerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DefaultSpecialEffectsControllerTest.kt
index 9951e7f..80f2dcb 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DefaultSpecialEffectsControllerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DefaultSpecialEffectsControllerTest.kt
@@ -24,6 +24,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
@@ -31,6 +33,10 @@
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class DefaultSpecialEffectsControllerTest {
+
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun fragmentManagerGetSetSpecialEffectsController() {
        withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
index 8ae92e1..8debe9d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
@@ -26,12 +26,14 @@
 import androidx.lifecycle.LifecycleEventObserver
 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
-import org.junit.runners.Parameterized
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 /**
  * Class representing the different ways of dismissing a [DialogFragment]
@@ -99,10 +101,13 @@
     }
 
     @Suppress("DEPRECATION")
+    val activityTestRule =
+        androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityTestRule = androidx.test.rule.ActivityTestRule(
-        EmptyFragmentTestActivity::class.java
-    )
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityTestRule)
 
     @Test
     fun testDialogFragmentDismiss() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentInflatedChildTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentInflatedChildTest.kt
index 7e427d56..18c0ce4 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentInflatedChildTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentInflatedChildTest.kt
@@ -28,6 +28,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
@@ -79,6 +81,9 @@
         }
     }
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testInflatedChildDialogFragment() {
        withUse(ActivityScenario.launch(SimpleContainerActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
index 7e025db..5a8a3cd 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
@@ -38,18 +38,24 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class DialogFragmentTest {
+
     @Suppress("DEPRECATION")
+    val activityTestRule =
+        androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    val activityTestRule = androidx.test.rule.ActivityTestRule(
-        EmptyFragmentTestActivity::class.java
-    )
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityTestRule)
 
     @Test
     fun testDialogFragmentShows() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
index ba52e8d..b4882b5 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
@@ -29,12 +29,18 @@
 import androidx.test.filters.LargeTest
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class DialogFragmentViewTreeTest {
+
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testDialogFragmentViewTree() {
        withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/EpicenterTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/EpicenterTest.kt
index 84b9f47..cc28826 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/EpicenterTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/EpicenterTest.kt
@@ -29,8 +29,10 @@
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @SmallTest
@@ -39,9 +41,13 @@
 class EpicenterTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun defaultEpicenter() {
         testViewEpicenter {}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentActivityResultTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentActivityResultTest.kt
index 3af0369..99caf17 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentActivityResultTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentActivityResultTest.kt
@@ -32,7 +32,9 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -43,6 +45,9 @@
 @MediumTest
 class FragmentActivityResultTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun registerActivityResultInOnAttach() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatedContainerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatedContainerTest.kt
index 4350b4f..be9369c 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatedContainerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatedContainerTest.kt
@@ -22,19 +22,26 @@
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class FragmentAnimatedContainerTest {
+
     @Suppress("DEPRECATION")
+    val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
     @Before
     fun setupContainer() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
index 69a2491..d9480fe 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
@@ -35,20 +35,26 @@
 import androidx.test.filters.LargeTest
 import androidx.testutils.waitForExecution
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class FragmentAnimatorTest {
 
     @Suppress("DEPRECATION")
+    val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
     @Before
     fun setupContainer() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
index b9e79ec..18af166 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
@@ -30,6 +30,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,6 +39,9 @@
 @LargeTest
 class FragmentArchLifecycleTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testFragmentAdditionDuringOnStop() {
        withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
index 436b3d4..81cf605 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
@@ -27,6 +27,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -34,6 +36,9 @@
 @LargeTest
 class FragmentContainerInflatedFragmentTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testContentViewWithInflatedFragment() {
         // The StrictViewFragment runs the appropriate checks to make sure
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
index 68ae0531..089d11e 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
@@ -21,24 +21,30 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class FragmentFinishEarlyTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     val activityRule = androidx.test.rule.ActivityTestRule(
         FragmentFinishEarlyTestActivity::class.java,
         false,
         false
     )
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     /**
      * FragmentActivity should not raise the state of a Fragment while it is being destroyed.
      */
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
index 28fe352..74c143b 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
@@ -31,15 +31,20 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class FragmentFocusTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun focusedViewRemoved() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
index d31fa72..aedeb6a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
@@ -42,9 +42,11 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.times
@@ -55,9 +57,13 @@
 class FragmentLifecycleTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun basicLifecycle() {
         val fm = activityRule.activity.supportFragmentManager
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerNonConfigTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerNonConfigTest.kt
index 1e72738..4221075 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerNonConfigTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerNonConfigTest.kt
@@ -23,8 +23,10 @@
 import androidx.test.filters.SdkSuppress
 import androidx.testutils.recreate
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @MediumTest
@@ -32,9 +34,13 @@
 class FragmentManagerNonConfigTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(NonConfigOnStopActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     /**
      * When a fragment is added during onStop(), it shouldn't show up in non-config
      * state when restored before P, because OnSaveInstanceState was already called.
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerSavedStateRegistryTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerSavedStateRegistryTest.kt
index 8fa1d0d..20705fd 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerSavedStateRegistryTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerSavedStateRegistryTest.kt
@@ -23,6 +23,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -30,6 +32,9 @@
 @MediumTest
 class FragmentManagerSavedStateRegistryTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     @Throws(Throwable::class)
     fun savedState() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
index a062eea..72dcbb3 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
@@ -24,7 +24,9 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -32,6 +34,9 @@
 @RunWith(AndroidJUnit4::class)
 class FragmentManagerTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun addRemoveFragmentOnAttachListener() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerViewModelTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerViewModelTest.kt
index e17ea7a..8b9fc41 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerViewModelTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerViewModelTest.kt
@@ -20,7 +20,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
@@ -29,6 +31,9 @@
 @LargeTest
 class FragmentManagerViewModelTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     private lateinit var viewModel: FragmentManagerViewModel
 
     @Before
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
index 6606f46..1b0f929 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
@@ -25,6 +25,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,6 +37,9 @@
 @LargeTest
 class FragmentReplaceTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testReplaceFragment() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentResultTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentResultTest.kt
index a73efd9..a7112a1 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentResultTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentResultTest.kt
@@ -28,6 +28,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,6 +37,9 @@
 @LargeTest
 class FragmentResultTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testReplaceResult() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
index 3df365a..c015eff 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
@@ -34,6 +34,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -41,6 +43,9 @@
 @LargeTest
 class FragmentSavedStateRegistryTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     private fun ActivityScenario<FragmentSavedStateActivity>.initializeSavedState(
         testFragment: Fragment = Fragment()
     ) = withActivity {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt
index c6ae071..4bd91d9 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt
@@ -27,6 +27,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -35,6 +37,9 @@
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
 class FragmentSharedElementTransitionTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testNestedSharedElementView() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
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 1669ac0..1af6505 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
@@ -21,7 +21,9 @@
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 
 import org.junit.runner.RunWith
@@ -38,6 +40,9 @@
     private val classLoader get() = InstrumentationRegistry.getInstrumentation()
         .targetContext.classLoader
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Before
     fun setup() {
         fragmentStore = FragmentStore()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.kt
index c5a62e7..2313a6f 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.kt
@@ -32,24 +32,31 @@
 import androidx.testutils.waitForExecution
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicInteger
 
 /**
  * Miscellaneous tests for fragments that aren't big enough to belong to their own classes.
  */
 @RunWith(AndroidJUnit4::class)
 class FragmentTest {
+
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     private lateinit var instrumentation: Instrumentation
 
     @Before
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
index e779648..3925588 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
@@ -32,14 +32,16 @@
 import androidx.testutils.waitForExecution
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.After
 import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 /**
  * Tests usage of the [androidx.fragment.app.FragmentTransaction] class.
@@ -49,9 +51,13 @@
 class FragmentTransactionTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     private var onBackStackChangedTimes: Int = 0
     private lateinit var onBackStackChangedListener: FragmentManager.OnBackStackChangedListener
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
index 0f9a36a..1a0a8a1 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
@@ -27,9 +27,11 @@
 import androidx.test.filters.LargeTest
 import androidx.testutils.waitForExecution
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 /**
@@ -38,10 +40,15 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class FragmentTransitTest {
+
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Before
     fun setupContainer() {
         activityRule.setContentView(R.layout.simple_container)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
index 9fb383b..ed669b4 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
@@ -32,12 +32,14 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
 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.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @LargeTest
 @RunWith(Parameterized::class)
@@ -47,6 +49,9 @@
 ) {
     private var onBackStackChangedTimes: Int = 0
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Before
     fun setup() {
         >
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/HangingFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/HangingFragmentTest.kt
index 4796cac..3da8c66 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/HangingFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/HangingFragmentTest.kt
@@ -21,8 +21,10 @@
 import androidx.test.filters.MediumTest
 import androidx.testutils.recreate
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
@@ -30,9 +32,13 @@
 class HangingFragmentTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(HangingFragmentActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun testNoCrash() {
         val newActivity = activityRule.recreate()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
index d94385c..97f5ae8 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
@@ -28,18 +28,25 @@
 import androidx.testutils.recreate
 import androidx.testutils.waitForExecution
 import com.google.common.truth.Truth.assertThat
+import java.lang.ref.WeakReference
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
-import java.lang.ref.WeakReference
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class LoaderTest {
+
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(LoaderActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     /**
      * Test to ensure that there is no Activity leak due to Loader
      */
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/MenuVisibilityFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/MenuVisibilityFragmentTest.kt
index b6c042c..e9c49dc 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/MenuVisibilityFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/MenuVisibilityFragmentTest.kt
@@ -24,6 +24,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -31,6 +33,9 @@
 @RunWith(AndroidJUnit4::class)
 class MenuVisibilityFragmentTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun setMenuVisibility() {
        withUse(ActivityScenario.launch(SimpleContainerActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
index c98d7a7..88e79ea 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
@@ -27,15 +27,20 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Test
-import org.junit.runner.RunWith
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class NestedFragmentRestoreTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Suppress("DEPRECATION")
     @Test
     fun recreateActivity() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedInflatedFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedInflatedFragmentTest.kt
index 6431a8d..2e65beb 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedInflatedFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedInflatedFragmentTest.kt
@@ -28,8 +28,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
@@ -37,9 +39,13 @@
 class NestedInflatedFragmentTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     @UiThreadTest
     fun inflatedChildFragment() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
index 578f168..241a570d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
@@ -29,6 +29,9 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,6 +40,9 @@
 @MediumTest
 class OnBackPressedCallbackTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun testBackPressFinishesActivity() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
@@ -167,6 +173,7 @@
         }
     }
 
+    @Ignore // b/250870927
     @Test
     fun testBackPressFinishesActivityAfterFragmentPop() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
index 3cdf8efd..27a4ec9 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
@@ -40,8 +40,10 @@
 import com.google.common.truth.Truth.assertWithMessage
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) // Previous SDK levels have issues passing
@@ -49,10 +51,15 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class OptionsMenuFragmentTest {
+
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun fragmentWithOptionsMenu() {
         activityRule.setContentView(R.layout.simple_container)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
index 4255529..beb4c0a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
@@ -27,17 +27,24 @@
 import androidx.testutils.waitForExecution
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class PrimaryNavFragmentTest {
+
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     fun delegateBackToPrimaryNav() {
         val fm = activityRule.activity.supportFragmentManager
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragmentTest.kt
index a9de786..a8992af 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragmentTest.kt
@@ -22,9 +22,11 @@
 import androidx.lifecycle.ViewModelStore
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
@@ -50,8 +52,12 @@
     }
 
     @Suppress("DEPRECATION")
+    val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
     // Make sure that executing transactions during activity lifecycle events
     // is properly prevented.
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SafeTransactionInOnResumeTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SafeTransactionInOnResumeTest.kt
index 8b58e97..a24708e 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SafeTransactionInOnResumeTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SafeTransactionInOnResumeTest.kt
@@ -23,23 +23,29 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.assertTrue
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class SafeTransactionInOnResumeTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule<OnResumeTestActivity>(
         OnResumeTestActivity::class.java
     )
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @Test
     @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
     fun onResumeTest() {
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 7ab46c8..413ec73 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
@@ -26,7 +26,9 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert.fail
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -34,6 +36,9 @@
 @RunWith(AndroidJUnit4::class)
 class SaveRestoreBackStackTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun saveBackStack() {
        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
index 4ea17b2..f9bcff3 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
@@ -26,8 +26,10 @@
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
@@ -35,8 +37,12 @@
 class SaveStateFragmentTest {
 
     @Suppress("DEPRECATION")
+    val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
     @Test
     @UiThreadTest
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
index 0b9d741..123bcf4 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
@@ -28,12 +28,18 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class SpecialEffectsControllerTest {
+
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test
     fun factoryCreateController() {
         val map = mutableMapOf<ViewGroup, TestSpecialEffectsController>()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
index b11ab9c..15e5a3c 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
@@ -24,9 +24,11 @@
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Assert
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @Suppress("DEPRECATION")
@@ -35,8 +37,12 @@
 class TargetFragmentLifeCycleTest {
 
     @Suppress("DEPRECATION")
+    val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
     @Test
     fun targetFragmentNoCycles() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/UserVisibleHintTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/UserVisibleHintTest.kt
index 14b4071..7613e37 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/UserVisibleHintTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/UserVisibleHintTest.kt
@@ -22,8 +22,10 @@
 import androidx.test.filters.MediumTest
 import androidx.testutils.runOnUiThreadRethrow
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @Suppress("DEPRECATION")
@@ -32,9 +34,13 @@
 class UserVisibleHintTest {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     @UiThreadTest
     @Test
     fun startOrdering() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
index fe34c2d..7fc4207 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
@@ -29,6 +29,8 @@
 import androidx.testutils.withActivity
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -36,6 +38,9 @@
 @RunWith(AndroidJUnit4::class)
 class ViewModelTest {
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Test(expected = IllegalStateException::class)
     @UiThreadTest
     fun testNotAttachedFragment() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.kt
index 9809d911..dcbb91a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.kt
@@ -24,8 +24,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 import org.junit.runner.RunWith
 
 @SmallTest
@@ -33,8 +35,12 @@
 class ViewModelTestInTransaction {
 
     @Suppress("DEPRECATION")
+    val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+    // Detect leaks BEFORE and AFTER activity is destroyed
     @get:Rule
-    var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
     @Test
     @UiThreadTest
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 f23e8b3..83a2f21 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
@@ -29,8 +29,10 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -41,6 +43,9 @@
 
     private lateinit var originalPolicy: FragmentStrictMode.Policy
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Before
     public fun setup() {
         originalPolicy = FragmentStrictMode.defaultPolicy
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
index 811b311..189229d 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
@@ -45,7 +45,7 @@
             </intent-filter>
             <meta-data
                 android:name="android.appwidget.provider"
-                android:resource="@xml/default_app_widget_info" />
+                android:resource="@xml/gallery_app_widget_info" />
         </receiver>
 
         <receiver
@@ -59,7 +59,7 @@
             </intent-filter>
             <meta-data
                 android:name="android.appwidget.provider"
-                android:resource="@xml/default_app_widget_info" />
+                android:resource="@xml/gallery_app_widget_info" />
         </receiver>
 
         <receiver
@@ -73,7 +73,7 @@
             </intent-filter>
             <meta-data
                 android:name="android.appwidget.provider"
-                android:resource="@xml/default_app_widget_info" />
+                android:resource="@xml/gallery_app_widget_info" />
         </receiver>
 
         <receiver
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
index 790285e..bbf0c0b 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
@@ -21,7 +21,6 @@
 import androidx.glance.ImageProvider
 import androidx.glance.appwidget.GlanceAppWidget
 import androidx.glance.appwidget.GlanceAppWidgetReceiver
-import androidx.glance.appwidget.SizeMode
 import androidx.glance.appwidget.action.actionRunCallback
 import androidx.glance.appwidget.template.GalleryTemplate
 import androidx.glance.appwidget.template.GlanceTemplateAppWidget
@@ -82,7 +81,6 @@
  * It is overridable by gallery image aspect ratio, image size, and main blocks ordering.
  */
 abstract class BaseGalleryTemplateWidget : GlanceTemplateAppWidget() {
-    override val sizeMode = SizeMode.Exact
 
     @Composable
     internal fun GalleryTemplateContent(
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/xml/gallery_app_widget_info.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/xml/gallery_app_widget_info.xml
new file mode 100644
index 0000000..d4ebabd
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/xml/gallery_app_widget_info.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+    android:minWidth="100dp"
+    android:minHeight="100dp"
+    android:minResizeHeight="40dp"
+    android:minResizeWidth="90dp"
+    android:initialLayout="@layout/glance_default_loading_layout"
+    android:previewImage="@drawable/palm_leaf"
+    android:resizeMode="horizontal|vertical"
+    android:widgetCategory="home_screen" />
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
index e8fb592..6963b7c 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
@@ -27,6 +27,7 @@
 import androidx.glance.appwidget.lazy.GridCells
 import androidx.glance.appwidget.lazy.LazyVerticalGrid
 import androidx.glance.appwidget.lazy.itemsIndexed
+import androidx.glance.appwidget.template.GlanceTemplateAppWidget.Companion.sizeMin
 import androidx.glance.background
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Column
@@ -160,12 +161,17 @@
 @Composable
 private fun HeaderAndTextBlocks(data: GalleryTemplateData, modifier: GlanceModifier) {
     Column(modifier = modifier) {
-        HeaderBlockTemplate(data.header)
-        // TODO(b/247613894): Spacing should be conditional
-        Spacer(modifier = GlanceModifier.height(16.dp).defaultWeight())
+        data.header?.let {
+            HeaderBlockTemplate(data.header)
+            Spacer(modifier = GlanceModifier.height(16.dp).defaultWeight())
+        }
         TextBlockTemplate(data.mainTextBlock)
-        Spacer(modifier = GlanceModifier.height(16.dp))
-        ActionBlockTemplate(data.mainActionBlock)
+        data.mainActionBlock?.let {
+            if (LocalSize.current.width > sizeMin && LocalSize.current.height > sizeMin) {
+                Spacer(modifier = GlanceModifier.height(16.dp))
+                ActionBlockTemplate(data.mainActionBlock)
+            }
+        }
     }
 }
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
index dfc7952..82b55d2 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
@@ -36,13 +36,17 @@
 abstract class GlanceTemplateAppWidget : GlanceAppWidget() {
 
     companion object {
-        private val COLLAPSED = DpSize(30.dp, 30.dp)
-        private val HORIZONTAL_S = DpSize(241.dp, 30.dp)
-        private val HORIZONTAL_M = DpSize(241.dp, 200.dp)
-        private val HORIZONTAL_L = DpSize(350.dp, 200.dp)
-        private val VERTICAL_S = DpSize(30.dp, 241.dp)
-        private val VERTICAL_M = DpSize(200.dp, 241.dp)
-        private val VERTICAL_L = DpSize(200.dp, 350.dp)
+        internal val sizeMin = 30.dp
+        internal val sizeS = 200.dp
+        internal val sizeM = 241.dp
+        internal val sizeL = 350.dp
+        private val COLLAPSED = DpSize(sizeMin, sizeMin)
+        private val HORIZONTAL_S = DpSize(sizeM, sizeMin)
+        private val HORIZONTAL_M = DpSize(sizeM, sizeS)
+        private val HORIZONTAL_L = DpSize(sizeL, sizeS)
+        private val VERTICAL_S = DpSize(sizeMin, sizeM)
+        private val VERTICAL_M = DpSize(sizeS, sizeM)
+        private val VERTICAL_L = DpSize(sizeS, sizeL)
     }
 
     /** Default widget size mode is [SizeMode.Responsive] */
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
index f56c2e5..f50c2ec 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
@@ -22,6 +22,8 @@
 import androidx.glance.GlanceTheme
 import androidx.glance.LocalSize
 import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.template.GlanceTemplateAppWidget.Companion.sizeMin
+import androidx.glance.appwidget.template.GlanceTemplateAppWidget.Companion.sizeS
 import androidx.glance.background
 import androidx.glance.layout.Column
 import androidx.glance.layout.ContentScale
@@ -88,7 +90,7 @@
         Row(modifier = GlanceModifier.fillMaxWidth()) {
             data.textBlock?.let { AppWidgetTextSection(textList(it.text1, it.text2)) }
             // TODO(b/247613894): Fix for multiple actions
-            if (LocalSize.current.width > 100.dp) {
+            if (LocalSize.current.width > sizeMin) {
                 data.actionBlock?.let {
                     Spacer(modifier = GlanceModifier.width(16.dp))
                     Spacer(modifier = GlanceModifier.defaultWeight())
@@ -111,13 +113,11 @@
             }
             Spacer(modifier = GlanceModifier.defaultWeight())
 
-            // TODO(b/247613894): Extract size constraints as template constants
             data.textBlock?.let {
-                val body = if (LocalSize.current.height > 150.dp) it.text3 else null
+                val body = if (LocalSize.current.height >= sizeS) it.text3 else null
                 AppWidgetTextSection(textList(it.text1, it.text2, body))
             }
-            // TODO(b/247613894): Extract threshold sizes as constants
-            if (LocalSize.current.height > 30.dp) {
+            if (LocalSize.current.height > sizeMin) {
                 data.actionBlock?.let {
                     Spacer(modifier = GlanceModifier.height(16.dp))
                     ActionBlockTemplate(it)
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index 30e291c..3d96ae5 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -57,6 +57,7 @@
     testImplementation("androidx.datastore:datastore-core:1.0.0")
     testImplementation("androidx.datastore:datastore-preferences-core:1.0.0")
     testImplementation("androidx.datastore:datastore-preferences:1.0.0-rc02")
+    testImplementation("androidx.room:room-runtime:2.4.3")
     testImplementation("androidx.work:work-testing:2.7.1")
     testImplementation("com.google.android.material:material:1.6.0")
 }
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
index 8bc9546..8b3e4da 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
@@ -24,6 +24,7 @@
 import androidx.work.CoroutineWorker
 import androidx.work.WorkManager
 import androidx.work.WorkerParameters
+import androidx.work.impl.WorkManagerImpl
 import androidx.work.testing.WorkManagerTestInitHelper
 import com.google.common.truth.Truth.assertThat
 import kotlin.coroutines.suspendCoroutine
@@ -31,7 +32,6 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
@@ -69,9 +69,11 @@
     @After
     fun cleanUp() {
         WorkManager.getInstance(context).cancelAllWork()
+        // TODO(b/242026176): remove this once WorkManager allows closing the test
+        // database.
+        WorkManagerImpl.getInstance(context).workDatabase.close()
     }
 
-    @Ignore // b/254048913
     @Test
     fun startSession() = runTest {
         assertThat(sessionManager.isSessionRunning(context, key)).isFalse()
@@ -80,7 +82,6 @@
         assertThat(sessionManager.getSession(key)).isSameInstanceAs(session)
     }
 
-    @Ignore // b/254048913
     @Test
     fun closeSession() = runTest {
         sessionManager.startSession(context, session)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8967b76..f628ef5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,12 +14,12 @@
 androidGradlePluginMin = "7.0.4"
 androidLintMin = "30.0.4"
 androidLintMinCompose = "30.0.0"
-androidxTestRunner = "1.4.0"
-androidxTestRules = "1.4.0"
-androidxTestMonitor = "1.5.0"
-androidxTestCore = "1.5.0-beta01"
-androidxTestExtJunit = "1.1.3"
-androidxTestExtTruth = "1.4.0"
+androidxTestRunner = "1.5.0-rc01"
+androidxTestRules = "1.5.0-rc01"
+androidxTestMonitor = "1.6.0-rc01"
+androidxTestCore = "1.5.0-rc01"
+androidxTestExtJunit = "1.1.4-rc01"
+androidxTestExtTruth = "1.5.0-rc01"
 atomicFu = "0.17.0"
 autoService = "1.0-rc6"
 autoValue = "1.6.3"
@@ -29,7 +29,7 @@
 dagger = "2.44"
 dexmaker = "2.28.3"
 dokka = "1.7.20"
-espresso = "3.5.0-beta01"
+espresso = "3.5.0-rc01"
 guavaJre = "31.1-jre"
 hilt = "2.44"
 incap = "0.2"
@@ -46,7 +46,7 @@
 metalava = "1.0.0-alpha06"
 mockito = "2.25.0"
 moshi = "1.13.0"
-protobuf = "3.19.4"
+protobuf = "3.21.8"
 paparazzi = "1.0.0"
 paparazziNative = "2022.1.1-canary-f5f9f71"
 skiko = "0.7.7"
@@ -192,9 +192,12 @@
 nullaway = { module = "com.uber.nullaway:nullaway", version = "0.3.7" }
 okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.7" }
 okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
+playFeatureDelivery = { module = "com.google.android.play:feature-delivery", version = "2.0.1" }
 playCore = { module = "com.google.android.play:core", version = "1.10.3" }
+playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.3.0"}
 playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
 playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
+playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "19.0.0-beta"}
 playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
 paparazzi = { module = "app.cash.paparazzi:paparazzi", version.ref = "paparazzi" }
 paparazziNativeJvm = { module = "app.cash.paparazzi:layoutlib-native-jdk11", version.ref = "paparazziNative" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f8abc10..c4aadd3 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -15,6 +15,7 @@
          <trust group="com.google.android.gms"/> <!-- b/215442095 -->
          <trust group="com.google.android.datatransport"/> <!-- b/215442095 -->
          <trust group="com.google.android.material"/> <!-- b/216192082 -->
+         <trust group="com.google.android.play"/> <!-- b/255621183 -->
          <trust group="com.google.firebase"/> <!-- b/223907608 -->
          <trust group="com.google.mlkit"/> <!-- b/223907608 -->
          <trust file=".*kotlin-native-prebuilt-macos-.*" regex="true"/> <!-- b/228184608 -->
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 430083d..63f1c4e 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -21,11 +21,9 @@
 import android.opengl.GLES20
 import android.opengl.Matrix
 import android.os.Build
-import android.util.Log
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
 import androidx.graphics.opengl.egl.EGLManager
-import androidx.graphics.opengl.egl.deviceSupportsNativeAndroidFence
 import androidx.graphics.surface.SurfaceControlCompat
 import androidx.graphics.surface.SurfaceControlUtils
 import androidx.lifecycle.Lifecycle
@@ -54,14 +52,12 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     @Test
     fun testFrontBufferedLayerRender() {
-        if (!deviceSupportsNativeAndroidFence()) {
-            // If the Android device does not support the corresponding extensions to create
-            // a file descriptor from an EGLSync object then skip the test
-            Log.w(TAG, "Skipping testFrontBufferedLayerRender, no native android fence support")
-            return
-        }
         val renderLatch = CountDownLatch(1)
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
+
+            val mProjectionMatrix = FloatArray(16)
+            val mOrthoMatrix = FloatArray(16)
+
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
                 bufferWidth: Int,
@@ -70,8 +66,18 @@
                 param: Any
             ) {
                 GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
-                GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
-                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                Rectangle().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
             }
 
             override fun onDrawDoubleBufferedLayer(
@@ -82,8 +88,18 @@
                 params: Collection<Any>
             ) {
                 GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
-                GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
-                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                Rectangle().draw(mProjectionMatrix, Color.BLUE, 0f, 0f, 100f, 100f)
             }
 
             override fun onFrontBufferedLayerRenderComplete(
@@ -140,15 +156,12 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     @Test
     fun testDoubleBufferedLayerRender() {
-        if (!deviceSupportsNativeAndroidFence()) {
-            // If the Android device does not support the corresponding extensions to create
-            // a file descriptor from an EGLSync object then skip the test
-            Log.w(TAG, "Skipping testDoubleBufferedLayerRender, no native android fence support")
-            return
-        }
-
         val renderLatch = CountDownLatch(1)
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
+
+            private val mOrthoMatrix = FloatArray(16)
+            private val mProjectionMatrix = FloatArray(16)
+
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
                 bufferWidth: Int,
@@ -157,8 +170,18 @@
                 param: Any
             ) {
                 GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
-                GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
-                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                Rectangle().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
             }
 
             override fun onDrawDoubleBufferedLayer(
@@ -169,8 +192,18 @@
                 params: Collection<Any>
             ) {
                 GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
-                GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
-                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                Rectangle().draw(mProjectionMatrix, Color.BLUE, 0f, 0f, 100f, 100f)
             }
 
             override fun onDoubleBufferedLayerRenderComplete(
@@ -287,17 +320,16 @@
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     fun testRenderFrontBufferSeveralTimes() {
-        if (!deviceSupportsNativeAndroidFence()) {
-            // If the Android device does not support the corresponding extensions to create
-            // a file descriptor from an EGLSync object then skip the test
-            Log.w(TAG, "Skipping testDoubleBufferedLayerRender, no native android fence support")
-            return
-        }
 
         val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
 
             var red = 1f
             var blue = 0f
+            val mOrthoMatrix = FloatArray(16)
+            val mProjectionMatrix = FloatArray(16)
+            var mRectangle: Rectangle? = null
+
+            private fun getSquare(): Rectangle = mRectangle ?: Rectangle().also { mRectangle = it }
 
             override fun onDrawFrontBufferedLayer(
                 eglManager: EGLManager,
@@ -307,8 +339,20 @@
                 param: Any
             ) {
                 GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
-                GLES20.glClearColor(red, 0.0f, blue, 1.0f)
-                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                val color = Color.argb(1f, red, 0f, blue)
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                getSquare().draw(mProjectionMatrix, color, 0f, 0f, 100f, 100f)
+
                 val tmp = red
                 red = blue
                 blue = tmp
@@ -322,8 +366,19 @@
                 params: Collection<Any>
             ) {
                 GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
-                GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
-                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                val color = Color.argb(1f, red, 0f, blue)
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                getSquare().draw(mProjectionMatrix, color, 0f, 0f, 100f, 100f)
             }
         }
         var renderer: GLFrontBufferedRenderer<Any>? = null
@@ -336,7 +391,7 @@
 
             scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
                 val param = Any()
-                repeat(4000) {
+                repeat(500) {
                     renderer?.renderFrontBufferedLayer(param)
                 }
             }
@@ -347,13 +402,7 @@
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     @Test
-    fun testFrontBufferedLayerPersistence() {
-        if (!deviceSupportsNativeAndroidFence()) {
-            // If the Android device does not support the corresponding extensions to create
-            // a file descriptor from an EGLSync object then skip the test
-            Log.w(TAG, "Skipping testDoubleBufferedLayerRender, no native android fence support")
-            return
-        }
+    fun testDoubleBufferedContentsNotPersisted() {
         val mOrthoMatrix = FloatArray(16)
         val mProjectionMatrix = FloatArray(16)
         val mLines = FloatArray(4)
@@ -474,9 +523,9 @@
             SurfaceControlUtils.validateOutput { bitmap ->
                 (bitmap.getPixel(
                     coords[0] + width / 4, coords[1] + height / 2
-                ) == Color.RED) &&
+                ) == Color.BLACK) &&
                     (bitmap.getPixel(
-                        coords[0] + 3 * width / 4,
+                        coords[0] + 3 * width / 4 - 1,
                         coords[1] + height / 2
                     ) == Color.RED)
             }
@@ -485,6 +534,116 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testRenderAfterPauseAndResume() {
+        val renderLatch = CountDownLatch(2)
+        val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
+
+            val mProjectionMatrix = FloatArray(16)
+            val mOrthoMatrix = FloatArray(16)
+
+            override fun onDrawFrontBufferedLayer(
+                eglManager: EGLManager,
+                bufferWidth: Int,
+                bufferHeight: Int,
+                transform: FloatArray,
+                param: Any
+            ) {
+                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                Rectangle().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
+            }
+
+            override fun onDrawDoubleBufferedLayer(
+                eglManager: EGLManager,
+                bufferWidth: Int,
+                bufferHeight: Int,
+                transform: FloatArray,
+                params: Collection<Any>
+            ) {
+                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                Rectangle().draw(mProjectionMatrix, Color.BLUE, 0f, 0f, 100f, 100f)
+            }
+
+            override fun onFrontBufferedLayerRenderComplete(
+                frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+                transaction: SurfaceControlCompat.Transaction
+            ) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                    transaction.addTransactionCommittedListener(
+                        Executors.newSingleThreadExecutor(),
+                        object : SurfaceControlCompat.TransactionCommittedListener {
+                            override fun onTransactionCommitted() {
+                                renderLatch.countDown()
+                            }
+                        }
+                    )
+                } else {
+                    renderLatch.countDown()
+                }
+            }
+        }
+        var renderer: GLFrontBufferedRenderer<Any>? = null
+        var surfaceView: SurfaceView? = null
+        try {
+            val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+                .moveToState(Lifecycle.State.CREATED)
+                .onActivity {
+                    surfaceView = it.getSurfaceView()
+                    renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+                }
+
+            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+                renderer?.renderFrontBufferedLayer(Any())
+            }
+            // Navigate to stopped and resumed state to simulate returning to the application
+            scenario.moveToState(Lifecycle.State.CREATED)
+                .moveToState(Lifecycle.State.RESUMED)
+                .onActivity {
+                    renderer?.renderFrontBufferedLayer(Any())
+                }
+            assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val coords = IntArray(2)
+            val width: Int
+            val height: Int
+            with(surfaceView!!) {
+                getLocationOnScreen(coords)
+                width = this.width
+                height = this.height
+            }
+
+            SurfaceControlUtils.validateOutput { bitmap ->
+                Color.RED ==
+                    bitmap.getPixel(coords[0] + width / 2, coords[1] + height / 2)
+            }
+        } finally {
+            renderer.blockingRelease()
+        }
+    }
+
     @RequiresApi(Build.VERSION_CODES.Q)
     private fun GLFrontBufferedRenderer<*>?.blockingRelease(timeoutMillis: Long = 3000) {
         if (this != null) {
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLUtils.kt
similarity index 67%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
copy to graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLUtils.kt
index 79122bf..04d04be 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLUtils.kt
@@ -14,10 +14,15 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler.generator
+package androidx.graphics.lowlatency
 
-import com.squareup.kotlinpoet.FileSpec
-import java.io.OutputStream
+import android.opengl.GLES20
 
-/** Convenience method to write [FileSpec]s to KSP-generated [OutputStream]s. */
-internal fun OutputStream.write(spec: FileSpec) = bufferedWriter().use(spec::writeTo)
+fun loadShader(type: Int, shaderCode: String?): Int {
+    val shader = GLES20.glCreateShader(type)
+
+    GLES20.glShaderSource(shader, shaderCode)
+    GLES20.glCompileShader(shader)
+
+    return shader
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
index 9cd1708..0e5642c 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
@@ -59,6 +59,8 @@
         private val mMVPMatrix = FloatArray(16)
         private val mProjection = FloatArray(16)
 
+        private val mSceneParams = ArrayList<FloatArray>()
+
         override fun onDrawFrontBufferedLayer(
             eglManager: EGLManager,
             bufferWidth: Int,
@@ -86,7 +88,7 @@
                 drawLines(mProjection, floatArrayOf(0f, vHeight, vWidth, vHeight), Color.CYAN)
                 drawLines(mProjection, floatArrayOf(vWidth, vHeight, vWidth, 0f), Color.BLUE)
                 drawLines(mProjection, floatArrayOf(vWidth, 0f, 0f, 0f), Color.MAGENTA)
-                drawLines(mProjection, param, Color.RED, 20f)
+                drawLines(mProjection, param, Color.YELLOW, LINE_WIDTH)
             }
         }
 
@@ -98,6 +100,8 @@
             params: Collection<FloatArray>
         ) {
             GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+            GLES20.glClearColor(0f, 0f, 0f, 0f)
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
             Matrix.orthoM(
                 mMVPMatrix,
                 0,
@@ -109,8 +113,9 @@
                 1f
             )
             Matrix.multiplyMM(mProjection, 0, mMVPMatrix, 0, transform, 0)
-            for (line in params) {
-                obtainRenderer().drawLines(mProjection, line, Color.BLUE, 20f)
+            mSceneParams.addAll(params)
+            for (line in mSceneParams) {
+                obtainRenderer().drawLines(mProjection, line, Color.BLUE, LINE_WIDTH)
             }
         }
     }
@@ -171,4 +176,8 @@
         }
         super.onDetachedFromWindow()
     }
+
+    private companion object {
+        private const val LINE_WIDTH = 5f
+    }
 }
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LineRenderer.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LineRenderer.kt
index e50d0ca56..363422b 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LineRenderer.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LineRenderer.kt
@@ -156,14 +156,5 @@
                     gl_FragColor = $vColor;
                 }
             """
-
-        fun loadShader(type: Int, shaderCode: String?): Int {
-            val shader = GLES20.glCreateShader(type)
-
-            GLES20.glShaderSource(shader, shaderCode)
-            GLES20.glCompileShader(shader)
-
-            return shader
-        }
     }
 }
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/ParamQueueTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/ParamQueueTest.kt
new file mode 100644
index 0000000..5ac0c6c
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/ParamQueueTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.lowlatency
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+internal class ParamQueueTest {
+
+    @Test
+    fun testAdd() {
+        val queue = ParamQueue<Int>()
+        queue.add(1)
+        queue.next {
+            assertEquals(1, it)
+        }
+    }
+
+    @Test
+    fun testNextWithEmptyQueue() {
+        var blockInvoked = false
+        ParamQueue<Int>().next { blockInvoked = true }
+        // empty queue should not call this lambda
+        assertFalse(blockInvoked)
+    }
+
+    @Test
+    fun testRelease() {
+        val queue = ParamQueue<Int>()
+        with(queue) {
+            for (i in 1 until 5) {
+                add(i)
+            }
+        }
+        val list = queue.release()
+        var count = 1
+        for (entry in list) {
+            assertEquals(count++, entry)
+        }
+    }
+
+    @Test
+    fun testClear() {
+        with(ParamQueue<Int>()) {
+            for (i in 0 until 2) {
+                add(i)
+            }
+            clear()
+            val list = release()
+            assertTrue(list.isEmpty())
+        }
+    }
+
+    @Test
+    fun testNext() {
+        with(ParamQueue<Int>()) {
+            for (i in 0 until 10) {
+                add(i)
+            }
+            var count = 0
+            for (i in 0 until 10) {
+                next { it ->
+                    assertEquals(i, it)
+                    count++
+                }
+            }
+            assertEquals(10, count)
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/Rectangle.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/Rectangle.kt
new file mode 100644
index 0000000..2b8b469
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/Rectangle.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.graphics.lowlatency
+
+import android.graphics.Color
+import android.opengl.GLES20
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+import java.nio.ShortBuffer
+
+class Rectangle {
+
+    private val mVertexBuffer: FloatBuffer
+    private val mDrawListBuffer: ShortBuffer
+    private val mProgram: Int
+    private var mPositionHandle = 0
+    private var mColorHandle = 0
+    private var mMVPMatrixHandle = 0
+    private var mColor = floatArrayOf(1f, 0f, 0f, 1f)
+    private var mSquareCoords = FloatArray(12)
+
+    /**
+     * Sets up the drawing object data for use in an OpenGL ES context.
+     */
+    init {
+        // initialize vertex byte buffer for shape coordinates
+        val bb = ByteBuffer.allocateDirect( // (# of coordinate values * 4 bytes per float)
+            mSquareCoords.size * 4
+        )
+        bb.order(ByteOrder.nativeOrder())
+        mVertexBuffer = bb.asFloatBuffer()
+        mVertexBuffer.put(mSquareCoords)
+        mVertexBuffer.position(0)
+        // initialize byte buffer for the draw list
+        val dlb = ByteBuffer.allocateDirect( // (# of coordinate values * 2 bytes per short)
+            DRAW_ORDER.size * 2
+        )
+        dlb.order(ByteOrder.nativeOrder())
+        mDrawListBuffer = dlb.asShortBuffer()
+        mDrawListBuffer.put(DRAW_ORDER)
+        mDrawListBuffer.position(0)
+        // prepare shaders and OpenGL program
+        val vertexShader = loadShader(
+            GLES20.GL_VERTEX_SHADER,
+            vertexShaderCode
+        )
+        val fragmentShader = loadShader(
+            GLES20.GL_FRAGMENT_SHADER,
+            fragmentShaderCode
+        )
+        mProgram = GLES20.glCreateProgram() // create empty OpenGL Program
+        GLES20.glAttachShader(mProgram, vertexShader) // add the vertex shader to program
+        GLES20.glAttachShader(mProgram, fragmentShader) // add the fragment shader to program
+        GLES20.glLinkProgram(mProgram) // create OpenGL program executables
+    }
+
+    fun draw(
+        mvpMatrix: FloatArray?,
+        color: Int,
+        left: Float,
+        top: Float,
+        right: Float,
+        bottom: Float
+    ) {
+
+        mColor[0] = Color.red(color) / 255f
+        mColor[1] = Color.green(color) / 255f
+        mColor[2] = Color.blue(color) / 255f
+        mColor[3] = Color.alpha(color) / 255f
+
+        // top left
+        mSquareCoords[0] = left
+        mSquareCoords[1] = top
+        mSquareCoords[2] = 0f
+        // bottom left
+        mSquareCoords[3] = left
+        mSquareCoords[4] = bottom
+        mSquareCoords[5] = 0f
+        // bottom right
+        mSquareCoords[6] = right
+        mSquareCoords[7] = bottom
+        mSquareCoords[8] = 0f
+        // top right
+        mSquareCoords[9] = right
+        mSquareCoords[10] = top
+        mSquareCoords[11] = 0f
+
+        mVertexBuffer.clear()
+        mVertexBuffer.put(mSquareCoords)
+        mVertexBuffer.position(0)
+
+        GLES20.glUseProgram(mProgram)
+        // get handle to vertex shader's vPosition member
+        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition")
+        // Enable a handle to the triangle vertices
+        GLES20.glEnableVertexAttribArray(mPositionHandle)
+        // Prepare the triangle coordinate data
+        // 4 bytes per vertex
+        val vertexStride = COORDS_PER_VERTEX * 4
+        GLES20.glVertexAttribPointer(
+            mPositionHandle, COORDS_PER_VERTEX,
+            GLES20.GL_FLOAT, false,
+            vertexStride, mVertexBuffer
+        )
+        // get handle to fragment shader's vColor member
+        mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor")
+        // Set color for drawing the triangle
+        GLES20.glUniform4fv(mColorHandle, 1, mColor, 0)
+        // get handle to shape's transformation matrix
+        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix")
+        // Apply the projection and view transformation
+        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0)
+        // Draw the square
+        GLES20.glDrawElements(
+            GLES20.GL_TRIANGLES, DRAW_ORDER.size,
+            GLES20.GL_UNSIGNED_SHORT, mDrawListBuffer
+        )
+        // Disable vertex array
+        GLES20.glDisableVertexAttribArray(mPositionHandle)
+    }
+
+    companion object {
+        private val vertexShaderCode =
+            """
+                uniform mat4 uMVPMatrix;
+                attribute vec4 vPosition;
+                void main() {
+                  gl_Position = uMVPMatrix * vPosition;
+                }
+            """
+        private val fragmentShaderCode =
+            """
+                precision mediump float;
+                uniform vec4 vColor;
+                void main() {
+                  gl_FragColor = vColor;
+                }
+            """
+
+        // number of coordinates per vertex in this array
+        val COORDS_PER_VERTEX = 3
+        val DRAW_ORDER = shortArrayOf(0, 1, 2, 0, 2, 3) // order to draw vertices
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
index 03eec57..313ac2d 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
@@ -40,10 +40,10 @@
 import androidx.graphics.lowlatency.FrameBufferRenderer
 import androidx.graphics.lowlatency.FrameBuffer
 import androidx.graphics.lowlatency.LineRenderer
+import androidx.graphics.lowlatency.Rectangle
 import androidx.graphics.lowlatency.SyncFenceCompat
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
-import androidx.graphics.opengl.egl.deviceSupportsNativeAndroidFence
 import androidx.graphics.opengl.egl.supportsNativeAndroidFence
 import androidx.lifecycle.Lifecycle.State
 import androidx.test.core.app.ActivityScenario
@@ -800,13 +800,6 @@
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     fun testFrontBufferedRenderer() {
-        if (!deviceSupportsNativeAndroidFence()) {
-            // If the Android device does not support the corresponding
-            // EGL Extensions to obtain native Android fence objects from EGLSync
-            // instances then skip this test as we cannot guarantee consistency
-            // for front buffered rendering
-            return
-        }
         val width = 10
         val height = 10
         val renderLatch = CountDownLatch(1)
@@ -814,8 +807,12 @@
         val glRenderer = GLRenderer().apply { start() }
         var frameBuffer: FrameBuffer? = null
         var status: Boolean? = false
+        var supportsFence = false
 
         val callbacks = object : FrameBufferRenderer.RenderCallback {
+
+            private val mOrthoMatrix = FloatArray(16)
+
             override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer =
                 FrameBuffer(
                     egl,
@@ -829,9 +826,26 @@
                 ).also { frameBuffer = it }
 
             override fun onDraw(eglManager: EGLManager) {
-                GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
-                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
-                GLES20.glFlush()
+                GLES20.glViewport(0, 0, width, height)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    width.toFloat(),
+                    0f,
+                    height.toFloat(),
+                    -1f,
+                    1f
+                )
+                Rectangle().draw(
+                    mOrthoMatrix,
+                    Color.RED,
+                    0f,
+                    0f,
+                    width.toFloat(),
+                    height.toFloat()
+                )
+                supportsFence = eglManager.supportsNativeAndroidFence()
             }
 
             override fun onDrawComplete(
@@ -852,9 +866,11 @@
         var hardwareBuffer: HardwareBuffer? = null
         try {
             assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
-            assert(status != null)
-            status?.let {
-                assertTrue(it)
+            if (supportsFence) {
+                assert(status != null)
+                status?.let {
+                    assertTrue(it)
+                }
             }
 
             hardwareBuffer = frameBuffer?.hardwareBuffer
@@ -883,13 +899,6 @@
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     fun testFrameBufferRendererWithSyncFence() {
-        if (!deviceSupportsNativeAndroidFence()) {
-            // If the Android device does not support the corresponding
-            // EGL Extensions to obtain native Android fence objects from EGLSync
-            // instances then skip this test as we cannot guarantee consistency
-            // for front buffered rendering
-            return
-        }
 
         val width = 10
         val height = 10
@@ -900,6 +909,7 @@
         var startTime = Long.MAX_VALUE
         var signalTime = 0L
 
+        var supportsFence = false
         val renderer =
             object : FrameBufferRenderer.RenderCallback, GLRenderer.EGLContextCallback {
                 private val mMVPMatrix = FloatArray(16)
@@ -947,6 +957,8 @@
                     mLines[3] = 5f
                     mLineRenderer.drawLines(mMVPMatrix, mLines)
                     assertEquals(GLES20.GL_NO_ERROR, GLES20.glGetError())
+
+                    supportsFence = eglManager.supportsNativeAndroidFence()
                 }
 
                 @WorkerThread
@@ -954,17 +966,18 @@
                     frameBuffer: FrameBuffer,
                     syncFenceCompat: SyncFenceCompat?
                 ) {
-                    assertNotNull(syncFenceCompat)
-                    assertTrue(syncFenceCompat!!.isValid())
+                    if (supportsFence) {
+                        assertNotNull(syncFenceCompat)
+                        assertTrue(syncFenceCompat!!.isValid())
+                        assertTrue(syncFenceCompat.await(3000))
+                        signalTime = syncFenceCompat.getSignalTimeNanos()
+
+                        assertTrue(syncFenceCompat.getSignalTimeNanos() < System.nanoTime())
+                        assertTrue(syncFenceCompat.getSignalTimeNanos() > startTime)
+                    }
+                    renderLatch.countDown()
 
                     assertEquals(GLES20.GL_NO_ERROR, GLES20.glGetError())
-
-                    assertTrue(syncFenceCompat.await(3000))
-                    signalTime = syncFenceCompat.getSignalTimeNanos()
-
-                    renderLatch.countDown()
-                    assertTrue(syncFenceCompat.getSignalTimeNanos() < System.nanoTime())
-                    assertTrue(syncFenceCompat.getSignalTimeNanos() > startTime)
                 }
             }
 
@@ -978,8 +991,10 @@
 
         try {
             assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
-            assertTrue(startTime < signalTime)
-            assertTrue(signalTime < System.nanoTime())
+            if (supportsFence) {
+                assertTrue(startTime < signalTime)
+                assertTrue(signalTime < System.nanoTime())
+            }
         } finally {
             glRenderer.stop(true) {
                 teardownLatch.countDown()
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
index f5b3c2a..bf68dd4 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
@@ -28,6 +28,7 @@
 import androidx.graphics.opengl.GLRenderer
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
+import androidx.opengl.EGLExt
 import java.util.concurrent.atomic.AtomicBoolean
 import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
 import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
@@ -71,16 +72,40 @@
 
             syncFenceCompat = if (eglManager.supportsNativeAndroidFence()) {
                 syncStrategy.createSyncFence(egl)
+            } else if (eglManager.isExtensionSupported(EGL_KHR_FENCE_SYNC)) {
+                // In this case the device only supports EGL sync objects but not creation
+                // of native SyncFence objects from an EGLSync.
+                // This usually occurs in emulator/cuttlefish instances as well as ChromeOS devices
+                // running ARC++. In this case fallback onto creating a sync object and waiting
+                // on it instead.
+                // TODO b/256217036 block on another thread instead of waiting here
+                val syncKhr = egl.eglCreateSyncKHR(EGLExt.EGL_SYNC_FENCE_KHR, null)
+                if (syncKhr != null) {
+                    GLES20.glFlush()
+                    val status = egl.eglClientWaitSyncKHR(
+                        syncKhr,
+                        EGLExt.EGL_SYNC_FLUSH_COMMANDS_BIT_KHR,
+                        EGLExt.EGL_FOREVER_KHR
+                    )
+                    if (status != EGLExt.EGL_CONDITION_SATISFIED_KHR) {
+                        Log.w(TAG, "warning waiting on sync object: $status")
+                    }
+                } else {
+                    Log.w(TAG, "Unable to create EGLSync")
+                    GLES20.glFinish()
+                }
+                null
             } else {
-                Log.w(TAG, "Device does not support creation of native fences")
+                Log.w(TAG, "Device does not support creation of any fences")
+                GLES20.glFinish()
                 null
             }
-
+        } catch (exception: Exception) {
+            Log.w(TAG, "Error attempting to render to frame buffer: ${exception.message}")
+        } finally {
             // At this point the HardwareBuffer has the contents of the GL rendering
             // Create a surface Control transaction to dispatch this request
             frameBufferRendererCallbacks.onDrawComplete(buffer, syncFenceCompat)
-        } finally {
-            syncFenceCompat?.close()
         }
     }
 
@@ -201,6 +226,7 @@
         return if (!isVisible) {
             eglSpec.createNativeSyncFence()
         } else if (supportsFrontBufferUsage) {
+            GLES20.glFlush()
             return null
         } else {
             val fence = eglSpec.createNativeSyncFence()
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index 1e64338d..1001bdb 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -31,8 +31,10 @@
 import androidx.graphics.surface.SurfaceControlCompat
 import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_270
 import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_90
-import java.util.Collections
+import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
+import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
 import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.Executors
 
 /**
  * Class responsible for supporting a "front buffered" rendering system. This allows for lower
@@ -81,6 +83,11 @@
             frontBufferedLayerSurfaceControl: SurfaceControlCompat,
             transaction: SurfaceControlCompat.Transaction
         ) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                transaction.addTransactionCommittedListener(mExecutor, mCommittedListener)
+            } else {
+                clearFrontBuffer()
+            }
             mFrontBufferSyncStrategy.isVisible = false
             callback.onDoubleBufferedLayerRenderComplete(
                 frontBufferedLayerSurfaceControl,
@@ -101,6 +108,19 @@
         }
     }
 
+    private val mExecutor = Executors.newSingleThreadExecutor()
+
+    private val mCommittedListener = object : SurfaceControlCompat.TransactionCommittedListener {
+        override fun onTransactionCommitted() {
+            clearFrontBuffer()
+        }
+    }
+
+    internal fun clearFrontBuffer() {
+        mFrontBufferedLayerRenderer?.clear()
+        mFrontBufferedRenderTarget?.requestRender()
+    }
+
     /**
      * [GLRenderer.EGLContextCallback]s used to release the corresponding [FrameBufferPool]
      * if the [GLRenderer] is torn down.
@@ -110,7 +130,12 @@
      */
     private val mContextCallbacks = object : GLRenderer.EGLContextCallback {
         override fun onEGLContextCreated(eglManager: EGLManager) {
-            // no-op
+            with(eglManager) {
+                val supportsEglFences = isExtensionSupported(EGL_KHR_FENCE_SYNC)
+                val supportsAndroidFences = isExtensionSupported(EGL_ANDROID_NATIVE_FENCE_SYNC)
+                Log.d(TAG, "Supports KHR_FENCE_SYNC: $supportsEglFences")
+                Log.d(TAG, "Supports ANDROID_NATIVE_FENCE_SYNC: $supportsAndroidFences")
+            }
         }
 
         override fun onEGLContextDestroyed(eglManager: EGLManager) {
@@ -130,11 +155,11 @@
         }
 
         override fun onLayerDestroyed() {
-            release(true)
+            detachTargets(true)
         }
 
-        override fun obtainDoubleBufferedLayerParams(): MutableCollection<T> =
-            mParentBufferParamQueue
+        override fun obtainDoubleBufferedLayerParams(): MutableCollection<T>? =
+            mSegments.poll()
 
         override fun getFrontBufferedLayerSurfaceControl(): SurfaceControlCompat? =
             mFrontBufferedLayerSurfaceControl
@@ -147,7 +172,7 @@
      * Queue of parameters to be consumed in [Callback.onDrawFrontBufferedLayer] with the parameter
      * provided in [renderFrontBufferedLayer]
      */
-    private val mFrontBufferQueueParams = ConcurrentLinkedQueue<T>()
+    private val mActiveSegment = ParamQueue<T>()
 
     /**
      * Collection of parameters to be consumed in [Callback.onDoubleBufferedLayerRenderComplete]
@@ -156,7 +181,7 @@
      * this collection is cleared and new parameters are added to it with consecutive calls to
      * [renderFrontBufferedLayer].
      */
-    private val mParentBufferParamQueue = Collections.synchronizedList(ArrayList<T>())
+    private val mSegments = ConcurrentLinkedQueue<MutableCollection<T>>()
 
     /**
      * [FrameBuffer] used for rendering into the front buffered layer. This buffer is persisted
@@ -257,8 +282,6 @@
         }
         renderer.registerEGLContextCallback(mContextCallbacks)
 
-        mDoubleBufferedLayerRenderTarget =
-            mParentRenderLayer.createRenderTarget(renderer, mCallback)
         mGLRenderer = renderer
 
         mHardwareBufferUsageFlags = obtainHardwareBufferUsageFlags()
@@ -267,7 +290,11 @@
     }
 
     internal fun update(width: Int, height: Int) {
-        if (mWidth != width || mHeight != height) {
+        if (mWidth != width || mHeight != height && isValid()) {
+
+            mDoubleBufferedLayerRenderTarget?.detach(true)
+            val doubleBufferTarget = mParentRenderLayer.createRenderTarget(mGLRenderer, mCallback)
+
             mFrontBufferedLayerSurfaceControl?.release()
 
             val frontBufferedSurfaceControl = SurfaceControlCompat.Builder()
@@ -326,6 +353,7 @@
 
             mFrontBufferedLayerRenderer = frontBufferedLayerRenderer
             mFrontBufferedLayerSurfaceControl = frontBufferedSurfaceControl
+            mDoubleBufferedLayerRenderTarget = doubleBufferTarget
             mBufferPool = bufferPool
             mWidth = width
             mHeight = height
@@ -357,8 +385,7 @@
      */
     fun renderFrontBufferedLayer(param: T) {
         if (isValid()) {
-            mFrontBufferQueueParams.add(param)
-            mParentBufferParamQueue.add(param)
+            mActiveSegment.add(param)
             mFrontBufferedRenderTarget?.requestRender()
         } else {
             Log.w(
@@ -389,9 +416,8 @@
      */
     fun commit() {
         if (isValid()) {
-            mFrontBufferQueueParams.clear()
+            mSegments.add(mActiveSegment.release())
             mDoubleBufferedLayerRenderTarget?.requestRender()
-            mFrontBufferedLayerRenderer?.clear()
         } else {
             Log.w(
                 TAG, "Attempt to render to the double buffered layer when " +
@@ -401,24 +427,10 @@
     }
 
     /**
-     * Releases the [GLFrontBufferedRenderer] and provides an optional callback that is invoked when
-     * the [GLFrontBufferedRenderer] is fully torn down. If the [cancelPending] flag is true, all
-     * pending requests to render into the front or double buffered layers will be processed before
-     * the [GLFrontBufferedRenderer] is torn down. Otherwise all in process requests are ignored.
-     * If the [GLFrontBufferedRenderer] is already released, that is [isValid] returns `false`, this
-     * method does nothing.
-     *
-     * @param cancelPending Flag indicating that requests to render should be processed before
-     * the [GLFrontBufferedRenderer] is released
-     * @param onReleaseComplete Optional callback invoked when the [GLFrontBufferedRenderer] has
-     * been released. This callback is invoked on the backing GLThread
+     * Helper method used to detach the front and multi buffered render targets as well as
+     * release SurfaceControl instances
      */
-    @JvmOverloads
-    fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)? = null) {
-        if (!isValid()) {
-            Log.w(TAG, "Attempt to release GLFrontbufferedRenderer that is already released")
-            return
-        }
+    internal fun detachTargets(cancelPending: Boolean, onReleaseComplete: (() -> Unit)? = null) {
         // Wrap the callback into a separate lambda to ensure it is invoked only after
         // both the front and double buffered layer target renderers are detached
         var callbackCount = 0
@@ -448,10 +460,35 @@
                 onReleaseComplete?.invoke()
             }
         }
+        mFrontBufferedLayerSurfaceControl = null
         mFrontBufferedRenderTarget?.detach(cancelPending, wrappedCallback)
         mDoubleBufferedLayerRenderTarget?.detach(cancelPending, wrappedCallback)
         mFrontBufferedRenderTarget = null
         mDoubleBufferedLayerRenderTarget = null
+        mWidth = -1
+        mHeight = -1
+    }
+
+    /**
+     * Releases the [GLFrontBufferedRenderer] and provides an optional callback that is invoked when
+     * the [GLFrontBufferedRenderer] is fully torn down. If the [cancelPending] flag is true, all
+     * pending requests to render into the front or double buffered layers will be processed before
+     * the [GLFrontBufferedRenderer] is torn down. Otherwise all in process requests are ignored.
+     * If the [GLFrontBufferedRenderer] is already released, that is [isValid] returns `false`, this
+     * method does nothing.
+     *
+     * @param cancelPending Flag indicating that requests to render should be processed before
+     * the [GLFrontBufferedRenderer] is released
+     * @param onReleaseComplete Optional callback invoked when the [GLFrontBufferedRenderer] has
+     * been released. This callback is invoked on the backing GLThread
+     */
+    @JvmOverloads
+    fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)? = null) {
+        if (!isValid()) {
+            Log.w(TAG, "Attempt to release GLFrontbufferedRenderer that is already released")
+            return
+        }
+        detachTargets(cancelPending, onReleaseComplete)
 
         mGLRenderer.unregisterEGLContextCallback(mContextCallbacks)
         if (mIsManagingGLRenderer) {
@@ -463,8 +500,7 @@
             mGLRenderer.stop(false)
         }
 
-        mFrontBufferedLayerSurfaceControl = null
-        mParentRenderLayer.setParentLayerCallbacks(null)
+        mExecutor.shutdown()
         mIsReleased = true
     }
 
@@ -508,24 +544,14 @@
 
                 @WorkerThread
                 override fun onDraw(eglManager: EGLManager) {
-                    try {
-                        // Explicitly call remove in order to delineate between scenarios where
-                        // no parameters are provided and the consumer explicitly supports nullable
-                        // parameters.
-                        // If poll was used instead, we would not be able to determine if the nullable
-                        // parameter was because there were no items in the queue or the consumer
-                        // explicitly provided null as a placeholder
+                    mActiveSegment.next { param ->
                         mCallback.onDrawFrontBufferedLayer(
                             eglManager,
                             mBufferTransform.glWidth,
                             mBufferTransform.glHeight,
                             mBufferTransform.transform,
-                            mFrontBufferQueueParams.remove()
+                            param
                         )
-                    } catch (_: NoSuchElementException) {
-                        // Skip rendering if we have been told to render but we do not have parameters
-                        // Because the call to render to the front buffer takes in a parameter we should
-                        // not run into this scenario.
                     }
                 }
 
@@ -564,8 +590,8 @@
     }
 
     private fun clearParamQueues() {
-        mFrontBufferQueueParams.clear()
-        mParentBufferParamQueue.clear()
+        mActiveSegment.clear()
+        mSegments.clear()
     }
 
     /**
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt
new file mode 100644
index 0000000..a0b1765
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.graphics.lowlatency
+
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * Thread-safe class responsible for maintaining a list of objects as well as an index
+ * that keeps track of the last consumed object within the queue.
+ * This ensures that the collection as well as the current index parameter are updated atomically
+ * within the same lock.
+ */
+internal class ParamQueue<T> {
+
+    private val mLock = ReentrantLock()
+    private var mParams = ArrayList<T>()
+    private var mIndex = 0
+
+    /**
+     * Clears the parameter queue and resets the index position to 0
+     */
+    fun clear() {
+        mLock.withLock {
+            mIndex = 0
+            mParams.clear()
+        }
+    }
+
+    /**
+     * Returns the current queue and resets the index position to 0.
+     * This collection is no longer owned by [ParamQueue] and a new queue is maintained after
+     * this call is made. This allows callers to manipulate the returned collection as they
+     * see fit without impacting the integrity of the [ParamQueue] after this method is invoked.
+     */
+    fun release(): MutableCollection<T> {
+        mLock.withLock {
+            val result = mParams
+            mParams = ArrayList<T>()
+            mIndex = 0
+            return result
+        }
+    }
+
+    /**
+     * Returns the next parameter at the index position and increments the index position.
+     * This parameter is provided as a callback. If the index position is out of range, this
+     * method acts as a no-op.
+     * This does not actually remove the parameter from the collection. Consumers must either
+     * call [clear], or clear the collection returned in [release] to ensure contents do not
+     * grow unbounded.
+     */
+    inline fun next(block: (T) -> Unit) {
+        mLock.withLock {
+            if (mIndex < mParams.size) {
+                val param = mParams[mIndex++]
+                block(param)
+            }
+        }
+    }
+
+    /**
+     * Adds a new entry into the parameter queue. This leaves the current index position unchanged.
+     */
+    fun add(param: T) {
+        mLock.withLock {
+            mParams.add(param)
+        }
+    }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
index 20e4d7b..b217dd9 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
@@ -104,12 +104,14 @@
 
         /**
          * Callback invoked by the [ParentRenderLayer] to query the parameters since the last
-         * render to the dry layer. This includes all parameters to each request to render content
-         * to the front buffered layer since the last time the dry layer was re-rendered.
+         * render to the multi-buffered layer. This includes all parameters to each request to
+         * render content to the front buffered layer since the last time the dry layer was
+         * re-rendered.
          * This is useful for recreating the entire scene when front buffered layer contents are to
          * be committed, that is the entire scene is re-rendered into the double buffered layer.
+         * This can return null if all the double buffered params have already been queried.
          */
-        fun obtainDoubleBufferedLayerParams(): MutableCollection<T>
+        fun obtainDoubleBufferedLayerParams(): MutableCollection<T>?
 
         /**
          * Obtain a handle to the front buffered layer [SurfaceControlCompat] to be used in
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
index 4ce82fe..c31a83f 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
@@ -45,6 +45,35 @@
 
     private val mTransformResolver = BufferTransformHintResolver()
 
+    private var transformHint = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+    private var inverse = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+    init {
+        surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
+
+            override fun surfaceCreated(holder: SurfaceHolder) {
+                // NO-OP wait on surfaceChanged callback
+            }
+
+            override fun surfaceChanged(
+                holder: SurfaceHolder,
+                format: Int,
+                width: Int,
+                height: Int
+            ) {
+                transformHint = getBufferTransformHint()
+                inverse = mBufferTransform.invertBufferTransform(transformHint)
+                mBufferTransform.computeTransform(width, height, inverse)
+                mParentSurfaceControl?.release()
+                mLayerCallback?.onSizeChanged(width, height)
+                mParentSurfaceControl = createDoubleBufferedSurfaceControl()
+            }
+
+            override fun surfaceDestroyed(p0: SurfaceHolder) {
+                mLayerCallback?.onLayerDestroyed()
+            }
+        })
+    }
+
     override fun getBufferTransformHint(): Int {
         return mTransformResolver.getBufferTransformHint(surfaceView)
     }
@@ -65,8 +94,7 @@
         renderer: GLRenderer,
         renderLayerCallback: GLFrontBufferedRenderer.Callback<T>
     ): GLRenderer.RenderTarget {
-        var transformHint = BufferTransformHintResolver.UNKNOWN_TRANSFORM
-        var inverse = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+        var params: Collection<T>? = null
         val frameBufferRenderer = FrameBufferRenderer(
             object : FrameBufferRenderer.RenderCallback {
 
@@ -75,7 +103,6 @@
                         ?: throw IllegalArgumentException("No FrameBufferPool available")
 
                 override fun onDraw(eglManager: EGLManager) {
-                    val params = mLayerCallback?.obtainDoubleBufferedLayerParams()
                     renderLayerCallback.onDrawDoubleBufferedLayer(
                         eglManager,
                         mBufferTransform.glWidth,
@@ -125,41 +152,25 @@
                     }
                 }
             })
-        surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
-
-            override fun surfaceCreated(holder: SurfaceHolder) {
-                // NO-OP wait on surfaceChanged callback
-            }
-
-            override fun surfaceChanged(
-                holder: SurfaceHolder,
-                format: Int,
-                width: Int,
-                height: Int
-            ) {
-                transformHint = getBufferTransformHint()
-                inverse = mBufferTransform.invertBufferTransform(transformHint)
-                mBufferTransform.computeTransform(width, height, inverse)
-                mParentSurfaceControl?.release()
-                mLayerCallback?.onSizeChanged(width, height)
-                mParentSurfaceControl = createDoubleBufferedSurfaceControl()
-            }
-
-            override fun surfaceDestroyed(p0: SurfaceHolder) {
-                mLayerCallback?.onLayerDestroyed()
-            }
-        })
-        val renderTarget = renderer.attach(surfaceView, frameBufferRenderer)
+        val parentFrameBufferRenderer = WrapperFrameBufferRenderer<T>(frameBufferRenderer) {
+            params = mLayerCallback?.obtainDoubleBufferedLayerParams()
+            params != null
+        }
+        val renderTarget = renderer.attach(surfaceView, parentFrameBufferRenderer)
         mRenderTarget = renderTarget
         mFrameBufferRenderer = frameBufferRenderer
         return renderTarget
     }
 
-    internal fun createDoubleBufferedSurfaceControl(): SurfaceControlCompat =
-        SurfaceControlCompat.Builder()
+    internal fun createDoubleBufferedSurfaceControl(): SurfaceControlCompat {
+        val surfaceControl = SurfaceControlCompat.Builder()
             .setParent(surfaceView)
             .setName("DoubleBufferedLayer")
             .build()
+        // SurfaceControl is not visible by default so make it visible right after creation
+        SurfaceControlCompat.Transaction().setVisibility(surfaceControl, true).commit()
+        return surfaceControl
+    }
 
     override fun setParentLayerCallbacks(callback: ParentRenderLayer.Callback<T>?) {
         mLayerCallback = callback
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
new file mode 100644
index 0000000..005b06d
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.lowlatency
+
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.os.Build
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.GLRenderer
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+
+/**
+ * Wrapper Renderer around [FrameBufferRenderer] that skips rendering on a given condition
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+internal class WrapperFrameBufferRenderer<T>(
+    private val frameBufferRenderer: FrameBufferRenderer,
+    private val shouldRender: () -> Boolean
+) : GLRenderer.RenderCallback {
+
+    override fun onSurfaceCreated(
+        spec: EGLSpec,
+        config: EGLConfig,
+        surface: Surface,
+        width: Int,
+        height: Int
+    ): EGLSurface? = frameBufferRenderer.onSurfaceCreated(spec, config, surface, width, height)
+
+    override fun onDrawFrame(eglManager: EGLManager) {
+        if (shouldRender()) {
+            frameBufferRenderer.onDrawFrame(eglManager)
+        }
+    }
+}
\ No newline at end of file
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
index a3806cb..97541f7 100644
--- a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
@@ -51,7 +51,7 @@
    * API version of the AIDL interface. Should be incremented every time a new
    * method is added.
    */
-  const int CURRENT_API_VERSION = 4;
+  const int CURRENT_API_VERSION = 5;
 
   const int MIN_API_VERSION = 1;
 
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index b3a6483..e80113c 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -16,7 +16,58 @@
 package androidx.health.connect.client.permission
 
 import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
+import androidx.health.connect.client.records.BasalBodyTemperatureRecord
+import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.BloodGlucoseRecord
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.BodyFatRecord
+import androidx.health.connect.client.records.BodyTemperatureRecord
+import androidx.health.connect.client.records.BodyWaterMassRecord
+import androidx.health.connect.client.records.BoneMassRecord
+import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
+import androidx.health.connect.client.records.DistanceRecord
+import androidx.health.connect.client.records.ElevationGainedRecord
+import androidx.health.connect.client.records.ExerciseEventRecord
+import androidx.health.connect.client.records.ExerciseLapRecord
+import androidx.health.connect.client.records.ExerciseRepetitionsRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.FloorsClimbedRecord
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
+import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
+import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
+import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
+import androidx.health.connect.client.records.HeightRecord
+import androidx.health.connect.client.records.HipCircumferenceRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MenstruationFlowRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.OvulationTestRecord
+import androidx.health.connect.client.records.OxygenSaturationRecord
+import androidx.health.connect.client.records.PowerRecord
 import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.records.RespiratoryRateRecord
+import androidx.health.connect.client.records.RestingHeartRateRecord
+import androidx.health.connect.client.records.SexualActivityRecord
+import androidx.health.connect.client.records.SleepSessionRecord
+import androidx.health.connect.client.records.SleepStageRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.SwimmingStrokesRecord
+import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
+import androidx.health.connect.client.records.Vo2MaxRecord
+import androidx.health.connect.client.records.WaistCircumferenceRecord
+import androidx.health.connect.client.records.WeightRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
 import kotlin.reflect.KClass
 
 /**
@@ -44,16 +95,52 @@
         }
 
         /**
+         * Returns a permission defined in [HealthPermission] to read provided [recordType], such as
+         * `Steps::class`.
+         *
+         * @return Permission to use with [androidx.health.connect.client.PermissionController].
+         * @throws IllegalArgumentException if the given record type is invalid.
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
+        @JvmStatic
+        public fun createReadPermissionInternal(recordType: KClass<out Record>): String {
+            if (RECORD_TYPE_TO_PERMISSION[recordType] == null) {
+                throw IllegalArgumentException(
+                    "Given recordType is not valid : $recordType.simpleName"
+                )
+            }
+            return READ_PERMISSION_PREFIX + RECORD_TYPE_TO_PERMISSION[recordType]
+        }
+
+        /**
          * Creates [HealthPermission] to write provided [recordType], such as `Steps::class`.
          *
-         * @return Permission object to use with
-         * [androidx.health.connect.client.PermissionController].
+         * @return Permission to use with [androidx.health.connect.client.PermissionController].
          */
         @JvmStatic
         public fun createWritePermission(recordType: KClass<out Record>): HealthPermission {
             return HealthPermission(recordType, AccessTypes.WRITE)
         }
 
+        /**
+         * Returns a permission defined in [HealthPermission] to read provided [recordType], such as
+         * `Steps::class`.
+         *
+         * @return Permission object to use with
+         * [androidx.health.connect.client.PermissionController].
+         * @throws IllegalArgumentException if the given record type is invalid.
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
+        @JvmStatic
+        public fun createWritePermissionInternal(recordType: KClass<out Record>): String {
+            if (RECORD_TYPE_TO_PERMISSION[recordType] == null) {
+                throw IllegalArgumentException(
+                    "Given recordType is not valid : $recordType.simpleName"
+                )
+            }
+            return WRITE_PERMISSION_PREFIX + RECORD_TYPE_TO_PERMISSION.getOrDefault(recordType, "")
+        }
+
         // Read permissions for ACTIVITY.
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         const val READ_ACTIVE_CALORIES_BURNED =
@@ -230,6 +317,99 @@
         const val WRITE_RESPIRATORY_RATE = "android.permission.health.WRITE_RESPIRATORY_RATE"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         const val WRITE_RESTING_HEART_RATE = "android.permission.health.WRITE_RESTING_HEART_RATE"
+
+        private const val READ_PERMISSION_PREFIX = "android.permission.health.READ_"
+
+        private const val WRITE_PERMISSION_PREFIX = "android.permission.health.WRITE_"
+
+        private val RECORD_TYPE_TO_PERMISSION =
+            mapOf<KClass<out Record>, String>(
+                ActiveCaloriesBurnedRecord::class to
+                    READ_ACTIVE_CALORIES_BURNED.substringAfter(READ_PERMISSION_PREFIX),
+                BasalBodyTemperatureRecord::class to
+                    READ_BASAL_BODY_TEMPERATURE.substringAfter(READ_PERMISSION_PREFIX),
+                BasalMetabolicRateRecord::class to
+                    READ_BASAL_METABOLIC_RATE.substringAfter(READ_PERMISSION_PREFIX),
+                BloodGlucoseRecord::class to
+                    READ_BLOOD_GLUCOSE.substringAfter(READ_PERMISSION_PREFIX),
+                BloodPressureRecord::class to
+                    READ_BLOOD_PRESSURE.substringAfter(READ_PERMISSION_PREFIX),
+                BodyFatRecord::class to READ_BODY_FAT.substringAfter(READ_PERMISSION_PREFIX),
+                BodyTemperatureRecord::class to
+                    READ_BODY_TEMPERATURE.substringAfter(READ_PERMISSION_PREFIX),
+                BodyWaterMassRecord::class to
+                    READ_BODY_WATER_MASS.substringAfter(READ_PERMISSION_PREFIX),
+                BoneMassRecord::class to READ_BONE_MASS.substringAfter(READ_PERMISSION_PREFIX),
+                CervicalMucusRecord::class to
+                    READ_CERVICAL_MUCUS.substringAfter(READ_PERMISSION_PREFIX),
+                CyclingPedalingCadenceRecord::class to
+                    READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+                DistanceRecord::class to READ_DISTANCE.substringAfter(READ_PERMISSION_PREFIX),
+                ElevationGainedRecord::class to
+                    READ_ELEVATION_GAINED.substringAfter(READ_PERMISSION_PREFIX),
+                ExerciseEventRecord::class to READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+                ExerciseLapRecord::class to READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+                ExerciseRepetitionsRecord::class to
+                    READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+                ExerciseSessionRecord::class to
+                    READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+                FloorsClimbedRecord::class to
+                    READ_FLOORS_CLIMBED.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateRecord::class to READ_HEART_RATE.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilityDifferentialIndexRecord::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilityRmssdRecord::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilitySd2Record::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilitySdannRecord::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilitySdnnIndexRecord::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilitySdnnRecord::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilitySdsdRecord::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilitySRecord::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeartRateVariabilityTinnRecord::class to
+                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+                HeightRecord::class to READ_HEIGHT.substringAfter(READ_PERMISSION_PREFIX),
+                HipCircumferenceRecord::class to
+                    READ_HIP_CIRCUMFERENCE.substringAfter(READ_PERMISSION_PREFIX),
+                HydrationRecord::class to READ_HYDRATION.substringAfter(READ_PERMISSION_PREFIX),
+                LeanBodyMassRecord::class to
+                    READ_LEAN_BODY_MASS.substringAfter(READ_PERMISSION_PREFIX),
+                MenstruationFlowRecord::class to
+                    READ_MENSTRUATION.substringAfter(READ_PERMISSION_PREFIX),
+                NutritionRecord::class to READ_NUTRITION.substringAfter(READ_PERMISSION_PREFIX),
+                OvulationTestRecord::class to
+                    READ_OVULATION_TEST.substringAfter(READ_PERMISSION_PREFIX),
+                OxygenSaturationRecord::class to
+                    READ_OXYGEN_SATURATION.substringAfter(READ_PERMISSION_PREFIX),
+                PowerRecord::class to READ_POWER.substringAfter(READ_PERMISSION_PREFIX),
+                RespiratoryRateRecord::class to
+                    READ_RESPIRATORY_RATE.substringAfter(READ_PERMISSION_PREFIX),
+                RestingHeartRateRecord::class to
+                    READ_RESTING_HEART_RATE.substringAfter(READ_PERMISSION_PREFIX),
+                SexualActivityRecord::class to
+                    READ_SEXUAL_ACTIVITY.substringAfter(READ_PERMISSION_PREFIX),
+                SleepSessionRecord::class to READ_SLEEP.substringAfter(READ_PERMISSION_PREFIX),
+                SleepStageRecord::class to READ_SLEEP.substringAfter(READ_PERMISSION_PREFIX),
+                SpeedRecord::class to READ_SPEED.substringAfter(READ_PERMISSION_PREFIX),
+                StepsCadenceRecord::class to READ_STEPS.substringAfter(READ_PERMISSION_PREFIX),
+                StepsRecord::class to READ_STEPS.substringAfter(READ_PERMISSION_PREFIX),
+                SwimmingStrokesRecord::class to
+                    READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+                TotalCaloriesBurnedRecord::class to
+                    READ_TOTAL_CALORIES_BURNED.substringAfter(READ_PERMISSION_PREFIX),
+                Vo2MaxRecord::class to READ_VO2_MAX.substringAfter(READ_PERMISSION_PREFIX),
+                WaistCircumferenceRecord::class to
+                    READ_WAIST_CIRCUMFERENCE.substringAfter(READ_PERMISSION_PREFIX),
+                WeightRecord::class to READ_WEIGHT.substringAfter(READ_PERMISSION_PREFIX),
+                WheelchairPushesRecord::class to
+                    READ_WHEELCHAIR_PUSHES.substringAfter(READ_PERMISSION_PREFIX),
+            )
     }
 
     override fun equals(other: Any?): Boolean {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
index d2e6388..1a566d9 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
@@ -83,6 +83,7 @@
                 "light" to STAGE_TYPE_LIGHT,
                 "deep" to STAGE_TYPE_DEEP,
                 "rem" to STAGE_TYPE_REM,
+                "unknown" to STAGE_TYPE_UNKNOWN
             )
 
         @RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
index 599b99d..fe4f595 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
@@ -93,7 +93,7 @@
     override fun filterGrantedPermissions(
         permissions: Set<PermissionProto.Permission>,
     ): ListenableFuture<Set<PermissionProto.Permission>> {
-        return executeWithVersionCheck(min(MIN_API_VERSION, 4)) { service, resultFuture ->
+        return executeWithVersionCheck(min(MIN_API_VERSION, 5)) { service, resultFuture ->
             service.filterGrantedPermissions(
                 getRequestContext(),
                 permissions.map { Permission(it) }.toList(),
@@ -207,7 +207,7 @@
     override fun registerForDataNotifications(
         request: RequestProto.RegisterForDataNotificationsRequest,
     ): ListenableFuture<Void> =
-        executeWithVersionCheck(/* minApiVersion = */ 2) { service, resultFuture ->
+        executeWithVersionCheck(min(MIN_API_VERSION, 2)) { service, resultFuture ->
             service.registerForDataNotifications(
                 getRequestContext(),
                 RegisterForDataNotificationsRequest(request),
@@ -218,7 +218,7 @@
     override fun unregisterFromDataNotifications(
         request: RequestProto.UnregisterFromDataNotificationsRequest,
     ): ListenableFuture<Void> =
-        executeWithVersionCheck(/* minApiVersion = */ 2) { service, resultFuture ->
+        executeWithVersionCheck(min(MIN_API_VERSION, 2)) { service, resultFuture ->
             service.unregisterFromDataNotifications(
                 getRequestContext(),
                 UnregisterFromDataNotificationsRequest(request),
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt
index 5ffb521..5539e49 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt
@@ -16,16 +16,19 @@
 
 package androidx.health.connect.client
 
+import androidx.health.connect.client.records.Record
 import java.io.File
 import java.net.URL
 import java.util.zip.ZipEntry
 import java.util.zip.ZipInputStream
 import kotlin.reflect.KClass
 
-val RECORD_CLASSES: List<KClass<*>> by lazy {
+@Suppress("UNCHECKED_CAST")
+val RECORD_CLASSES: List<KClass<out Record>> by lazy {
     findClasses("androidx.health.connect.client.records")
         .filterNot { it.java.isInterface }
         .filter { it.simpleName.orEmpty().endsWith("Record") }
+        .map { it as KClass<out Record> }
 }
 
 fun findClasses(packageName: String): Set<KClass<*>> {
@@ -43,20 +46,19 @@
     }
 }
 
-private fun findClasses(directory: String, packageName: String): Set<String> =
-    buildSet {
-        if (directory.startsWith("file:") && ('!' in directory)) {
-            addAll(unzipClasses(path = directory, packageName = packageName))
-        }
+private fun findClasses(directory: String, packageName: String): Set<String> = buildSet {
+    if (directory.startsWith("file:") && ('!' in directory)) {
+        addAll(unzipClasses(path = directory, packageName = packageName))
+    }
 
-        for (file in File(directory).takeIf(File::exists)?.listFiles() ?: emptyArray()) {
-            if (file.isDirectory) {
-                addAll(findClasses(file.absolutePath, "$packageName.${file.name}"))
-            } else if (file.name.endsWith(".class")) {
-                add("$packageName.${file.name.dropLast(6)}")
-            }
+    for (file in File(directory).takeIf(File::exists)?.listFiles() ?: emptyArray()) {
+        if (file.isDirectory) {
+            addAll(findClasses(file.absolutePath, "$packageName.${file.name}"))
+        } else if (file.name.endsWith(".class")) {
+            add("$packageName.${file.name.dropLast(6)}")
         }
     }
+}
 
 private fun unzipClasses(path: String, packageName: String): Set<String> =
     ZipInputStream(URL(path.substringBefore('!')).openStream()).use { zip ->
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
index 7dc78ba..fd39a8f 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
@@ -51,4 +51,20 @@
         Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
         Truth.assertThat(intent.`package`).isEqualTo(PROVIDER_PACKAGE_NAME)
     }
+
+    @Test
+    fun createIntentTest_permissionStrings() {
+        val requestPermissionContract =
+            PermissionController.createRequestPermissionResultContractInternal(
+                PROVIDER_PACKAGE_NAME
+            )
+        val intent =
+            requestPermissionContract.createIntent(
+                context,
+                setOf(HealthPermission.READ_ACTIVE_CALORIES_BURNED)
+            )
+
+        Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
+        Truth.assertThat(intent.`package`).isEqualTo(PROVIDER_PACKAGE_NAME)
+    }
 }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/SleepStageConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/SleepStageConverterTest.kt
new file mode 100644
index 0000000..cd0e471
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/SleepStageConverterTest.kt
@@ -0,0 +1,38 @@
+package androidx.health.connect.client.impl.converters.records
+
+import androidx.health.connect.client.records.SleepStageRecord
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import java.time.ZoneOffset
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SuppressWarnings("GoodTime") // Safe to use in test
+private val START_TIME = Instant.ofEpochMilli(1234L)
+@SuppressWarnings("GoodTime") // Safe to use in test
+private val END_TIME = Instant.ofEpochMilli(5678L)
+@SuppressWarnings("GoodTime") // Safe to use in test
+private val START_ZONE_OFFSET = ZoneOffset.ofHours(1)
+@SuppressWarnings("GoodTime") // Safe to use in test
+private val END_ZONE_OFFSET = ZoneOffset.ofHours(2)
+
+@RunWith(JUnit4::class)
+class SleepStageConverterTest {
+
+    // Because the APK schema has stage type required
+    // we will need to serialize "unknown" explicitly.
+    @Test
+    fun unknownStageType_serializesUnknown() {
+        val record =
+            SleepStageRecord(
+                stage = SleepStageRecord.STAGE_TYPE_UNKNOWN,
+                startTime = START_TIME,
+                startZoneOffset = START_ZONE_OFFSET,
+                endTime = END_TIME,
+                endZoneOffset = END_ZONE_OFFSET,
+            )
+        val proto = record.toProto()
+        assertThat(proto.valuesMap).containsExactly("stage", enumVal("unknown"))
+    }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
index 7d5a4c3..eb2649b 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
@@ -15,9 +15,13 @@
  */
 package androidx.health.connect.client.permission
 
+import androidx.health.connect.client.RECORD_CLASSES
+import androidx.health.connect.client.records.ExerciseRouteRecord
+import androidx.health.connect.client.records.Record
 import androidx.health.connect.client.records.StepsRecord
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,4 +41,48 @@
         assertThat(permission.accessType).isEqualTo(AccessTypes.WRITE)
         assertThat(permission.recordType).isEqualTo(StepsRecord::class)
     }
+
+    @Test
+    fun createReadPermissionInternal() {
+        val permission = HealthPermission.createReadPermissionInternal(StepsRecord::class)
+        assertThat(permission).isEqualTo(HealthPermission.READ_STEPS)
+    }
+
+    @Test
+    fun createReadPermissionInternal_everyRecord() {
+        RECORD_CLASSES.filterNot { it == ExerciseRouteRecord::class }
+            .forEach {
+                val permission = HealthPermission.createReadPermissionInternal(it)
+                assertThat(permission).isNotNull()
+            }
+    }
+
+    @Test
+    fun createReadPermissionInternal_invalidRecord_isNull() {
+        assertThrows(IllegalArgumentException::class.java) {
+            HealthPermission.createReadPermissionInternal(Record::class)
+        }
+    }
+
+    @Test
+    fun createWritePermissionInternal() {
+        val permission = HealthPermission.createWritePermissionInternal(StepsRecord::class)
+        assertThat(permission).isEqualTo(HealthPermission.WRITE_STEPS)
+    }
+
+    @Test
+    fun createWritePermissionInternal_everyRecord() {
+        RECORD_CLASSES.filterNot { it == ExerciseRouteRecord::class }
+            .forEach {
+                val permission = HealthPermission.createWritePermissionInternal(it)
+                assertThat(permission).isNotNull()
+            }
+    }
+
+    @Test
+    fun createWritePermissionInternal_invalidRecord_isNull() {
+        assertThrows(IllegalArgumentException::class.java) {
+            HealthPermission.createWritePermissionInternal(Record::class)
+        }
+    }
 }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SleepStageRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SleepStageRecordTest.kt
index d282950..9ebd4a4 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SleepStageRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SleepStageRecordTest.kt
@@ -68,7 +68,7 @@
             SleepStageRecord.Companion::class
                 .members
                 .asSequence()
-                .filter { it -> it.name.startsWith("STAGE_TYPE") && !it.name.endsWith("UNKNOWN") }
+                .filter { it -> it.name.startsWith("STAGE_TYPE") }
                 .filter { it -> it.returnType == typeOf<Int>() }
                 .map { it -> it.call(ExerciseSessionRecord.Companion) }
                 .toHashSet()
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
index a81ad20..609c83a5 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
@@ -28,6 +28,7 @@
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageInfo
 import android.os.Looper.getMainLooper
+import androidx.health.connect.client.permission.HealthPermission
 import androidx.health.platform.client.impl.ipc.ClientConfiguration
 import androidx.health.platform.client.impl.ipc.internal.ConnectionManager
 import androidx.health.platform.client.impl.testing.FakeHealthDataService
@@ -123,6 +124,25 @@
     }
 
     @Test
+    fun filterGrantedPermissions_somePermissionsGranted_expectCorrectGrantedList() {
+        val readPermission =
+            PermissionProto.Permission.newBuilder()
+                .setPermission(HealthPermission.READ_HEART_RATE)
+                .build()
+        val writePermission =
+            PermissionProto.Permission.newBuilder()
+                .setPermission(HealthPermission.WRITE_BLOOD_PRESSURE)
+                .build()
+
+        fakeAhpServiceStub.addGrantedPermission(Permission(readPermission))
+        val resultFuture = ahpClient.getGrantedPermissions(setOf(readPermission, writePermission))
+        shadowOf(getMainLooper()).idle()
+
+        val expected = setOf(readPermission)
+        assertSuccess(resultFuture, expected)
+    }
+
+    @Test
     fun revokeAllPermissions_success() {
         val resultFuture: ListenableFuture<Unit> = ahpClient.revokeAllPermissions()
         shadowOf(getMainLooper()).idle()
diff --git a/inspection/inspection-gradle-plugin/build.gradle b/inspection/inspection-gradle-plugin/build.gradle
index dc1c3a7..fe20a87 100644
--- a/inspection/inspection-gradle-plugin/build.gradle
+++ b/inspection/inspection-gradle-plugin/build.gradle
@@ -38,7 +38,6 @@
 
     testImplementation(project(":internal-testutils-gradle-plugin"))
     testImplementation(gradleTestKit())
-    testImplementation(libs.testRunner)
     testImplementation(libs.junit)
     testImplementation(libs.kotlinTest)
 }
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 878ec32..0d04026 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -10,7 +10,7 @@
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
     api("androidx.leanback:leanback:1.1.0-beta01")
-    api("androidx.paging:paging-runtime:3.0.0")
+    api("androidx.paging:paging-runtime:3.1.0")
 
     // To avoid manifest merger warnings due to duplicate package names
     // It can be removed if leanback library is updated to a newer version
diff --git a/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt b/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
index d4ae23a..9b2d05e 100644
--- a/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
+++ b/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
@@ -122,7 +122,6 @@
         // empty previous list.
         assertEvents(
             listOf(
-                localLoadStatesOf(),
                 localLoadStatesOf(refreshLocal = LoadState.Loading),
                 localLoadStatesOf(
                     refreshLocal = LoadState.NotLoading(endOfPaginationReached = false)
diff --git a/libraryversions.toml b/libraryversions.toml
index 2d67f06..cc12935 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -19,7 +19,7 @@
 CAR_APP = "1.3.0-beta02"
 COLLECTION = "1.3.0-dev01"
 COMPOSE = "1.4.0-alpha02"
-COMPOSE_COMPILER = "1.3.3"
+COMPOSE_COMPILER = "1.4.0-alpha01"
 COMPOSE_MATERIAL3 = "1.1.0-alpha02"
 COMPOSE_RUNTIME_TRACING = "1.0.0-alpha02"
 CONSTRAINTLAYOUT = "2.2.0-alpha05"
@@ -91,6 +91,7 @@
 PRINT = "1.1.0-beta01"
 PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha01"
 PRIVACYSANDBOX_TOOLS = "1.0.0-alpha01"
+PRIVACYSANDBOX_UI = "1.0.0-alpha01"
 PROFILEINSTALLER = "1.3.0-alpha02"
 RECOMMENDATION = "1.1.0-alpha01"
 RECYCLERVIEW = "1.4.0-alpha01"
@@ -99,7 +100,7 @@
 RESOURCEINSPECTION = "1.1.0-alpha01"
 ROOM = "2.6.0-alpha01"
 SAVEDSTATE = "1.3.0-alpha01"
-SECURITY = "1.1.0-alpha04"
+SECURITY = "1.1.0-alpha05"
 SECURITY_APP_AUTHENTICATOR = "1.0.0-alpha03"
 SECURITY_APP_AUTHENTICATOR_TESTING = "1.0.0-alpha02"
 SECURITY_BIOMETRIC = "1.0.0-alpha01"
@@ -219,6 +220,7 @@
 PRINT = { group = "androidx.print", atomicGroupVersion = "versions.PRINT" }
 PRIVACYSANDBOX_SDKRUNTIME = { group = "androidx.privacysandbox.sdkruntime", atomicGroupVersion = "versions.PRIVACYSANDBOX_SDKRUNTIME" }
 PRIVACYSANDBOX_TOOLS = { group = "androidx.privacysandbox.tools", atomicGroupVersion = "versions.PRIVACYSANDBOX_TOOLS" }
+PRIVACYSANDBOX_UI = { group = "androidx.privacysandbox.ui", atomicGroupVersion = "versions.PRIVACYSANDBOX_UI" }
 PROFILEINSTALLER = { group = "androidx.profileinstaller", atomicGroupVersion = "versions.PROFILEINSTALLER" }
 RECOMMENDATION = { group = "androidx.recommendation", atomicGroupVersion = "versions.RECOMMENDATION" }
 RECYCLERVIEW = { group = "androidx.recyclerview" }
diff --git a/navigation/navigation-benchmark/src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt b/navigation/navigation-benchmark/src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt
new file mode 100644
index 0000000..561a1d3
--- /dev/null
+++ b/navigation/navigation-benchmark/src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation
+
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NavDeepLinkBenchmark {
+
+    @get:Rule
+    val benchmarkRule = BenchmarkRule()
+
+    @Test
+    fun navGraphDestinations_withRoutes() = inflateNavGraph_withRoutes(1)
+
+    @Test
+    fun navGraphDestinations_withRoutes10() = inflateNavGraph_withRoutes(10)
+
+    @Test
+    fun navGraphDestinations_withRoutes50() = inflateNavGraph_withRoutes(50)
+
+    @Test
+    fun navGraphDestinations_withRoutes100() = inflateNavGraph_withRoutes(100)
+
+    private fun inflateNavGraph_withRoutes(count: Int) {
+        val navigatorProvider = NavigatorProvider().apply {
+            addNavigator(NavGraphNavigator(this))
+            addNavigator(NoOpNavigator())
+        }
+        val navigator = navigatorProvider.getNavigator(NoOpNavigator::class.java)
+        benchmarkRule.measureRepeated {
+            navigatorProvider.getNavigator(NavGraphNavigator::class.java)
+                .createDestination().apply {
+                    id = GRAPH_ID
+                    setStartDestination(START_DESTINATION_ID)
+                    for (i in 0 until count) {
+                        addDestination(
+                            navigator.createDestination().apply {
+                                route = URI_PATH + i + URI_EXTRAS
+                                id = i
+                            }
+                        )
+                    }
+                }
+        }
+    }
+
+    companion object {
+        const val URI_PATH = "example.com/"
+        const val URI_EXTRAS = "test/{test}?param1={param}#fragment"
+        const val START_DESTINATION_ID = 0
+        const val GRAPH_ID = 111
+    }
+}
\ No newline at end of file
diff --git a/navigation/navigation-dynamic-features-runtime/build.gradle b/navigation/navigation-dynamic-features-runtime/build.gradle
index 4adba73..6711b01 100644
--- a/navigation/navigation-dynamic-features-runtime/build.gradle
+++ b/navigation/navigation-dynamic-features-runtime/build.gradle
@@ -32,7 +32,7 @@
 
 dependencies {
     api(project(":navigation:navigation-runtime"))
-    api(libs.playCore)
+    api(libs.playFeatureDelivery)
 
     testImplementation(project(":navigation:navigation-testing"))
     testImplementation("androidx.arch.core:core-testing:2.1.0")
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
index 0b3528e..856f8fd 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
@@ -26,19 +26,26 @@
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.MemberName
 import com.squareup.kotlinpoet.TypeSpec
 
 class AbstractSdkProviderGenerator(private val api: ParsedApi) {
     companion object {
-        private val SANDBOXED_SDK_PROVIDER_CLASS =
-            ClassName("android.app.sdksandbox", "SandboxedSdkProvider")
-        private val DATA_RECEIVED_CALLBACK_CLASS =
-            ClassName("android.app.sdksandbox", "SandboxedSdkProvider", "DataReceivedCallback")
-        private val SANDBOXED_SDK_CLASS =
-            ClassName("android.app.sdksandbox", "SandboxedSdk")
-        private val CONTEXT_CLASS = ClassName("android.content", "Context")
-        private val BUNDLE_CLASS = ClassName("android.os", "Bundle")
-        private val VIEW_CLASS = ClassName("android.view", "View")
+        private val sandboxedSdkProviderClass =
+            ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkProviderCompat")
+        private val sandboxedSdkClass =
+            ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkCompat")
+        private val sandboxedSdkCreateMethod =
+            MemberName(
+                ClassName(
+                    sandboxedSdkClass.packageName,
+                    sandboxedSdkClass.simpleName,
+                    "Companion"
+                ), "create"
+            )
+        private val contextClass = ClassName("android.content", "Context")
+        private val bundleClass = ClassName("android.os", "Bundle")
+        private val viewClass = ClassName("android.view", "View")
     }
 
     fun generate(): FileSpec? {
@@ -49,11 +56,10 @@
         val className = "AbstractSandboxedSdkProvider"
         val classSpec =
             TypeSpec.classBuilder(className)
-                .superclass(SANDBOXED_SDK_PROVIDER_CLASS)
+                .superclass(sandboxedSdkProviderClass)
                 .addModifiers(KModifier.ABSTRACT)
                 .addFunction(generateOnLoadSdkFunction())
                 .addFunction(generateGetViewFunction())
-                .addFunction(generateOnDataReceivedFunction())
                 .addFunction(generateCreateServiceFunction(api.getOnlyService()))
 
         return FileSpec.builder(packageName, className)
@@ -64,14 +70,14 @@
     private fun generateOnLoadSdkFunction(): FunSpec {
         return FunSpec.builder("onLoadSdk").build {
             addModifiers(KModifier.OVERRIDE)
-            addParameter("params", BUNDLE_CLASS)
-            returns(SANDBOXED_SDK_CLASS)
+            addParameter("params", bundleClass)
+            returns(sandboxedSdkClass)
             addStatement(
                 "val sdk = ${getCreateServiceFunctionName(api.getOnlyService())}(context!!)"
             )
             addStatement(
-                "return %T(%T(sdk))",
-                SANDBOXED_SDK_CLASS,
+                "return %M(%T(sdk))",
+                sandboxedSdkCreateMethod,
                 api.getOnlyService().stubDelegateNameSpec()
             )
         }
@@ -80,11 +86,11 @@
     private fun generateGetViewFunction(): FunSpec {
         return FunSpec.builder("getView").build {
             addModifiers(KModifier.OVERRIDE)
-            addParameter("windowContext", CONTEXT_CLASS)
-            addParameter("params", BUNDLE_CLASS)
+            addParameter("windowContext", contextClass)
+            addParameter("params", bundleClass)
             addParameter("width", Int::class)
             addParameter("height", Int::class)
-            returns(VIEW_CLASS)
+            returns(viewClass)
             addStatement("TODO(\"Implement\")")
         }
     }
@@ -92,19 +98,11 @@
     private fun generateCreateServiceFunction(service: AnnotatedInterface): FunSpec {
         return FunSpec.builder(getCreateServiceFunctionName(service))
             .addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED)
-            .addParameter("context", CONTEXT_CLASS)
+            .addParameter("context", contextClass)
             .returns(service.type.poetSpec())
             .build()
     }
 
-    private fun generateOnDataReceivedFunction(): FunSpec {
-        return FunSpec.builder("onDataReceived")
-            .addModifiers(KModifier.OVERRIDE)
-            .addParameter("data", BUNDLE_CLASS)
-            .addParameter("callback", DATA_RECEIVED_CALLBACK_CLASS)
-            .build()
-    }
-
     private fun getCreateServiceFunctionName(service: AnnotatedInterface) =
         "create${service.type.simpleName}"
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
index c56f35ed..0d1b157 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
@@ -16,27 +16,32 @@
 
 package androidx.privacysandbox.tools.apicompiler.generator
 
-import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.Metadata
 import androidx.privacysandbox.tools.core.generator.AidlCompiler
 import androidx.privacysandbox.tools.core.generator.AidlGenerator
-import androidx.privacysandbox.tools.core.generator.BinderCodeConverter
 import androidx.privacysandbox.tools.core.generator.ClientProxyTypeGenerator
+import androidx.privacysandbox.tools.core.generator.ServerBinderCodeConverter
 import androidx.privacysandbox.tools.core.generator.StubDelegatesGenerator
+import androidx.privacysandbox.tools.core.generator.ThrowableParcelConverterFileGenerator
 import androidx.privacysandbox.tools.core.generator.TransportCancellationGenerator
 import androidx.privacysandbox.tools.core.generator.ValueConverterFileGenerator
+import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.model.getOnlyService
+import androidx.privacysandbox.tools.core.model.hasSuspendFunctions
 import com.google.devtools.ksp.processing.CodeGenerator
 import com.google.devtools.ksp.processing.Dependencies
 import com.squareup.kotlinpoet.FileSpec
 import java.nio.file.Files.createTempDirectory
 import java.nio.file.Path
+import kotlin.io.path.extension
+import kotlin.io.path.nameWithoutExtension
 
 class SdkCodeGenerator(
     private val codeGenerator: CodeGenerator,
     private val api: ParsedApi,
     private val aidlCompilerPath: Path,
 ) {
-    private val binderCodeConverter = BinderCodeConverter(api)
+    private val binderCodeConverter = ServerBinderCodeConverter(api)
 
     fun generate() {
         if (api.services.isEmpty()) {
@@ -47,6 +52,8 @@
         generateStubDelegates()
         generateValueConverters()
         generateCallbackProxies()
+        generateToolMetadata()
+        generateSuspendFunctionUtilities()
     }
 
     private fun generateAidlSources() {
@@ -74,28 +81,42 @@
     }
 
     private fun generateStubDelegates() {
-        val basePackageName = api.getOnlyService().type.packageName
-        val stubDelegateGenerator = StubDelegatesGenerator(basePackageName, binderCodeConverter)
+        val stubDelegateGenerator = StubDelegatesGenerator(basePackageName(), binderCodeConverter)
         api.services.map(stubDelegateGenerator::generate).forEach(::write)
         api.interfaces.map(stubDelegateGenerator::generate).forEach(::write)
-
-        val transportCancellationGenerator = TransportCancellationGenerator(basePackageName)
-        transportCancellationGenerator.generate().also(::write)
     }
 
     private fun generateValueConverters() {
-        val valueConverterFileGenerator = ValueConverterFileGenerator(api)
+        val valueConverterFileGenerator = ValueConverterFileGenerator(binderCodeConverter)
         api.values.map(valueConverterFileGenerator::generate).forEach(::write)
     }
 
     private fun generateCallbackProxies() {
-        val basePackageName = api.getOnlyService().type.packageName
-        val clientProxyGenerator = ClientProxyTypeGenerator(basePackageName, binderCodeConverter)
+        val clientProxyGenerator = ClientProxyTypeGenerator(basePackageName(), binderCodeConverter)
         api.callbacks.map(clientProxyGenerator::generate).forEach(::write)
     }
 
-    private fun write(spec: FileSpec) {
-        codeGenerator.createNewFile(Dependencies(false), spec.packageName, spec.name)
-            .write(spec)
+    private fun generateToolMetadata() {
+        codeGenerator.createNewFile(
+            Dependencies(false),
+            Metadata.filePath.parent.toString(),
+            Metadata.filePath.nameWithoutExtension,
+            Metadata.filePath.extension,
+        ).use { Metadata.toolMetadata.writeTo(it) }
     }
+
+    private fun generateSuspendFunctionUtilities() {
+        if (!api.hasSuspendFunctions()) return
+        TransportCancellationGenerator(basePackageName()).generate().also(::write)
+        ThrowableParcelConverterFileGenerator(basePackageName()).generate(convertToParcel = true)
+            .also(::write)
+    }
+
+    private fun write(spec: FileSpec) {
+
+        codeGenerator.createNewFile(Dependencies(false), spec.packageName, spec.name)
+            .bufferedWriter().use(spec::writeTo)
+    }
+
+    private fun basePackageName() = api.getOnlyService().type.packageName
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 4fd38fd..36dbbfb 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -16,12 +16,15 @@
 
 package androidx.privacysandbox.tools.apicompiler
 
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.compileAll
 import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
+import androidx.privacysandbox.tools.testing.resourceOutputDir
+import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
 import androidx.room.compiler.processing.util.compiler.compile
-import androidx.room.compiler.processing.util.Source
+import com.google.common.truth.Truth.assertThat
 import java.io.File
 import java.nio.file.Files
 import org.junit.Test
@@ -66,6 +69,9 @@
                 "com/mysdk/IResponseTransactionCallback.java",
                 "com/mysdk/MyCallbackClientProxy.kt",
                 "com/mysdk/IMyCallback.java",
+                "com/mysdk/PrivacySandboxThrowableParcelConverter.kt",
+                "com/mysdk/ParcelableStackFrame.java",
+                "com/mysdk/PrivacySandboxThrowableParcel.java",
             )
         }.also {
             it.generatesSourcesWithContents(expectedOutput)
@@ -89,6 +95,43 @@
     }
 
     @Test
+    fun generatesMetadataFile() {
+        val source =
+            Source.kotlin(
+                "com/mysdk/MySdk.kt",
+                """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+                    @PrivacySandboxService
+                    interface MySdk {
+                        fun doStuff(x: Int, y: Int)
+                    }
+                """
+            )
+        val provider = PrivacySandboxKspCompiler.Provider()
+        val compilationResult =
+            compileAll(
+                listOf(source),
+                symbolProcessorProviders = listOf(provider),
+                processorOptions = getProcessorOptions(),
+            )
+        assertThat(compilationResult).succeeds()
+
+        val resourceMap = compilationResult.resourceOutputDir.walk()
+            .filter { it.isFile }
+            .map { it.toRelativeString(compilationResult.resourceOutputDir) to it.readBytes() }
+            .toMap()
+        val expectedMetadataRelativePath = "META-INF/privacysandbox/tool-metadata.pb"
+        assertThat(resourceMap).containsKey(expectedMetadataRelativePath)
+        assertThat(ToolMetadata.parseFrom(resourceMap[expectedMetadataRelativePath]))
+            .isEqualTo(
+                ToolMetadata.newBuilder()
+                    .setCodeGenerationVersion(1)
+                    .build()
+            )
+    }
+
+    @Test
     fun compileInvalidServiceInterface_fails() {
         val source =
             Source.kotlin(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
index abdec6c..5dd00bf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
@@ -178,8 +178,9 @@
         )
         checkSourceFails(dataClass)
             .containsExactlyErrors(
-                "Error in com.mysdk.MySdkRequest.foo: only primitives and data classes " +
-                    "annotated with @PrivacySandboxValue are supported as properties."
+                "Error in com.mysdk.MySdkRequest.foo: only primitives, data classes " +
+                    "annotated with @PrivacySandboxValue and interfaces annotated with " +
+                    "@PrivacySandboxInterface are supported as properties."
             )
     }
 
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
index 94386a63..94ec579 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
@@ -39,14 +39,16 @@
 }
 
 @PrivacySandboxValue
-data class Request(val query: String)
+data class Request(val query: String, val myInterface: MyInterface)
 
 @PrivacySandboxValue
-data class Response(val response: String)
+data class Response(val response: String, val mySecondInterface: MySecondInterface)
 
 @PrivacySandboxCallback
 interface MyCallback {
     fun onComplete(response: Response)
 
     fun onClick(x: Int, y: Int)
+
+    fun onCompleteInterface(myInterface: MyInterface)
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
index 97ab29e..6efe8a0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
@@ -1,17 +1,17 @@
 package com.mysdk
 
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SandboxedSdkProvider
 import android.content.Context
 import android.os.Bundle
 import android.view.View
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat.Companion.create
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
 import kotlin.Int
-import kotlin.Unit
 
-public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProvider() {
-  public override fun onLoadSdk(params: Bundle): SandboxedSdk {
+public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProviderCompat() {
+  public override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
     val sdk = createMySdk(context!!)
-    return SandboxedSdk(MySdkStubDelegate(sdk))
+    return create(MySdkStubDelegate(sdk))
   }
 
   public override fun getView(
@@ -23,9 +23,5 @@
     TODO("Implement")
   }
 
-  public override fun onDataReceived(`data`: Bundle,
-      callback: SandboxedSdkProvider.DataReceivedCallback): Unit {
-  }
-
   protected abstract fun createMySdk(context: Context): MySdk
 }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
index 6fa273f..2f861da 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
@@ -1,12 +1,9 @@
 package com.mysdk
 
 import com.mysdk.ResponseConverter.toParcelable
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
 
 public class MyCallbackClientProxy(
-    private val remote: IMyCallback,
+    public val remote: IMyCallback,
 ) : MyCallback {
     public override fun onComplete(response: Response): Unit {
         remote.onComplete(toParcelable(response))
@@ -15,4 +12,8 @@
     public override fun onClick(x: Int, y: Int): Unit {
         remote.onClick(x, y)
     }
+
+    public override fun onCompleteInterface(myInterface: MyInterface): Unit {
+        remote.onCompleteInterface(MyInterfaceStubDelegate(myInterface))
+    }
 }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt
index 7b71956..2c9cf05 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt
@@ -1,9 +1,12 @@
 package com.mysdk
 
+import com.mysdk.PrivacySandboxThrowableParcelConverter
+import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
 import com.mysdk.RequestConverter.fromParcelable
 import com.mysdk.ResponseConverter.toParcelable
 import kotlin.Int
 import kotlin.Unit
+import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
@@ -13,13 +16,14 @@
 ) : IMyInterface.Stub() {
   public override fun doSomething(request: ParcelableRequest,
       transactionCallback: IResponseTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
     val job = GlobalScope.launch(Dispatchers.Main) {
       try {
         val result = delegate.doSomething(fromParcelable(request))
         transactionCallback.onSuccess(toParcelable(result))
       }
       catch (t: Throwable) {
-        transactionCallback.onFailure(404, t.message)
+        transactionCallback.onFailure(toThrowableParcel(t))
       }
     }
     val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -28,13 +32,14 @@
 
   public override fun getMyInterface(input: IMyInterface,
       transactionCallback: IMyInterfaceTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
     val job = GlobalScope.launch(Dispatchers.Main) {
       try {
         val result = delegate.getMyInterface((input as MyInterfaceStubDelegate).delegate)
         transactionCallback.onSuccess(MyInterfaceStubDelegate(result))
       }
       catch (t: Throwable) {
-        transactionCallback.onFailure(404, t.message)
+        transactionCallback.onFailure(toThrowableParcel(t))
       }
     }
     val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -43,6 +48,7 @@
 
   public override fun getMySecondInterface(input: IMySecondInterface,
       transactionCallback: IMySecondInterfaceTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
     val job = GlobalScope.launch(Dispatchers.Main) {
       try {
         val result = delegate.getMySecondInterface((input as
@@ -50,7 +56,7 @@
         transactionCallback.onSuccess(MySecondInterfaceStubDelegate(result))
       }
       catch (t: Throwable) {
-        transactionCallback.onFailure(404, t.message)
+        transactionCallback.onFailure(toThrowableParcel(t))
       }
     }
     val cancellationSignal = TransportCancellationCallback() { job.cancel() }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
index 5228c52..0f4c3ee 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
@@ -1,10 +1,13 @@
 package com.mysdk
 
+import com.mysdk.PrivacySandboxThrowableParcelConverter
+import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
 import com.mysdk.RequestConverter
 import com.mysdk.RequestConverter.fromParcelable
 import com.mysdk.ResponseConverter.toParcelable
 import kotlin.Int
 import kotlin.Unit
+import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
@@ -17,13 +20,14 @@
     y: Int,
     transactionCallback: IStringTransactionCallback,
   ): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
     val job = GlobalScope.launch(Dispatchers.Main) {
       try {
         val result = delegate.doStuff(x, y)
         transactionCallback.onSuccess(result)
       }
       catch (t: Throwable) {
-        transactionCallback.onFailure(404, t.message)
+        transactionCallback.onFailure(toThrowableParcel(t))
       }
     }
     val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -32,13 +36,14 @@
 
   public override fun handleRequest(request: ParcelableRequest,
       transactionCallback: IResponseTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
     val job = GlobalScope.launch(Dispatchers.Main) {
       try {
         val result = delegate.handleRequest(fromParcelable(request))
         transactionCallback.onSuccess(toParcelable(result))
       }
       catch (t: Throwable) {
-        transactionCallback.onFailure(404, t.message)
+        transactionCallback.onFailure(toThrowableParcel(t))
       }
     }
     val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -47,13 +52,14 @@
 
   public override fun logRequest(request: ParcelableRequest,
       transactionCallback: IUnitTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
     val job = GlobalScope.launch(Dispatchers.Main) {
       try {
-        val result = delegate.logRequest(fromParcelable(request))
+        delegate.logRequest(fromParcelable(request))
         transactionCallback.onSuccess()
       }
       catch (t: Throwable) {
-        transactionCallback.onFailure(404, t.message)
+        transactionCallback.onFailure(toThrowableParcel(t))
       }
     }
     val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -70,13 +76,14 @@
 
   public override fun getMyInterface(input: IMyInterface,
       transactionCallback: IMyInterfaceTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
     val job = GlobalScope.launch(Dispatchers.Main) {
       try {
         val result = delegate.getMyInterface((input as MyInterfaceStubDelegate).delegate)
         transactionCallback.onSuccess(MyInterfaceStubDelegate(result))
       }
       catch (t: Throwable) {
-        transactionCallback.onFailure(404, t.message)
+        transactionCallback.onFailure(toThrowableParcel(t))
       }
     }
     val cancellationSignal = TransportCancellationCallback() { job.cancel() }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
new file mode 100644
index 0000000..368f02f
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
@@ -0,0 +1,25 @@
+package com.mysdk
+
+public object PrivacySandboxThrowableParcelConverter {
+    public fun toThrowableParcel(throwable: Throwable): PrivacySandboxThrowableParcel {
+        val parcel = PrivacySandboxThrowableParcel()
+        parcel.exceptionClass = throwable::class.qualifiedName
+        parcel.errorMessage = throwable.message
+        parcel.stackTrace = throwable.stackTrace.map {
+            val stackFrame = ParcelableStackFrame()
+            stackFrame.declaringClass = it.className
+            stackFrame.methodName = it.methodName
+            stackFrame.fileName = it.fileName
+            stackFrame.lineNumber = it.lineNumber
+            stackFrame
+        }.toTypedArray()
+        throwable.cause?.let {
+            parcel.cause = toThrowableParcel(it)
+        }
+        parcel.suppressedExceptions =
+            throwable.suppressedExceptions.map {
+                toThrowableParcel(it)
+            }.toTypedArray()
+        return parcel
+    }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/RequestConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/RequestConverter.kt
new file mode 100644
index 0000000..538f1cd
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/RequestConverter.kt
@@ -0,0 +1,17 @@
+package com.mysdk
+
+public object RequestConverter {
+    public fun fromParcelable(parcelable: ParcelableRequest): Request {
+        val annotatedValue = Request(
+                query = parcelable.query,
+                myInterface = (parcelable.myInterface as MyInterfaceStubDelegate).delegate)
+        return annotatedValue
+    }
+
+    public fun toParcelable(annotatedValue: Request): ParcelableRequest {
+        val parcelable = ParcelableRequest()
+        parcelable.query = annotatedValue.query
+        parcelable.myInterface = MyInterfaceStubDelegate(annotatedValue.myInterface)
+        return parcelable
+    }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/ResponseConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/ResponseConverter.kt
new file mode 100644
index 0000000..3138ba1
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/ResponseConverter.kt
@@ -0,0 +1,19 @@
+package com.mysdk
+
+public object ResponseConverter {
+    public fun fromParcelable(parcelable: ParcelableResponse): Response {
+        val annotatedValue = Response(
+                response = parcelable.response,
+                mySecondInterface = (parcelable.mySecondInterface as
+                        MySecondInterfaceStubDelegate).delegate)
+        return annotatedValue
+    }
+
+    public fun toParcelable(annotatedValue: Response): ParcelableResponse {
+        val parcelable = ParcelableResponse()
+        parcelable.response = annotatedValue.response
+        parcelable.mySecondInterface =
+                MySecondInterfaceStubDelegate(annotatedValue.mySecondInterface)
+        return parcelable
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
index 6417ee2..56e1a34 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
@@ -17,20 +17,29 @@
 package androidx.privacysandbox.tools.apigenerator
 
 import androidx.privacysandbox.tools.apigenerator.parser.ApiStubParser
-import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.generator.AidlCompiler
 import androidx.privacysandbox.tools.core.generator.AidlGenerator
 import androidx.privacysandbox.tools.core.generator.BinderCodeConverter
+import androidx.privacysandbox.tools.core.generator.ClientBinderCodeConverter
 import androidx.privacysandbox.tools.core.generator.ClientProxyTypeGenerator
+import androidx.privacysandbox.tools.core.generator.PrivacySandboxExceptionFileGenerator
 import androidx.privacysandbox.tools.core.generator.StubDelegatesGenerator
+import androidx.privacysandbox.tools.core.generator.ThrowableParcelConverterFileGenerator
 import androidx.privacysandbox.tools.core.generator.ValueConverterFileGenerator
 import androidx.privacysandbox.tools.core.generator.ValueFileGenerator
+import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.model.getOnlyService
+import androidx.privacysandbox.tools.core.model.hasSuspendFunctions
 import java.io.File
-import java.nio.file.Files
 import java.nio.file.Path
+import java.util.zip.ZipInputStream
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createFile
 import kotlin.io.path.exists
+import kotlin.io.path.inputStream
 import kotlin.io.path.isDirectory
+import kotlin.io.path.moveTo
+import kotlin.io.path.outputStream
 
 /** Generate source files for communicating with an SDK running in the Privacy Sandbox. */
 class PrivacySandboxApiGenerator {
@@ -54,14 +63,15 @@
             "$outputDirectory is not a valid output path."
         }
 
+        val api = unzipDescriptorsFileAndParseStubs(sdkInterfaceDescriptors, outputDirectory)
         val output = outputDirectory.toFile()
-        val api = ApiStubParser.parse(sdkInterfaceDescriptors)
 
         val basePackageName = api.getOnlyService().type.packageName
-        val binderCodeConverter = BinderCodeConverter(api)
+        val binderCodeConverter = ClientBinderCodeConverter(api)
         val interfaceFileGenerator = InterfaceFileGenerator()
 
         generateBinders(api, AidlCompiler(aidlCompiler), output)
+        generateServiceFactory(api, output)
         generateStubDelegates(
             api,
             basePackageName,
@@ -76,7 +86,8 @@
             interfaceFileGenerator,
             output
         )
-        generateValueConverters(api, output)
+        generateValueConverters(api, binderCodeConverter, output)
+        generateSuspendFunctionUtilities(api, basePackageName, output)
     }
 
     private fun generateBinders(api: ParsedApi, aidlCompiler: AidlCompiler, output: File) {
@@ -88,14 +99,21 @@
                 val relativePath = aidlWorkingDir.toPath().relativize(it.file.toPath())
                 val source = it.file.toPath()
                 val dest = output.toPath().resolve(relativePath)
-                dest.toFile().parentFile.mkdirs()
-                Files.move(source, dest)
+                dest.parent.createDirectories()
+                source.moveTo(dest)
             }
         } finally {
             aidlWorkingDir.deleteRecursively()
         }
     }
 
+    private fun generateServiceFactory(api: ParsedApi, output: File) {
+        val serviceFactoryFileGenerator = ServiceFactoryFileGenerator()
+        api.services.forEach {
+            serviceFactoryFileGenerator.generate(it).writeTo(output)
+        }
+    }
+
     private fun generateStubDelegates(
         api: ParsedApi,
         basePackageName: String,
@@ -118,20 +136,58 @@
         output: File
     ) {
         val clientProxyGenerator = ClientProxyTypeGenerator(basePackageName, binderCodeConverter)
-        val serviceFactoryFileGenerator = ServiceFactoryFileGenerator()
-        api.services.forEach {
+        val annotatedInterfaces = api.services + api.interfaces
+        annotatedInterfaces.forEach {
             interfaceFileGenerator.generate(it).writeTo(output)
             clientProxyGenerator.generate(it).writeTo(output)
-            serviceFactoryFileGenerator.generate(it).writeTo(output)
         }
     }
 
-    private fun generateValueConverters(api: ParsedApi, output: File) {
+    private fun generateValueConverters(
+        api: ParsedApi,
+        binderCodeConverter: BinderCodeConverter,
+        output: File
+    ) {
         val valueFileGenerator = ValueFileGenerator()
-        val valueConverterFileGenerator = ValueConverterFileGenerator(api)
+        val valueConverterFileGenerator = ValueConverterFileGenerator(binderCodeConverter)
         api.values.forEach {
             valueFileGenerator.generate(it).writeTo(output)
             valueConverterFileGenerator.generate(it).writeTo(output)
         }
     }
+
+    private fun unzipDescriptorsFileAndParseStubs(
+        sdkInterfaceDescriptors: Path,
+        outputDirectory: Path,
+    ): ParsedApi {
+        val workingDirectory = outputDirectory.resolve("tmp-descriptors").createDirectories()
+        try {
+            ZipInputStream(sdkInterfaceDescriptors.inputStream()).use { input ->
+                generateSequence { input.nextEntry }
+                    .forEach { zipEntry ->
+                        val destination = workingDirectory.resolve(zipEntry.name)
+                        check(destination.startsWith(workingDirectory))
+                        destination.parent?.createDirectories()
+                        destination.createFile()
+                        input.copyTo(destination.outputStream())
+                    }
+            }
+
+            return ApiStubParser.parse(workingDirectory)
+        } finally {
+            workingDirectory.toFile().deleteRecursively()
+        }
+    }
+
+    private fun generateSuspendFunctionUtilities(
+        api: ParsedApi,
+        basePackageName: String,
+        output: File
+    ) {
+        if (!api.hasSuspendFunctions()) return
+        ThrowableParcelConverterFileGenerator(basePackageName).generate(
+            convertFromParcel = true
+        ).writeTo(output)
+        PrivacySandboxExceptionFileGenerator(basePackageName).generate().writeTo(output)
+    }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
index 89baf13..293d0ad 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
@@ -16,86 +16,32 @@
 
 package androidx.privacysandbox.tools.apigenerator
 
-import androidx.privacysandbox.tools.core.generator.addCode
+import androidx.privacysandbox.tools.core.generator.SpecNames.iBinderClassName
 import androidx.privacysandbox.tools.core.generator.addCommonSettings
-import androidx.privacysandbox.tools.core.generator.addControlFlow
-import androidx.privacysandbox.tools.core.generator.addStatement
 import androidx.privacysandbox.tools.core.generator.aidlInterfaceNameSpec
 import androidx.privacysandbox.tools.core.generator.build
 import androidx.privacysandbox.tools.core.generator.clientProxyNameSpec
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import com.squareup.kotlinpoet.ClassName
-import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
-import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.ParameterSpec
-import com.squareup.kotlinpoet.joinToCode
 
-internal class ServiceFactoryFileGenerator() {
+internal class ServiceFactoryFileGenerator {
+
     fun generate(service: AnnotatedInterface): FileSpec =
         FileSpec.builder(service.type.packageName, "${service.type.simpleName}Factory").build {
             addCommonSettings()
-            addImport("kotlinx.coroutines", "suspendCancellableCoroutine")
-            addImport("kotlin.coroutines", "resume")
-            addImport("kotlin.coroutines", "resumeWithException")
-
             addFunction(generateFactoryFunction(service))
         }
 
     private fun generateFactoryFunction(service: AnnotatedInterface) =
-        FunSpec.builder("create${service.type.simpleName}").build {
-            addModifiers(KModifier.SUSPEND)
-            addParameter(ParameterSpec("context", AndroidClassNames.context))
+        FunSpec.builder("wrapTo${service.type.simpleName}").build {
+            addParameter(ParameterSpec("binder", iBinderClassName))
             returns(ClassName(service.type.packageName, service.type.simpleName))
-
-            addCode {
-                addControlFlow("return suspendCancellableCoroutine") {
-                    addStatement(
-                        "val sdkSandboxManager = context.getSystemService(%T::class.java)",
-                        AndroidClassNames.sandboxManager
-                    )
-                    addControlFlow(
-                        "val outcomeReceiver = object: %T<%T, %T>",
-                        AndroidClassNames.outcomeReceiver,
-                        AndroidClassNames.sandboxedSdk,
-                        AndroidClassNames.loadSdkException
-                    ) {
-                        addControlFlow(
-                            "override fun onResult(result: %T)", AndroidClassNames.sandboxedSdk
-                        ) {
-                            addStatement(
-                                "it.resume(%T(%T.Stub.asInterface(result.getInterface())))",
-                                service.clientProxyNameSpec(), service.aidlInterfaceNameSpec()
-                            )
-                        }
-                        addControlFlow(
-                            "override fun onError(error: %T)", AndroidClassNames.loadSdkException
-                        ) {
-                            addStatement("it.resumeWithException(error)")
-                        }
-                    }
-                    val loadSdkParameters = listOf(
-                        CodeBlock.of("%S", service.type.packageName),
-                        CodeBlock.of("%T.EMPTY", AndroidClassNames.bundle),
-                        CodeBlock.of("Runnable::run"),
-                        CodeBlock.of("outcomeReceiver"),
-                    )
-                    addStatement {
-                        add("sdkSandboxManager.loadSdk(")
-                        add(loadSdkParameters.joinToCode())
-                        add(")")
-                    }
-                }
-            }
+            addStatement(
+                "return %T(%T.Stub.asInterface(binder))",
+                service.clientProxyNameSpec(), service.aidlInterfaceNameSpec()
+            )
         }
 }
-
-private object AndroidClassNames {
-    val context = ClassName("android.content", "Context")
-    val bundle = ClassName("android.os", "Bundle")
-    val outcomeReceiver = ClassName("android.os", "OutcomeReceiver")
-    val sandboxManager = ClassName("android.app.sdksandbox", "SdkSandboxManager")
-    val sandboxedSdk = ClassName("android.app.sdksandbox", "SandboxedSdk")
-    val loadSdkException = ClassName("android.app.sdksandbox", "LoadSdkException")
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
index 38b8a19..b756b24 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
@@ -17,10 +17,10 @@
 package androidx.privacysandbox.tools.apigenerator.parser
 
 import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxInterface
 import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.tools.PrivacySandboxValue
 import java.nio.file.Path
-import java.util.zip.ZipInputStream
 import kotlinx.metadata.KmClass
 import kotlinx.metadata.jvm.KotlinClassHeader
 import kotlinx.metadata.jvm.KotlinClassMetadata
@@ -34,7 +34,9 @@
     val services: Set<KmClass>,
     val values: Set<KmClass>,
     val callbacks: Set<KmClass>,
+    val interfaces: Set<KmClass>,
 )
+
 internal object AnnotatedClassReader {
     val annotations = listOf(PrivacySandboxService::class)
 
@@ -42,7 +44,11 @@
         val services = mutableSetOf<KmClass>()
         val values = mutableSetOf<KmClass>()
         val callbacks = mutableSetOf<KmClass>()
-        readClassNodes(stubClassPath).forEach { classNode ->
+        val interfaces = mutableSetOf<KmClass>()
+        stubClassPath.toFile().walk()
+            .filter { it.extension == "class" }
+            .map { toClassNode(it.readBytes()) }
+            .forEach { classNode ->
             if (classNode.isAnnotatedWith<PrivacySandboxService>()) {
                 services.add(parseKotlinMetadata(classNode))
             }
@@ -52,19 +58,18 @@
             if (classNode.isAnnotatedWith<PrivacySandboxCallback>()) {
                 callbacks.add(parseKotlinMetadata(classNode))
             }
+            if (classNode.isAnnotatedWith<PrivacySandboxInterface>()) {
+                interfaces.add(parseKotlinMetadata(classNode))
+            }
         }
-        return AnnotatedClasses(services.toSet(), values.toSet(), callbacks.toSet())
+        return AnnotatedClasses(
+            services = services.toSet(),
+            values = values.toSet(),
+            callbacks = callbacks.toSet(),
+            interfaces = interfaces.toSet()
+        )
     }
 
-    private fun readClassNodes(stubClassPath: Path): List<ClassNode> =
-        ZipInputStream(stubClassPath.toFile().inputStream()).use { input ->
-            generateSequence { input.nextEntry }
-                .filter { it.name.endsWith(".class") }
-                .map {
-                    toClassNode(input.readAllBytes())
-                }.toList()
-        }
-
     private fun toClassNode(classContents: ByteArray): ClassNode {
         val reader = ClassReader(classContents)
         val classNode = ClassNode(Opcodes.ASM9)
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
index 6a3fa930..1db70b6 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
@@ -37,13 +37,11 @@
     /**
      * Parses the API annotated by a Privacy Sandbox SDK from its compiled classes.
      *
-     * @param sdkInterfaceDescriptors Path to SDK interface descriptors. This should be a jar
-     *      file with a set of compiled SDK classes and at least one of them should be a valid
-     *      Kotlin interface annotated with @PrivacySandboxService.
+     * @param sdkStubsClasspath root directory of SDK classpath.
      */
-    internal fun parse(sdkInterfaceDescriptors: Path): ParsedApi {
-        val (services, values, callbacks) =
-            AnnotatedClassReader.readAnnotatedClasses(sdkInterfaceDescriptors)
+    internal fun parse(sdkStubsClasspath: Path): ParsedApi {
+        val (services, values, callbacks, interfaces) =
+            AnnotatedClassReader.readAnnotatedClasses(sdkStubsClasspath)
         if (services.isEmpty()) throw PrivacySandboxParsingException(
             "Unable to find valid interfaces annotated with @PrivacySandboxService."
         )
@@ -51,6 +49,7 @@
             services.map { parseInterface(it, "PrivacySandboxService") }.toSet(),
             values.map(::parseValue).toSet(),
             callbacks.map { parseInterface(it, "PrivacySandboxCallback") }.toSet(),
+            interfaces.map { parseInterface(it, "PrivacySandboxInterface") }.toSet(),
         ).also(::validate)
     }
 
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
index 005e8b9..a647718 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
@@ -16,10 +16,12 @@
 
 package androidx.privacysandbox.tools.apigenerator
 
+import androidx.privacysandbox.tools.core.Metadata
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
 import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
 import androidx.room.compiler.processing.util.Source
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import java.io.File
 import java.nio.file.Files
 import kotlin.io.path.Path
@@ -33,7 +35,8 @@
     private val generatedSources: List<Source> by lazy {
         val descriptors =
             compileIntoInterfaceDescriptorsJar(
-                *loadSourcesFromDirectory(inputDirectory).toTypedArray()
+                loadSourcesFromDirectory(inputDirectory),
+                mapOf(Metadata.filePath to Metadata.toolMetadata.toByteArray())
             )
         val aidlPath = System.getProperty("aidl_compiler_path")?.let(::Path)
             ?: throw IllegalArgumentException("aidl_compiler_path flag not set.")
@@ -47,7 +50,7 @@
 
     @Test
     fun generatedApi_compiles() {
-      assertCompiles(generatedSources)
+        assertCompiles(generatedSources)
     }
 
     @Test
@@ -60,7 +63,10 @@
 
         val outputSourceMap = generatedSources.associateBy(Source::relativePath)
         for (expected in expectedSources) {
-            assertThat(outputSourceMap[expected.relativePath]?.contents)
+            assertWithMessage(
+                "Contents of generated file %s don't match goldens.",
+                expected.relativePath
+            ).that(outputSourceMap[expected.relativePath]?.contents)
                 .isEqualTo(expected.contents)
         }
     }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
index bc17d9d..03bc25d 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
@@ -25,10 +25,9 @@
     override val inputDirectory = File("src/test/test-data/callbacks/input")
     override val outputDirectory = File("src/test/test-data/callbacks/output")
     override val relativePathsToExpectedAidlClasses = listOf(
-        "com/sdkwithcallbacks/ParcelableResponse.java",
+        "com/sdkwithcallbacks/IMyInterface.java",
         "com/sdkwithcallbacks/ISdkCallback.java",
-        "com/sdkwithcallbacks/ICancellationSignal.java",
-        "com/sdkwithcallbacks/IUnitTransactionCallback.java",
         "com/sdkwithcallbacks/ISdkService.java",
+        "com/sdkwithcallbacks/ParcelableResponse.java",
     )
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt
new file mode 100644
index 0000000..dea1e4a
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.apigenerator
+
+import java.io.File
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class InterfaceApiGeneratorTest : BaseApiGeneratorTest() {
+    override val inputDirectory = File("src/test/test-data/interfaces/input")
+    override val outputDirectory = File("src/test/test-data/interfaces/output")
+    override val relativePathsToExpectedAidlClasses = listOf(
+        "com/sdk/IMySdk.java",
+        "com/sdk/IMyInterface.java",
+        "com/sdk/IMySecondInterface.java",
+        "com/sdk/IMyInterfaceTransactionCallback.java",
+        "com/sdk/IIntTransactionCallback.java",
+        "com/sdk/ICancellationSignal.java",
+        "com/sdk/ParcelableStackFrame.java",
+        "com/sdk/PrivacySandboxThrowableParcel.java",
+    )
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
index 411a357..955a26b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
@@ -29,5 +29,7 @@
         "com/mysdk/IBooleanTransactionCallback.java",
         "com/mysdk/ICancellationSignal.java",
         "com/mysdk/IUnitTransactionCallback.java",
+        "com/mysdk/ParcelableStackFrame.java",
+        "com/mysdk/PrivacySandboxThrowableParcel.java",
     )
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
index 5c13a3f..fade128 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
@@ -19,15 +19,52 @@
 import androidx.privacysandbox.tools.apipackager.PrivacySandboxApiPackager
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
 import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
 import java.nio.file.Files.createTempDirectory
 import java.nio.file.Path
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createFile
+import kotlin.io.path.writeBytes
 
-fun compileIntoInterfaceDescriptorsJar(vararg sources: Source): Path {
+/**
+ * Compiles the given [sources] and creates a packaged SDK API descriptors jar.
+ *
+ * @param descriptorResources map of extra resources that will be added to descriptors jar keyed by
+ *      their relative path.
+ */
+fun compileIntoInterfaceDescriptorsJar(
+    sources: List<Source>,
+    descriptorResources: Map<Path, ByteArray> = mapOf(),
+): Path {
     val tempDir = createTempDirectory("compile").also { it.toFile().deleteOnExit() }
     val result = assertCompiles(sources.toList())
     val sdkInterfaceDescriptors = tempDir.resolve("sdk-interface-descriptors.jar")
+    val outputClasspath = mergedClasspath(result)
+    descriptorResources.forEach { (relativePath, contents) ->
+        outputClasspath.resolve(relativePath).apply {
+            parent?.createDirectories()
+            createFile()
+            writeBytes(contents)
+        }
+    }
     PrivacySandboxApiPackager().packageSdkDescriptors(
-        result.outputClasspath.first().toPath(), sdkInterfaceDescriptors
+        outputClasspath, sdkInterfaceDescriptors
     )
     return sdkInterfaceDescriptors
 }
+
+/**
+ * Merges all class paths from a compilation result into a single one.
+ *
+ * Room's compilation library returns different class paths for Kotlin and Java, so we need to
+ * merge them for tests that depend on the two. This is a naive implementation that simply
+ * overwrites classes that appear in multiple class paths.
+ */
+fun mergedClasspath(compilationResult: TestCompilationResult): Path {
+    val outputClasspath = createTempDirectory("classpath").also { it.toFile().deleteOnExit() }
+    compilationResult.outputClasspath
+        .forEach { classpath ->
+            classpath.copyRecursively(outputClasspath.toFile(), overwrite = true)
+        }
+    return outputClasspath
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
index c6a0b26..6eb5997 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
@@ -25,11 +25,14 @@
     override val inputDirectory = File("src/test/test-data/values/input")
     override val outputDirectory = File("src/test/test-data/values/output")
     override val relativePathsToExpectedAidlClasses = listOf(
+        "com/sdkwithvalues/IMyInterface.java",
         "com/sdkwithvalues/ISdkInterface.java",
         "com/sdkwithvalues/ISdkResponseTransactionCallback.java",
         "com/sdkwithvalues/ParcelableInnerSdkValue.java",
         "com/sdkwithvalues/ParcelableSdkRequest.java",
         "com/sdkwithvalues/ParcelableSdkResponse.java",
         "com/sdkwithvalues/ICancellationSignal.java",
+        "com/sdkwithvalues/ParcelableStackFrame.java",
+        "com/sdkwithvalues/PrivacySandboxThrowableParcel.java",
     )
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/VersionCompatibilityCheckTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/VersionCompatibilityCheckTest.kt
new file mode 100644
index 0000000..33cf59b
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/VersionCompatibilityCheckTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.apigenerator
+
+import androidx.privacysandbox.tools.core.Metadata
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
+import androidx.room.compiler.processing.util.Source
+import androidx.testutils.assertThrows
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.Path
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class VersionCompatibilityCheckTest {
+    private val validSources = listOf(
+        Source.kotlin(
+            "com/mysdk/TestSandboxSdk.kt", """
+                    package com.mysdk
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+                    @PrivacySandboxService
+                    interface MySdk {
+                      fun doSomething(number: Int)
+                    }
+                """
+        )
+    )
+
+    private val validMetadataContent = Metadata.toolMetadata.toByteArray()
+
+    @Test
+    @Ignore("b/255740194")
+    fun sdkDescriptorWithMissingMetadata_throws() {
+        assertThrows<IllegalArgumentException> {
+            runGeneratorWithResources(mapOf())
+        }.hasMessageThat().contains("Missing tool metadata in SDK API descriptor")
+    }
+
+    @Test
+    @Ignore("b/255740194")
+    fun sdkDescriptorWithMetadataInWrongPath_throws() {
+        assertThrows<IllegalArgumentException> {
+            runGeneratorWithResources(
+                mapOf(Path("invalid/dir/metadata.pb") to validMetadataContent)
+            )
+        }.hasMessageThat().contains("Missing tool metadata in SDK API descriptor")
+    }
+
+    @Test
+    @Ignore("b/255740194")
+    fun sdkDescriptorWithInvalidMetadataContent_throws() {
+        assertThrows<IllegalArgumentException> {
+            runGeneratorWithResources(
+                mapOf(Metadata.filePath to "bogus data".toByteArray())
+            )
+        }.hasMessageThat().contains("Invalid Privacy Sandbox tool metadata")
+    }
+
+    @Test
+    @Ignore("b/255740194")
+    fun sdkDescriptorWithIncompatibleVersion_throws() {
+        val sdkMetadata = ToolMetadata.newBuilder()
+            .setCodeGenerationVersion(999)
+            .build()
+        assertThrows<IllegalArgumentException> {
+            runGeneratorWithResources(
+                mapOf(Metadata.filePath to sdkMetadata.toByteArray())
+            )
+        }.hasMessageThat().contains(
+            "SDK uses incompatible Privacy Sandbox tooling (version 999)"
+        )
+    }
+
+    @Test
+    @Ignore("b/255740194")
+    fun sdkDescriptorWithLowerVersion_isCompatible() {
+        val sdkMetadata = ToolMetadata.newBuilder()
+            .setCodeGenerationVersion(0)
+            .build()
+        runGeneratorWithResources(mapOf(Metadata.filePath to sdkMetadata.toByteArray()))
+    }
+
+    private fun runGeneratorWithResources(resources: Map<Path, ByteArray>) {
+        val aidlPath = System.getProperty("aidl_compiler_path")?.let(::Path)
+            ?: throw IllegalArgumentException("aidl_compiler_path flag not set.")
+        val descriptors = compileIntoInterfaceDescriptorsJar(validSources, resources)
+        val generator = PrivacySandboxApiGenerator()
+        val outputDir = Files.createTempDirectory("output").also { it.toFile().deleteOnExit() }
+        generator.generate(descriptors, aidlPath, outputDir)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
index 3489840..cc006e8 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
@@ -16,7 +16,7 @@
 
 package androidx.privacysandbox.tools.apigenerator.parser
 
-import androidx.privacysandbox.tools.apigenerator.compileIntoInterfaceDescriptorsJar
+import androidx.privacysandbox.tools.apigenerator.mergedClasspath
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.AnnotatedValue
 import androidx.privacysandbox.tools.core.model.Method
@@ -25,6 +25,7 @@
 import androidx.privacysandbox.tools.core.model.Type
 import androidx.privacysandbox.tools.core.model.Types
 import androidx.privacysandbox.tools.core.model.ValueProperty
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
 import androidx.room.compiler.processing.util.Source
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
@@ -40,12 +41,18 @@
             "com/mysdk/TestSandboxSdk.kt", """
                     package com.mysdk
                     import androidx.privacysandbox.tools.PrivacySandboxCallback
+                    import androidx.privacysandbox.tools.PrivacySandboxInterface
                     import androidx.privacysandbox.tools.PrivacySandboxService
                     import androidx.privacysandbox.tools.PrivacySandboxValue
                     @PrivacySandboxService
                     interface MySdk {
                       fun doSomething(magicNumber: Int, awesomeString: String)
                       suspend fun getPayload(request: PayloadRequest): PayloadResponse
+                      suspend fun getInterface(): MyInterface
+                    }
+                    @PrivacySandboxInterface
+                    interface MyInterface {
+                      suspend fun getMorePayload(request: PayloadRequest): PayloadResponse
                     }
                     @PrivacySandboxValue
                     data class PayloadType(val size: Long, val appId: String)
@@ -96,6 +103,12 @@
                         isSuspend = false,
                     ),
                     Method(
+                        name = "getInterface",
+                        parameters = listOf(),
+                        returnType = Type("com.mysdk", "MyInterface"),
+                        isSuspend = true,
+                    ),
+                    Method(
                         name = "getPayload",
                         parameters = listOf(
                             Parameter(
@@ -108,7 +121,23 @@
                     )
                 )
             )
-
+        val expectedInterface =
+            AnnotatedInterface(
+                type = Type(packageName = "com.mysdk", simpleName = "MyInterface"),
+                methods = listOf(
+                    Method(
+                        name = "getMorePayload",
+                        parameters = listOf(
+                            Parameter(
+                                "request",
+                                expectedPayloadRequest.type
+                            )
+                        ),
+                        returnType = expectedPayloadResponse.type,
+                        isSuspend = true,
+                    )
+                )
+            )
         val expectedCallback = AnnotatedInterface(
             type = Type(packageName = "com.mysdk", simpleName = "CustomCallback"),
                 methods = listOf(
@@ -134,6 +163,7 @@
             expectedPayloadResponse,
         )
         assertThat(actualApi.callbacks).containsExactly(expectedCallback)
+        assertThat(actualApi.interfaces).containsExactly(expectedInterface)
     }
 
     @Test
@@ -335,7 +365,7 @@
     }
 
     private fun compileAndParseApi(vararg sources: Source): ParsedApi {
-        compileIntoInterfaceDescriptorsJar(*sources)
-        return ApiStubParser.parse(compileIntoInterfaceDescriptorsJar(*sources))
+        val classpath = mergedClasspath(assertCompiles(sources.toList()))
+        return ApiStubParser.parse(classpath)
     }
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt
index 2bc8ff1..c4be47f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt
@@ -3,10 +3,11 @@
 import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.tools.PrivacySandboxCallback
 import androidx.privacysandbox.tools.PrivacySandboxValue
+import androidx.privacysandbox.tools.PrivacySandboxInterface
 
 @PrivacySandboxService
 interface SdkService {
-    suspend fun registerCallback(callback: SdkCallback)
+    fun registerCallback(callback: SdkCallback)
 }
 
 @PrivacySandboxCallback
@@ -16,7 +17,14 @@
     fun onPrimitivesReceived(x: Int, y: Int)
 
     fun onEmptyEvent()
+
+    fun onCompleteInterface(myInterface: MyInterface)
 }
 
 @PrivacySandboxValue
 data class Response(val response: String)
+
+@PrivacySandboxInterface
+interface MyInterface {
+    fun doStuff()
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterface.kt
new file mode 100644
index 0000000..0474314
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterface.kt
@@ -0,0 +1,5 @@
+package com.sdkwithcallbacks
+
+public interface MyInterface {
+    public fun doStuff(): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterfaceClientProxy.kt
new file mode 100644
index 0000000..f325ee4
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterfaceClientProxy.kt
@@ -0,0 +1,9 @@
+package com.sdkwithcallbacks
+
+public class MyInterfaceClientProxy(
+    public val remote: IMyInterface,
+) : MyInterface {
+    public override fun doStuff(): Unit {
+        remote.doStuff()
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt
index da06ec7..1e6c2d4 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt
@@ -1,6 +1,8 @@
 package com.sdkwithcallbacks
 
 public interface SdkCallback {
+    public fun onCompleteInterface(myInterface: MyInterface): Unit
+
     public fun onEmptyEvent(): Unit
 
     public fun onPrimitivesReceived(x: Int, y: Int): Unit
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt
index a15d0c9..f4043ef 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt
@@ -7,6 +7,10 @@
 public class SdkCallbackStubDelegate internal constructor(
   public val `delegate`: SdkCallback,
 ) : ISdkCallback.Stub() {
+  public override fun onCompleteInterface(myInterface: IMyInterface): Unit {
+    delegate.onCompleteInterface(MyInterfaceClientProxy(myInterface))
+  }
+
   public override fun onEmptyEvent(): Unit {
     delegate.onEmptyEvent()
   }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt
index 8363e3d..510bd89 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt
@@ -1,5 +1,5 @@
 package com.sdkwithcallbacks
 
 public interface SdkService {
-    public suspend fun registerCallback(callback: SdkCallback): Unit
+    public fun registerCallback(callback: SdkCallback): Unit
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceClientProxy.kt
index 4ed15f2..83a8df2 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceClientProxy.kt
@@ -1,32 +1,9 @@
 package com.sdkwithcallbacks
 
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
-
 public class SdkServiceClientProxy(
-    private val remote: ISdkService,
+    public val remote: ISdkService,
 ) : SdkService {
-    public override suspend fun registerCallback(callback: SdkCallback): Unit =
-            suspendCancellableCoroutine {
-        var mCancellationSignal: ICancellationSignal? = null
-        val transactionCallback = object: IUnitTransactionCallback.Stub() {
-            override fun onCancellable(cancellationSignal: ICancellationSignal) {
-                if (it.isCancelled) {
-                    cancellationSignal.cancel()
-                }
-                mCancellationSignal = cancellationSignal
-            }
-            override fun onSuccess() {
-                it.resumeWith(Result.success(Unit))
-            }
-            override fun onFailure(errorCode: Int, errorMessage: String) {
-                it.resumeWithException(RuntimeException(errorMessage))
-            }
-        }
-        remote.registerCallback(SdkCallbackStubDelegate(callback), transactionCallback)
-        it.invokeOnCancellation {
-            mCancellationSignal?.cancel()
-        }
+    public override fun registerCallback(callback: SdkCallback): Unit {
+        remote.registerCallback(SdkCallbackStubDelegate(callback))
     }
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt
index 0936943..0a284a0 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt
@@ -1,24 +1,6 @@
 package com.sdkwithcallbacks
 
-import android.app.sdksandbox.LoadSdkException
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SdkSandboxManager
-import android.content.Context
-import android.os.Bundle
-import android.os.OutcomeReceiver
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
+import android.os.IBinder
 
-public suspend fun createSdkService(context: Context): SdkService = suspendCancellableCoroutine {
-    val sdkSandboxManager = context.getSystemService(SdkSandboxManager::class.java)
-    val outcomeReceiver = object: OutcomeReceiver<SandboxedSdk, LoadSdkException> {
-        override fun onResult(result: SandboxedSdk) {
-            it.resume(SdkServiceClientProxy(ISdkService.Stub.asInterface(result.getInterface())))
-        }
-        override fun onError(error: LoadSdkException) {
-            it.resumeWithException(error)
-        }
-    }
-    sdkSandboxManager.loadSdk("com.sdkwithcallbacks", Bundle.EMPTY, Runnable::run, outcomeReceiver)
-}
+public fun wrapToSdkService(binder: IBinder): SdkService =
+        SdkServiceClientProxy(ISdkService.Stub.asInterface(binder))
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
new file mode 100644
index 0000000..fce47b8
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
@@ -0,0 +1,21 @@
+package com.sdk
+
+import androidx.privacysandbox.tools.PrivacySandboxInterface
+import androidx.privacysandbox.tools.PrivacySandboxService
+
+@PrivacySandboxService
+interface MySdk {
+    suspend fun getInterface(): MyInterface
+}
+
+@PrivacySandboxInterface
+interface MyInterface {
+    suspend fun add(x: Int, y: Int): Int
+
+    fun doSomething(firstInterface: MyInterface, secondInterface: MySecondInterface)
+}
+
+@PrivacySandboxInterface
+interface MySecondInterface {
+   fun doStuff()
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt
new file mode 100644
index 0000000..155dec9
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt
@@ -0,0 +1,7 @@
+package com.sdk
+
+public interface MyInterface {
+    public suspend fun add(x: Int, y: Int): Int
+
+    public fun doSomething(firstInterface: MyInterface, secondInterface: MySecondInterface): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt
new file mode 100644
index 0000000..00b33dd
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt
@@ -0,0 +1,37 @@
+package com.sdk
+
+import com.sdk.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+public class MyInterfaceClientProxy(
+    public val remote: IMyInterface,
+) : MyInterface {
+    public override suspend fun add(x: Int, y: Int): Int = suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IIntTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: Int) {
+                it.resumeWith(Result.success(result))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.add(x, y, transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
+    public override fun doSomething(firstInterface: MyInterface,
+            secondInterface: MySecondInterface): Unit {
+        remote.doSomething((firstInterface as MyInterfaceClientProxy).remote, (secondInterface as
+                MySecondInterfaceClientProxy).remote)
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdk.kt
new file mode 100644
index 0000000..967cd6d8
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdk.kt
@@ -0,0 +1,5 @@
+package com.sdk
+
+public interface MySdk {
+    public suspend fun getInterface(): MyInterface
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkClientProxy.kt
new file mode 100644
index 0000000..3a6ae61
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkClientProxy.kt
@@ -0,0 +1,31 @@
+package com.sdk
+
+import com.sdk.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+public class MySdkClientProxy(
+    public val remote: IMySdk,
+) : MySdk {
+    public override suspend fun getInterface(): MyInterface = suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IMyInterfaceTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: IMyInterface) {
+                it.resumeWith(Result.success(MyInterfaceClientProxy(result)))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.getInterface(transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkFactory.kt
new file mode 100644
index 0000000..014a616
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkFactory.kt
@@ -0,0 +1,5 @@
+package com.sdk
+
+import android.os.IBinder
+
+public fun wrapToMySdk(binder: IBinder): MySdk = MySdkClientProxy(IMySdk.Stub.asInterface(binder))
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt
new file mode 100644
index 0000000..b858ed5
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt
@@ -0,0 +1,5 @@
+package com.sdk
+
+public interface MySecondInterface {
+    public fun doStuff(): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
new file mode 100644
index 0000000..67e7d28a
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
@@ -0,0 +1,9 @@
+package com.sdk
+
+public class MySecondInterfaceClientProxy(
+    public val remote: IMySecondInterface,
+) : MySecondInterface {
+    public override fun doStuff(): Unit {
+        remote.doStuff()
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxException.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxException.kt
new file mode 100644
index 0000000..1e0a20d
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxException.kt
@@ -0,0 +1,8 @@
+package com.sdk
+
+import java.lang.RuntimeException
+
+public class PrivacySandboxException(
+    public override val message: String?,
+    public override val cause: Throwable?,
+) : RuntimeException()
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxThrowableParcelConverter.kt
new file mode 100644
index 0000000..4b0857c
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxThrowableParcelConverter.kt
@@ -0,0 +1,27 @@
+package com.sdk
+
+import java.lang.StackTraceElement
+
+public object PrivacySandboxThrowableParcelConverter {
+    public fun fromThrowableParcel(throwableParcel: PrivacySandboxThrowableParcel): Throwable {
+        val exceptionClass = throwableParcel.exceptionClass
+        val errorMessage = throwableParcel.errorMessage
+        val stackTrace = throwableParcel.stackTrace
+        val exception = PrivacySandboxException(
+            "[$exceptionClass] $errorMessage",
+            throwableParcel.cause?.let { fromThrowableParcel(it) })
+        for (suppressed in throwableParcel.suppressedExceptions) {
+            exception.addSuppressed(fromThrowableParcel(suppressed))
+        }
+        exception.stackTrace =
+            stackTrace.map {
+                StackTraceElement(
+                    it.declaringClass,
+                    it.methodName,
+                    it.fileName,
+                    it.lineNumber
+                )
+            }.toTypedArray()
+        return exception
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxException.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxException.kt
new file mode 100644
index 0000000..d165932
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxException.kt
@@ -0,0 +1,8 @@
+package com.mysdk
+
+import java.lang.RuntimeException
+
+public class PrivacySandboxException(
+    public override val message: String?,
+    public override val cause: Throwable?,
+) : RuntimeException()
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
new file mode 100644
index 0000000..42297ec
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
@@ -0,0 +1,27 @@
+package com.mysdk
+
+import java.lang.StackTraceElement
+
+public object PrivacySandboxThrowableParcelConverter {
+    public fun fromThrowableParcel(throwableParcel: PrivacySandboxThrowableParcel): Throwable {
+        val exceptionClass = throwableParcel.exceptionClass
+        val errorMessage = throwableParcel.errorMessage
+        val stackTrace = throwableParcel.stackTrace
+        val exception = PrivacySandboxException(
+            "[$exceptionClass] $errorMessage",
+            throwableParcel.cause?.let { fromThrowableParcel(it) })
+        for (suppressed in throwableParcel.suppressedExceptions) {
+            exception.addSuppressed(fromThrowableParcel(suppressed))
+        }
+        exception.stackTrace =
+            stackTrace.map {
+                StackTraceElement(
+                    it.declaringClass,
+                    it.methodName,
+                    it.fileName,
+                    it.lineNumber
+                )
+            }.toTypedArray()
+        return exception
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
index 607b29e..2d5b21d 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
@@ -1,11 +1,12 @@
 package com.mysdk
 
-import kotlin.coroutines.resume
+import com.mysdk.PrivacySandboxThrowableParcelConverter
+import com.mysdk.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
 import kotlin.coroutines.resumeWithException
 import kotlinx.coroutines.suspendCancellableCoroutine
 
 public class TestSandboxSdkClientProxy(
-    private val remote: ITestSandboxSdk,
+    public val remote: ITestSandboxSdk,
 ) : TestSandboxSdk {
     public override suspend fun doSomethingAsync(
         first: Int,
@@ -23,8 +24,8 @@
             override fun onSuccess(result: Boolean) {
                 it.resumeWith(Result.success(result))
             }
-            override fun onFailure(errorCode: Int, errorMessage: String) {
-                it.resumeWithException(RuntimeException(errorMessage))
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
             }
         }
         remote.doSomethingAsync(first, second, third, transactionCallback)
@@ -77,8 +78,8 @@
             override fun onSuccess() {
                 it.resumeWith(Result.success(Unit))
             }
-            override fun onFailure(errorCode: Int, errorMessage: String) {
-                it.resumeWithException(RuntimeException(errorMessage))
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
             }
         }
         remote.receiveAndReturnNothingAsync(transactionCallback)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkFactory.kt
index 6bc1a04..db02846 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkFactory.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkFactory.kt
@@ -1,25 +1,6 @@
 package com.mysdk
 
-import android.app.sdksandbox.LoadSdkException
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SdkSandboxManager
-import android.content.Context
-import android.os.Bundle
-import android.os.OutcomeReceiver
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
+import android.os.IBinder
 
-public suspend fun createTestSandboxSdk(context: Context): TestSandboxSdk =
-        suspendCancellableCoroutine {
-    val sdkSandboxManager = context.getSystemService(SdkSandboxManager::class.java)
-    val outcomeReceiver = object: OutcomeReceiver<SandboxedSdk, LoadSdkException> {
-        override fun onResult(result: SandboxedSdk) {
-            it.resume(TestSandboxSdkClientProxy(ITestSandboxSdk.Stub.asInterface(result.getInterface())))
-        }
-        override fun onError(error: LoadSdkException) {
-            it.resumeWithException(error)
-        }
-    }
-    sdkSandboxManager.loadSdk("com.mysdk", Bundle.EMPTY, Runnable::run, outcomeReceiver)
-}
+public fun wrapToTestSandboxSdk(binder: IBinder): TestSandboxSdk =
+        TestSandboxSdkClientProxy(ITestSandboxSdk.Stub.asInterface(binder))
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
index 1fd19d0..6265564 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
@@ -1,5 +1,6 @@
 package com.sdkwithvalues
 
+import androidx.privacysandbox.tools.PrivacySandboxInterface
 import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.tools.PrivacySandboxValue
 
@@ -17,6 +18,7 @@
     val message: String,
     val floatingPoint: Float,
     val hugeNumber: Double,
+    val myInterface: MyInterface,
 )
 
 @PrivacySandboxValue
@@ -24,3 +26,8 @@
 
 @PrivacySandboxValue
 data class SdkResponse(val success: Boolean, val originalRequest: SdkRequest)
+
+@PrivacySandboxInterface
+interface MyInterface {
+    fun doStuff()
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
index 295f659..dc6d855 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
@@ -6,6 +6,7 @@
     public val hugeNumber: Double,
     public val id: Int,
     public val message: String,
+    public val myInterface: MyInterface,
     public val separator: Char,
     public val shouldBeAwesome: Boolean,
 )
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
index 7bb55c5..ffa0b12 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
@@ -8,6 +8,7 @@
                 hugeNumber = parcelable.hugeNumber,
                 id = parcelable.id,
                 message = parcelable.message,
+                myInterface = MyInterfaceClientProxy(parcelable.myInterface),
                 separator = parcelable.separator,
                 shouldBeAwesome = parcelable.shouldBeAwesome)
         return annotatedValue
@@ -20,6 +21,7 @@
         parcelable.hugeNumber = annotatedValue.hugeNumber
         parcelable.id = annotatedValue.id
         parcelable.message = annotatedValue.message
+        parcelable.myInterface = (annotatedValue.myInterface as MyInterfaceClientProxy).remote
         parcelable.separator = annotatedValue.separator
         parcelable.shouldBeAwesome = annotatedValue.shouldBeAwesome
         return parcelable
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterface.kt
new file mode 100644
index 0000000..b7db6ef
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterface.kt
@@ -0,0 +1,5 @@
+package com.sdkwithvalues
+
+public interface MyInterface {
+    public fun doStuff(): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterfaceClientProxy.kt
new file mode 100644
index 0000000..2fd68e4
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterfaceClientProxy.kt
@@ -0,0 +1,9 @@
+package com.sdkwithvalues
+
+public class MyInterfaceClientProxy(
+    public val remote: IMyInterface,
+) : MyInterface {
+    public override fun doStuff(): Unit {
+        remote.doStuff()
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxException.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxException.kt
new file mode 100644
index 0000000..7597726
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxException.kt
@@ -0,0 +1,8 @@
+package com.sdkwithvalues
+
+import java.lang.RuntimeException
+
+public class PrivacySandboxException(
+    public override val message: String?,
+    public override val cause: Throwable?,
+) : RuntimeException()
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxThrowableParcelConverter.kt
new file mode 100644
index 0000000..34de1d7
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxThrowableParcelConverter.kt
@@ -0,0 +1,27 @@
+package com.sdkwithvalues
+
+import java.lang.StackTraceElement
+
+public object PrivacySandboxThrowableParcelConverter {
+    public fun fromThrowableParcel(throwableParcel: PrivacySandboxThrowableParcel): Throwable {
+        val exceptionClass = throwableParcel.exceptionClass
+        val errorMessage = throwableParcel.errorMessage
+        val stackTrace = throwableParcel.stackTrace
+        val exception = PrivacySandboxException(
+            "[$exceptionClass] $errorMessage",
+            throwableParcel.cause?.let { fromThrowableParcel(it) })
+        for (suppressed in throwableParcel.suppressedExceptions) {
+            exception.addSuppressed(fromThrowableParcel(suppressed))
+        }
+        exception.stackTrace =
+            stackTrace.map {
+                StackTraceElement(
+                    it.declaringClass,
+                    it.methodName,
+                    it.fileName,
+                    it.lineNumber
+                )
+            }.toTypedArray()
+        return exception
+    }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
index 660db1b..1d6fada 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
@@ -1,13 +1,13 @@
 package com.sdkwithvalues
 
+import com.sdkwithvalues.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
 import com.sdkwithvalues.SdkRequestConverter.toParcelable
 import com.sdkwithvalues.SdkResponseConverter.fromParcelable
-import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
 import kotlinx.coroutines.suspendCancellableCoroutine
 
 public class SdkInterfaceClientProxy(
-    private val remote: ISdkInterface,
+    public val remote: ISdkInterface,
 ) : SdkInterface {
     public override suspend fun exampleMethod(request: SdkRequest): SdkResponse =
             suspendCancellableCoroutine {
@@ -22,8 +22,8 @@
             override fun onSuccess(result: ParcelableSdkResponse) {
                 it.resumeWith(Result.success(fromParcelable(result)))
             }
-            override fun onFailure(errorCode: Int, errorMessage: String) {
-                it.resumeWithException(RuntimeException(errorMessage))
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
             }
         }
         remote.exampleMethod(toParcelable(request), transactionCallback)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceFactory.kt
index bc79005..6a84537 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceFactory.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceFactory.kt
@@ -1,25 +1,6 @@
 package com.sdkwithvalues
 
-import android.app.sdksandbox.LoadSdkException
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SdkSandboxManager
-import android.content.Context
-import android.os.Bundle
-import android.os.OutcomeReceiver
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
+import android.os.IBinder
 
-public suspend fun createSdkInterface(context: Context): SdkInterface =
-        suspendCancellableCoroutine {
-    val sdkSandboxManager = context.getSystemService(SdkSandboxManager::class.java)
-    val outcomeReceiver = object: OutcomeReceiver<SandboxedSdk, LoadSdkException> {
-        override fun onResult(result: SandboxedSdk) {
-            it.resume(SdkInterfaceClientProxy(ISdkInterface.Stub.asInterface(result.getInterface())))
-        }
-        override fun onError(error: LoadSdkException) {
-            it.resumeWithException(error)
-        }
-    }
-    sdkSandboxManager.loadSdk("com.sdkwithvalues", Bundle.EMPTY, Runnable::run, outcomeReceiver)
-}
+public fun wrapToSdkInterface(binder: IBinder): SdkInterface =
+        SdkInterfaceClientProxy(ISdkInterface.Stub.asInterface(binder))
diff --git a/privacysandbox/tools/tools-apipackager/build.gradle b/privacysandbox/tools/tools-apipackager/build.gradle
index 4bcd717..812d48a 100644
--- a/privacysandbox/tools/tools-apipackager/build.gradle
+++ b/privacysandbox/tools/tools-apipackager/build.gradle
@@ -28,6 +28,7 @@
     implementation(libs.asmCommons)
 
     implementation project(path: ':privacysandbox:tools:tools')
+    implementation project(path: ':privacysandbox:tools:tools-core')
 
     testImplementation(project(":internal-testutils-truth"))
     testImplementation(project(":privacysandbox:tools:tools-testing"))
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
index 0a1ad9f..5d8437f 100644
--- a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
@@ -17,9 +17,11 @@
 package androidx.privacysandbox.tools.apipackager
 
 import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxInterface
 import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.tools.PrivacySandboxValue
-import java.io.File
+import java.nio.file.Path
+import kotlin.io.path.readBytes
 import org.objectweb.asm.AnnotationVisitor
 import org.objectweb.asm.ClassReader
 import org.objectweb.asm.ClassVisitor
@@ -29,11 +31,12 @@
 internal object AnnotationInspector {
     private val privacySandboxAnnotations = setOf(
         PrivacySandboxCallback::class,
+        PrivacySandboxInterface::class,
         PrivacySandboxService::class,
         PrivacySandboxValue::class,
     )
 
-    fun hasPrivacySandboxAnnotation(classFile: File): Boolean {
+    fun hasPrivacySandboxAnnotation(classFile: Path): Boolean {
         val reader = ClassReader(classFile.readBytes())
         val annotationExtractor = AnnotationExtractor()
         reader.accept(
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
index cb5f049..382927c 100644
--- a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
@@ -23,6 +23,9 @@
 import kotlin.io.path.exists
 import kotlin.io.path.isDirectory
 import kotlin.io.path.notExists
+import androidx.privacysandbox.tools.core.Metadata
+import kotlin.io.path.extension
+import kotlin.io.path.inputStream
 
 class PrivacySandboxApiPackager {
 
@@ -55,14 +58,24 @@
 
         ZipOutputStream(outputFile.outputStream()).use { zipOutputStream ->
             sdkClasspath.toFile().walk()
-                .filter { it.extension == "class" }
-                .filter { hasPrivacySandboxAnnotation(it) }
-                .forEach { classFile ->
-                    val zipEntry = ZipEntry(sdkClasspath.relativize(classFile.toPath()).toString())
+                .map { it.toPath() }
+                .filter { shouldKeepFile(sdkClasspath, it) }
+                .forEach { file ->
+                    val zipEntry = ZipEntry(sdkClasspath.relativize(file).toString())
                     zipOutputStream.putNextEntry(zipEntry)
-                    classFile.inputStream().copyTo(zipOutputStream)
+                    file.inputStream().copyTo(zipOutputStream)
                     zipOutputStream.closeEntry()
                 }
         }
     }
+
+    private fun shouldKeepFile(sdkClasspath: Path, filePath: Path): Boolean {
+        if (sdkClasspath.relativize(filePath) == Metadata.filePath) {
+            return true
+        }
+        if (filePath.extension == "class" && hasPrivacySandboxAnnotation(filePath)) {
+            return true
+        }
+        return false
+    }
 }
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
index 5469653..10a9cad 100644
--- a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -16,6 +16,8 @@
 
 package androidx.privacysandbox.tools.apipackager
 
+import androidx.privacysandbox.tools.core.Metadata
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.compileAll
 import androidx.room.compiler.processing.util.Source
@@ -26,7 +28,10 @@
 import java.nio.file.Files
 import java.nio.file.Path
 import java.util.zip.ZipInputStream
+import kotlin.io.path.createDirectories
 import kotlin.io.path.createFile
+import kotlin.io.path.inputStream
+import kotlin.io.path.outputStream
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -116,6 +121,38 @@
             )
         assertThat(compileWithExtraClasspath(appSource, packagedSdkClasspath)).succeeds()
     }
+    @Test
+    fun sdkClasspathWithMetadataFile_isKeptInDescriptor() {
+        val metadata = ToolMetadata.newBuilder()
+            .setCodeGenerationVersion(42)
+            .build()
+        val source = Source.kotlin(
+            "com/mysdk/Valid.kt", """
+            |package com.mysdk
+            |interface Valid
+        """.trimMargin()
+        )
+        val sdkClasspath = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
+            .outputClasspath.first().toPath()
+
+        val metadataPath = sdkClasspath.resolve(Metadata.filePath).also {
+            it.parent.createDirectories()
+            it.createFile()
+        }
+        metadata.writeTo(metadataPath.outputStream())
+
+        val sdkDescriptor = makeTestDirectory().resolve("sdk-descriptors.jar")
+        PrivacySandboxApiPackager().packageSdkDescriptors(sdkClasspath, sdkDescriptor)
+        val packagedMetadata =
+            ZipInputStream(sdkDescriptor.inputStream()).use { input ->
+            generateSequence { input.nextEntry }
+                .filter { it.name == Metadata.filePath.toString() }
+                .map { ToolMetadata.parseFrom(input.readAllBytes()) }
+                .toList()
+        }
+
+        assertThat(packagedMetadata).containsExactly(metadata)
+    }
 
     @Test
     fun sdkClasspathDoesNotExist_throwException() {
@@ -162,9 +199,9 @@
         }
     }
 
-    /** Compiles the given source files and returns a classpath with the results. */
-    private fun compileAndReturnUnzippedPackagedClasspath(vararg sources: Source): File {
-        val result = compileAll(sources.toList(), includePrivacySandboxPlatformSources = false)
+    /** Compiles the given source file and returns a classpath with the results. */
+    private fun compileAndReturnUnzippedPackagedClasspath(source: Source): File {
+        val result = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
         assertThat(result).succeeds()
         assertThat(result.outputClasspath).hasSize(1)
 
@@ -173,7 +210,7 @@
         PrivacySandboxApiPackager().packageSdkDescriptors(originalClasspath, descriptors)
 
         val outputDir = makeTestDirectory().toFile()
-        ZipInputStream(descriptors.toFile().inputStream()).use { input ->
+        ZipInputStream(descriptors.inputStream()).use { input ->
             generateSequence { input.nextEntry }
                 .forEach {
                     val file: File = outputDir.resolve(it.name)
diff --git a/privacysandbox/tools/tools-core/build.gradle b/privacysandbox/tools/tools-core/build.gradle
index af7b5c3..f18b9ba 100644
--- a/privacysandbox/tools/tools-core/build.gradle
+++ b/privacysandbox/tools/tools-core/build.gradle
@@ -22,11 +22,13 @@
 plugins {
     id("AndroidXPlugin")
     id("kotlin")
+    id("com.google.protobuf")
 }
 
 dependencies {
     api(libs.kotlinStdlib)
-    implementation(libs.kotlinPoet)
+    api(libs.protobufLite)
+    api(libs.kotlinPoet)
 
     testImplementation(libs.junit)
     testImplementation(libs.truth)
@@ -49,6 +51,21 @@
     }
 }
 
+protobuf {
+    protoc {
+        artifact = libs.protobufCompiler.get()
+    }
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                java {
+                    option "lite"
+                }
+            }
+        }
+    }
+}
+
 androidx {
     name = "androidx.privacysandbox.tools:tools-core"
     type = LibraryType.ANNOTATION_PROCESSOR_UTILS
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/Metadata.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/Metadata.kt
new file mode 100644
index 0000000..06a088a
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/Metadata.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core
+
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
+import kotlin.io.path.Path
+
+/** Privacy Sandbox Tool metadata constants. */
+object Metadata {
+    /** Tool metadata message. It's serialized and stored in every SDK API descriptor. */
+    val toolMetadata: ToolMetadata =
+        ToolMetadata.newBuilder()
+            .setCodeGenerationVersion(1)
+            .build()
+
+    /** Relative path to metadata file in SDK API descriptor jar. */
+    val filePath = Path("META-INF/privacysandbox/tool-metadata.pb")
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
index d61b8e8..4c245ac 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
@@ -20,7 +20,7 @@
 import androidx.privacysandbox.tools.core.generator.poet.AidlInterfaceSpec
 import androidx.privacysandbox.tools.core.generator.poet.AidlInterfaceSpec.Companion.aidlInterface
 import androidx.privacysandbox.tools.core.generator.poet.AidlMethodSpec
-import androidx.privacysandbox.tools.core.generator.poet.AidlParcelableSpec
+import androidx.privacysandbox.tools.core.generator.poet.AidlParcelableSpec.Companion.aidlParcelable
 import androidx.privacysandbox.tools.core.generator.poet.AidlTypeSpec
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.AnnotatedValue
@@ -30,6 +30,7 @@
 import androidx.privacysandbox.tools.core.model.Type
 import androidx.privacysandbox.tools.core.model.Types
 import androidx.privacysandbox.tools.core.model.getOnlyService
+import androidx.privacysandbox.tools.core.model.hasSuspendFunctions
 import java.io.File
 import java.nio.file.Path
 import java.nio.file.Paths
@@ -53,6 +54,8 @@
         }
 
         const val cancellationSignalName = "ICancellationSignal"
+        const val throwableParcelName = "PrivacySandboxThrowableParcel"
+        const val parcelableStackFrameName = "ParcelableStackFrame"
     }
 
     private fun generate(): List<GeneratedSource> {
@@ -91,12 +94,11 @@
 
     private fun generateAidlContent(): List<AidlFileSpec> {
         val values = api.values.map(::generateValue)
-        val transactionCallbacks = generateTransactionCallbacks()
         val service = aidlInterface(api.getOnlyService())
         val customCallbacks = api.callbacks.map(::aidlInterface)
         val interfaces = api.interfaces.map(::aidlInterface)
-        return transactionCallbacks +
-            generateICancellationSignal() +
+        val suspendFunctionUtilities = generateSuspendFunctionUtilities()
+        return suspendFunctionUtilities +
             service +
             values +
             customCallbacks +
@@ -128,6 +130,14 @@
         )
     }
 
+    private fun generateSuspendFunctionUtilities(): List<AidlFileSpec> {
+        if (!api.hasSuspendFunctions()) return emptyList()
+        return generateTransactionCallbacks() +
+            generateParcelableFailure() +
+            generateParcelableStackTrace() +
+            generateICancellationSignal()
+    }
+
     private fun generateTransactionCallbacks(): List<AidlFileSpec> {
         val annotatedInterfaces = api.services + api.interfaces
         return annotatedInterfaces
@@ -146,8 +156,7 @@
                 if (type != Types.unit) addParameter(Parameter("result", type))
             }
             addMethod("onFailure") {
-                addParameter("errorCode", primitive("int"))
-                addParameter("errorMessage", primitive("String"))
+                addParameter("throwableParcel", AidlTypeSpec(throwableParcelType()), isIn = true)
             }
         }
     }
@@ -156,14 +165,33 @@
         addMethod("cancel")
     }
 
+    private fun generateParcelableFailure(): AidlFileSpec {
+        return aidlParcelable(throwableParcelType()) {
+            addProperty("exceptionClass", primitive("String"))
+            addProperty("errorMessage", primitive("String"))
+            addProperty("stackTrace", AidlTypeSpec(parcelableStackFrameType(), isList = true))
+            addProperty("cause", AidlTypeSpec(throwableParcelType()), isNullable = true)
+            addProperty(
+                "suppressedExceptions", AidlTypeSpec(throwableParcelType(), isList = true)
+            )
+        }
+    }
+
+    private fun generateParcelableStackTrace(): AidlFileSpec {
+        return aidlParcelable(parcelableStackFrameType()) {
+            addProperty("declaringClass", primitive("String"))
+            addProperty("methodName", primitive("String"))
+            addProperty("fileName", primitive("String"))
+            addProperty("lineNumber", primitive("int"))
+        }
+    }
+
     private fun generateValue(value: AnnotatedValue): AidlFileSpec {
-        val typesToImport =
-            value.properties.mapNotNull { api.valueMap[it.type]?.aidlType()?.innerType }.toSet()
-        return AidlParcelableSpec(type = value.aidlType().innerType,
-            typesToImport = typesToImport,
-            properties = value.properties.map {
-                "${getAidlTypeDeclaration(it.type)} ${it.name};"
-            })
+        return aidlParcelable(value.aidlType().innerType) {
+            for (property in value.properties) {
+                addProperty(property.name, getAidlTypeDeclaration(property.type))
+            }
+        }
     }
 
     private fun getAidlFile(rootPath: Path, aidlSource: AidlFileSpec) = Paths.get(
@@ -181,6 +209,8 @@
 
     private fun packageName() = api.getOnlyService().type.packageName
     private fun cancellationSignalType() = AidlTypeSpec(Type(packageName(), cancellationSignalName))
+    private fun throwableParcelType() = Type(packageName(), throwableParcelName)
+    private fun parcelableStackFrameType() = Type(packageName(), parcelableStackFrameName)
     private fun transactionCallback(type: Type) =
         AidlTypeSpec(Type(api.getOnlyService().type.packageName, type.transactionCallbackName()))
 
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
index 1b0e9f8..c791c9c 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
@@ -16,7 +16,7 @@
 
 package androidx.privacysandbox.tools.core.generator
 
-import androidx.privacysandbox.tools.core.model.Parameter
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.model.Type
 import androidx.privacysandbox.tools.core.model.Types
@@ -24,8 +24,7 @@
 import com.squareup.kotlinpoet.CodeBlock
 
 /** Utility to generate [CodeBlock]s that convert values to/from their binder equivalent. */
-class BinderCodeConverter(private val api: ParsedApi) {
-
+abstract class BinderCodeConverter(private val api: ParsedApi) {
     /**
      * Generate a block that converts the given expression from the binder representation to its
      * model equivalent.
@@ -47,20 +46,15 @@
         }
         val sandboxInterface = api.interfaceMap[type]
         if (sandboxInterface != null) {
-            return CodeBlock.of(
-                "(%L as %T).delegate", expression, sandboxInterface.stubDelegateNameSpec())
+            return convertToInterfaceModelCode(sandboxInterface, expression)
         }
         return CodeBlock.of(expression)
     }
 
-    /**
-     * Convert the given [Parameter] from the binder representation to its model equivalent.
-     *
-     * See [convertToModelCode].
-     */
-    fun convertToModelCode(parameter: Parameter): CodeBlock {
-        return convertToModelCode(parameter.type, parameter.name)
-    }
+    protected abstract fun convertToInterfaceModelCode(
+        annotatedInterface: AnnotatedInterface,
+        expression: String
+    ): CodeBlock
 
     /**
      * Generate a block that converts the given expression from the model representation to its
@@ -83,19 +77,15 @@
         }
         val sandboxInterface = api.interfaceMap[type]
         if (sandboxInterface != null) {
-            return CodeBlock.of("%T(%L)", sandboxInterface.stubDelegateNameSpec(), expression)
+            return convertToInterfaceBinderCode(sandboxInterface, expression)
         }
         return CodeBlock.of(expression)
     }
 
-    /**
-     * Convert the given [Parameter] from the model representation to its binder equivalent.
-     *
-     * See [convertToBinderCode].
-     */
-    fun convertToBinderCode(parameter: Parameter): CodeBlock {
-        return convertToBinderCode(parameter.type, parameter.name)
-    }
+    protected abstract fun convertToInterfaceBinderCode(
+        annotatedInterface: AnnotatedInterface,
+        expression: String
+    ): CodeBlock
 
     /** Convert the given model type declaration to its binder equivalent. */
     fun convertToBinderType(type: Type): ClassName {
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverter.kt
new file mode 100644
index 0000000..679197d
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverter.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
+import androidx.privacysandbox.tools.core.model.ParsedApi
+import com.squareup.kotlinpoet.CodeBlock
+
+class ClientBinderCodeConverter(api: ParsedApi) : BinderCodeConverter(api) {
+    override fun convertToInterfaceModelCode(
+        annotatedInterface: AnnotatedInterface,
+        expression: String
+    ): CodeBlock = CodeBlock.of("%T(%L)", annotatedInterface.clientProxyNameSpec(), expression)
+
+    override fun convertToInterfaceBinderCode(
+        annotatedInterface: AnnotatedInterface,
+        expression: String
+    ): CodeBlock =
+        CodeBlock.of(
+            "(%L as %T).remote", expression, annotatedInterface.clientProxyNameSpec()
+        )
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
index 49b17ce..97472c9 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
@@ -16,6 +16,9 @@
 
 package androidx.privacysandbox.tools.core.generator
 
+import androidx.privacysandbox.tools.core.generator.AidlGenerator.Companion.throwableParcelName
+import androidx.privacysandbox.tools.core.generator.SpecNames.resumeWithExceptionMethod
+import androidx.privacysandbox.tools.core.generator.SpecNames.suspendCancellableCoroutineMethod
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.Method
 import com.squareup.kotlinpoet.ClassName
@@ -23,7 +26,6 @@
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
-import com.squareup.kotlinpoet.MemberName
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.joinToCode
@@ -45,7 +47,7 @@
             primaryConstructor(
                 listOf(
                     PropertySpec.builder("remote", remoteBinderClassName)
-                        .addModifiers(KModifier.PRIVATE).build()
+                        .addModifiers(KModifier.PUBLIC).build()
                 )
             )
 
@@ -54,12 +56,6 @@
 
         return FileSpec.builder(annotatedInterface.type.packageName, className).build {
             addCommonSettings()
-
-            // TODO(b/254660742): Only add these when needed
-            addImport("kotlinx.coroutines", "suspendCancellableCoroutine")
-            addImport("kotlin.coroutines", "resume")
-            addImport("kotlin.coroutines", "resumeWithException")
-
             addType(classSpec)
         }
     }
@@ -77,7 +73,7 @@
             returns(method.returnType.poetSpec())
 
             addCode {
-                addControlFlow("return suspendCancellableCoroutine") {
+                addControlFlow("return %M", suspendCancellableCoroutineMethod) {
                     addStatement("var mCancellationSignal: %T? = null", cancellationSignalClassName)
 
                     add(generateTransactionCallbackObject(method))
@@ -118,10 +114,16 @@
 
             add(generateTransactionCallbackOnSuccess(method))
 
-            addControlFlow("override fun onFailure(errorCode: Int, errorMessage: String)") {
+            addControlFlow(
+                "override fun onFailure(throwableParcel: %T)",
+                ClassName(basePackageName, throwableParcelName)
+            ) {
                 addStatement(
-                    "it.%M(RuntimeException(errorMessage))",
-                    MemberName("kotlin.coroutines", "resumeWithException")
+                    "it.%M(%M(throwableParcel))",
+                    resumeWithExceptionMethod,
+                    ThrowableParcelConverterFileGenerator.fromThrowableParcelNameSpec(
+                        basePackageName
+                    )
                 )
             }
         }
@@ -154,7 +156,8 @@
         extraParameters: List<CodeBlock> = emptyList(),
     ) = CodeBlock.builder().build {
         val parameters =
-            method.parameters.map { binderCodeConverter.convertToBinderCode(it) } + extraParameters
+            method.parameters.map { binderCodeConverter.convertToBinderCode(it.type, it.name) } +
+                extraParameters
         addStatement {
             add("remote.${method.name}(")
             add(parameters.joinToCode())
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
index 828123af..bb26bbd 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
@@ -41,16 +41,22 @@
 
 fun AnnotatedValue.converterNameSpec() =
     ClassName(type.packageName, "${type.simpleName}Converter")
+
 fun AnnotatedValue.toParcelableNameSpec() =
     MemberName(converterNameSpec(), "toParcelable")
+
 fun AnnotatedValue.fromParcelableNameSpec() =
     MemberName(converterNameSpec(), "fromParcelable")
+
 fun AnnotatedValue.parcelableNameSpec() =
     ClassName(type.packageName, "Parcelable${type.simpleName}")
+
 fun AnnotatedInterface.clientProxyNameSpec() =
     ClassName(type.packageName, "${type.simpleName}ClientProxy")
+
 fun AnnotatedInterface.stubDelegateNameSpec() =
     ClassName(type.packageName, "${type.simpleName}StubDelegate")
+
 fun AnnotatedInterface.aidlInterfaceNameSpec() =
     ClassName(type.packageName, aidlType().innerType.simpleName)
 
@@ -131,11 +137,15 @@
     add("\n»")
 }
 
-fun CodeBlock.Companion.statement(builderBlock: CodeBlock.Builder.() -> Unit) =
-    builder().build { addStatement(builderBlock) }
-
 object SpecNames {
     val dispatchersMainClass = ClassName("kotlinx.coroutines", "Dispatchers", "Main")
+    val delicateCoroutinesApiClass = ClassName("kotlinx.coroutines", "DelicateCoroutinesApi")
     val globalScopeClass = ClassName("kotlinx.coroutines", "GlobalScope")
+    val stackTraceElementClass = ClassName("java.lang", "StackTraceElement")
+    val suspendCancellableCoroutineMethod =
+        MemberName("kotlinx.coroutines", "suspendCancellableCoroutine", isExtension = true)
+    val resumeWithExceptionMethod =
+        MemberName("kotlin.coroutines", "resumeWithException", isExtension = true)
     val launchMethod = MemberName("kotlinx.coroutines", "launch", isExtension = true)
+    val iBinderClassName = ClassName("android.os", "IBinder")
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/PrivacySandboxExceptionFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/PrivacySandboxExceptionFileGenerator.kt
new file mode 100644
index 0000000..f587725
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/PrivacySandboxExceptionFileGenerator.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asTypeName
+
+class PrivacySandboxExceptionFileGenerator(private val basePackageName: String) {
+    companion object {
+        const val privacySandboxExceptionName = "PrivacySandboxException"
+    }
+
+    fun generate(): FileSpec {
+        val classSpec = TypeSpec.classBuilder(privacySandboxExceptionName).build {
+            superclass(RuntimeException::class)
+            addModifiers(KModifier.PUBLIC)
+            primaryConstructor(
+                listOf(
+                    PropertySpec.builder(
+                        "message",
+                        String::class.asTypeName().copy(nullable = true),
+                    ).addModifiers(KModifier.OVERRIDE).build(),
+                    PropertySpec.builder(
+                        "cause",
+                        Throwable::class.asTypeName().copy(nullable = true),
+                    ).addModifiers(KModifier.OVERRIDE).build()
+                )
+            )
+        }
+
+        return FileSpec.builder(
+            basePackageName,
+            privacySandboxExceptionName
+        ).build {
+            addCommonSettings()
+            addType(classSpec)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverter.kt
new file mode 100644
index 0000000..e76dd5fb
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverter.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
+import androidx.privacysandbox.tools.core.model.ParsedApi
+import com.squareup.kotlinpoet.CodeBlock
+
+class ServerBinderCodeConverter(private val api: ParsedApi) : BinderCodeConverter(api) {
+    override fun convertToInterfaceModelCode(
+        annotatedInterface: AnnotatedInterface,
+        expression: String
+    ): CodeBlock = CodeBlock.of(
+        "(%L as %T).delegate", expression, annotatedInterface.stubDelegateNameSpec()
+    )
+
+    override fun convertToInterfaceBinderCode(
+        annotatedInterface: AnnotatedInterface,
+        expression: String
+    ): CodeBlock =
+        CodeBlock.of("%T(%L)", annotatedInterface.stubDelegateNameSpec(), expression)
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
index 649485a..e94ab33 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
@@ -16,6 +16,7 @@
 
 package androidx.privacysandbox.tools.core.generator
 
+import androidx.privacysandbox.tools.core.generator.SpecNames.delicateCoroutinesApiClass
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.Method
 import androidx.privacysandbox.tools.core.model.Types
@@ -70,6 +71,7 @@
             addModifiers(KModifier.OVERRIDE)
             addParameters(getParameters(method))
             addCode {
+                addStatement("@OptIn(%T::class)", delicateCoroutinesApiClass)
                 addControlFlow(
                     "val job = %T.%M(%T)",
                     SpecNames.globalScopeClass,
@@ -78,7 +80,9 @@
                 ) {
                     addControlFlow("try") {
                         addStatement {
-                            add("val result = ")
+                            if (method.returnType != Types.unit) {
+                                add("val result = ")
+                            }
                             add(getDelegateCallBlock(method))
                         }
                         if (method.returnType == Types.unit) {
@@ -86,12 +90,19 @@
                         } else {
                             addStatement(
                                 "transactionCallback.onSuccess(%L)",
-                                binderCodeConverter.convertToBinderCode(method.returnType, "result")
+                                binderCodeConverter.convertToBinderCode(
+                                    method.returnType, "result"
+                                )
                             )
                         }
                     }
                     addControlFlow("catch (t: Throwable)") {
-                        addStatement("transactionCallback.onFailure(404, t.message)")
+                        addStatement(
+                            "transactionCallback.onFailure(%M(t))",
+                            ThrowableParcelConverterFileGenerator.toThrowableParcelNameSpec(
+                                basePackageName
+                            )
+                        )
                     }
                 }
                 addStatement(
@@ -127,7 +138,8 @@
 
     private fun getDelegateCallBlock(method: Method) = CodeBlock.builder().build {
         add("delegate.${method.name}(")
-        add(method.parameters.map { binderCodeConverter.convertToModelCode(it) }.joinToCode())
+        add(method.parameters.map { binderCodeConverter.convertToModelCode(it.type, it.name) }
+            .joinToCode())
         add(")")
     }
 }
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ThrowableParcelConverterFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ThrowableParcelConverterFileGenerator.kt
new file mode 100644
index 0000000..4ac7844
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ThrowableParcelConverterFileGenerator.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.generator.AidlGenerator.Companion.parcelableStackFrameName
+import androidx.privacysandbox.tools.core.generator.AidlGenerator.Companion.throwableParcelName
+import androidx.privacysandbox.tools.core.generator.SpecNames.stackTraceElementClass
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.MemberName
+import com.squareup.kotlinpoet.TypeSpec
+
+class ThrowableParcelConverterFileGenerator(private val basePackageName: String) {
+    companion object {
+        const val converterName = "${throwableParcelName}Converter"
+        fun toThrowableParcelNameSpec(packageName: String) = MemberName(ClassName(
+            packageName,
+            converterName
+        ), "toThrowableParcel")
+        fun fromThrowableParcelNameSpec(packageName: String) = MemberName(ClassName(
+            packageName,
+            converterName
+        ), "fromThrowableParcel")
+    }
+
+    private val throwableParcelNameSpec = ClassName(basePackageName, throwableParcelName)
+    private val parcelableStackFrameNameSpec = ClassName(basePackageName, parcelableStackFrameName)
+    private val toThrowableParcelNameSpec = toThrowableParcelNameSpec(basePackageName)
+    private val fromThrowableParcelNameSpec = fromThrowableParcelNameSpec(basePackageName)
+
+    fun generate(convertToParcel: Boolean = false, convertFromParcel: Boolean = false) =
+        FileSpec.builder(
+            basePackageName,
+            converterName
+        ).build {
+            addCommonSettings()
+            addType(generateConverter(convertToParcel, convertFromParcel))
+        }
+
+    private fun generateConverter(convertToParcel: Boolean, convertFromParcel: Boolean) =
+        TypeSpec.objectBuilder(ClassName(basePackageName, converterName)).build {
+            if (convertToParcel) addFunction(generateToThrowableParcel())
+            if (convertFromParcel) addFunction(generateFromThrowableParcel())
+        }
+
+    private fun generateToThrowableParcel() =
+        FunSpec.builder(toThrowableParcelNameSpec.simpleName).build {
+            addParameter("throwable", Throwable::class)
+            returns(throwableParcelNameSpec)
+            addCode {
+                add(
+                    """
+                    val parcel = %T()
+                    parcel.exceptionClass = throwable::class.qualifiedName
+                    parcel.errorMessage = throwable.message
+                    parcel.stackTrace = throwable.stackTrace.map {
+                        val stackFrame = %T()
+                        stackFrame.declaringClass = it.className
+                        stackFrame.methodName = it.methodName
+                        stackFrame.fileName = it.fileName
+                        stackFrame.lineNumber = it.lineNumber
+                        stackFrame
+                    }.toTypedArray()
+                    throwable.cause?.let {
+                        parcel.cause = ${toThrowableParcelNameSpec.simpleName}(it)
+                    }
+                    parcel.suppressedExceptions =
+                        throwable.suppressedExceptions.map {
+                            ${toThrowableParcelNameSpec.simpleName}(it)
+                        }.toTypedArray()
+                    return parcel
+                """.trimIndent(),
+                    throwableParcelNameSpec,
+                    parcelableStackFrameNameSpec,
+                )
+            }
+        }
+
+    private fun generateFromThrowableParcel() =
+        FunSpec.builder(fromThrowableParcelNameSpec.simpleName).build {
+            addParameter("throwableParcel", throwableParcelNameSpec)
+            returns(Throwable::class)
+            addCode {
+                add(
+                    """
+                    val exceptionClass = throwableParcel.exceptionClass
+                    val errorMessage = throwableParcel.errorMessage
+                    val stackTrace = throwableParcel.stackTrace
+                    val exception = PrivacySandboxException(
+                        "[${'$'}exceptionClass] ${'$'}errorMessage",
+                        throwableParcel.cause?.let { ${fromThrowableParcelNameSpec.simpleName}(it) })
+                    for (suppressed in throwableParcel.suppressedExceptions) {
+                        exception.addSuppressed(${fromThrowableParcelNameSpec.simpleName}(suppressed))
+                    }
+                    exception.stackTrace =
+                        stackTrace.map {
+                            %T(
+                                it.declaringClass,
+                                it.methodName,
+                                it.fileName,
+                                it.lineNumber
+                            )
+                        }.toTypedArray()
+                    return exception
+                """.trimIndent(),
+                    stackTraceElementClass,
+                )
+            }
+        }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
index 0687c63..0b5ced9 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
@@ -17,7 +17,6 @@
 package androidx.privacysandbox.tools.core.generator
 
 import androidx.privacysandbox.tools.core.model.AnnotatedValue
-import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.model.ValueProperty
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FileSpec
@@ -29,7 +28,7 @@
  * Generates a file that defines a converter for an SDK defined value and its AIDL parcelable
  * counterpart.
  */
-class ValueConverterFileGenerator(private val api: ParsedApi) {
+class ValueConverterFileGenerator(private val binderConverter: BinderCodeConverter) {
 
     fun generate(value: AnnotatedValue) =
         FileSpec.builder(
@@ -42,9 +41,9 @@
 
     private fun generateConverter(value: AnnotatedValue) =
         TypeSpec.objectBuilder(value.converterNameSpec()).build {
-        addFunction(generateFromParcelable(value))
-        addFunction(generateToParcelable(value))
-    }
+            addFunction(generateFromParcelable(value))
+            addFunction(generateToParcelable(value))
+        }
 
     private fun generateToParcelable(value: AnnotatedValue) =
         FunSpec.builder(value.toParcelableNameSpec().simpleName).build {
@@ -57,14 +56,13 @@
 
     private fun generateToParcelablePropertyConversion(property: ValueProperty) =
         CodeBlock.builder().build {
-            val innerValue = api.valueMap[property.type]
-            if (innerValue != null) {
-                addStatement(
-                    "parcelable.${property.name} = %M(annotatedValue.${property.name})",
-                    innerValue.toParcelableNameSpec())
-            } else {
-                addStatement("parcelable.${property.name} = annotatedValue.${property.name}")
-            }
+            addStatement(
+                "parcelable.${property.name} = %L",
+                binderConverter.convertToBinderCode(
+                    property.type,
+                    "annotatedValue.${property.name}"
+                )
+            )
         }
 
     private fun generateFromParcelable(value: AnnotatedValue) =
@@ -82,14 +80,11 @@
 
     private fun generateFromParcelablePropertyConversion(property: ValueProperty) =
         CodeBlock.builder().build {
-            val innerValue = api.valueMap[property.type]
-            if (innerValue != null) {
-                add(
-                    "${property.name} = %M(parcelable.${property.name})",
-                    innerValue.fromParcelableNameSpec()
+            add(
+                "${property.name} = %L",
+                binderConverter.convertToModelCode(
+                    property.type, "parcelable.${property.name}"
                 )
-            } else {
-                add("${property.name} = parcelable.${property.name}")
-            }
+            )
         }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlInterfaceSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlInterfaceSpec.kt
index 81ced87..3453725 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlInterfaceSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlInterfaceSpec.kt
@@ -35,7 +35,8 @@
     override val typesToImport: Set<Type>
         get() {
             return methods.flatMap { method ->
-                method.parameters.map { it.type }.filter { it.requiresImport }.map { it.innerType }
+                method.parameters.map { it.type }
+                    .filter { it.requiresImport && it.innerType != type }.map { it.innerType }
             }.toSet()
         }
 
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParameterSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParameterSpec.kt
index 894c7a3..39fb6a6 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParameterSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParameterSpec.kt
@@ -21,10 +21,6 @@
     val type: AidlTypeSpec,
     val isIn: Boolean = false
 ) {
-    companion object {
-        fun parameter(name: String, vararg parameters: AidlParameterSpec) =
-            AidlMethodSpec(name, parameters.toList())
-    }
 
     override fun toString() = "${if (isIn) "in " else ""}$type $name"
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParcelableSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParcelableSpec.kt
index 9c7cf46..94906d5 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParcelableSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParcelableSpec.kt
@@ -21,16 +21,45 @@
 /** AIDL file with a single parcelable defined. */
 internal data class AidlParcelableSpec(
     override val type: Type,
-    val properties: List<String>,
-    override val typesToImport: Set<Type> = emptySet(),
+    val properties: List<AidlPropertySpec>,
 ) : AidlFileSpec {
+    companion object {
+        fun aidlParcelable(
+            type: Type,
+            block: Builder.() -> Unit = {}
+        ): AidlParcelableSpec {
+            return Builder(type).also(block).build()
+        }
+    }
+
+    override val typesToImport: Set<Type>
+        get() {
+            return properties.map { it.type }.filter { it.requiresImport && it.innerType != type }
+                .map { it.innerType }
+                .toSet()
+        }
+
     override val innerContent: String
         get() {
-            val body = properties.sorted().joinToString("\n|    ")
+            val body = properties.map { it.toString() }.sorted().joinToString("\n|    ")
             return """
                 |parcelable ${type.simpleName} {
                 |    $body
                 |}
                 """.trimMargin()
         }
+
+    class Builder(val type: Type) {
+        val properties = mutableListOf<AidlPropertySpec>()
+
+        fun addProperty(property: AidlPropertySpec) {
+            properties.add(property)
+        }
+
+        fun addProperty(name: String, type: AidlTypeSpec, isNullable: Boolean = false) {
+            addProperty(AidlPropertySpec(name, type, isNullable))
+        }
+
+        fun build() = AidlParcelableSpec(type, properties)
+    }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlPropertySpec.kt
similarity index 67%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
copy to privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlPropertySpec.kt
index 79122bf..b78ad94 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlPropertySpec.kt
@@ -14,10 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.privacysandbox.tools.apicompiler.generator
+package androidx.privacysandbox.tools.core.generator.poet
 
-import com.squareup.kotlinpoet.FileSpec
-import java.io.OutputStream
-
-/** Convenience method to write [FileSpec]s to KSP-generated [OutputStream]s. */
-internal fun OutputStream.write(spec: FileSpec) = bufferedWriter().use(spec::writeTo)
+internal data class AidlPropertySpec(
+    val name: String,
+    val type: AidlTypeSpec,
+    val isNullable: Boolean = false,
+) {
+    override fun toString() = "${if (isNullable) "@nullable(heap=true) " else ""}$type $name;"
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
index 99433e0..aeee60f 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
@@ -18,6 +18,10 @@
 
 import androidx.privacysandbox.tools.core.model.Type
 
-internal data class AidlTypeSpec(val innerType: Type, val requiresImport: Boolean = true) {
-    override fun toString() = innerType.simpleName
+internal data class AidlTypeSpec(
+    val innerType: Type,
+    val requiresImport: Boolean = true,
+    val isList: Boolean = false
+) {
+    override fun toString() = innerType.simpleName + if (isList) "[]" else ""
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
index ea04578..467d8ac 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
@@ -23,6 +23,13 @@
     return services.first()
 }
 
+fun ParsedApi.hasSuspendFunctions(): Boolean {
+    val annotatedInterfaces = services + interfaces
+    return annotatedInterfaces
+        .flatMap(AnnotatedInterface::methods)
+        .any(Method::isSuspend)
+}
+
 object Types {
     val unit = Type(packageName = "kotlin", simpleName = "Unit")
     val boolean = Type(packageName = "kotlin", simpleName = "Boolean")
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
index d09fd40..0f6e3f8 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
@@ -96,14 +96,18 @@
 
     private fun validateValuePropertyTypes() {
         val allowedValuePropertyTypes =
-            (api.values.map(AnnotatedValue::type) + Types.primitiveTypes).toSet()
+            (api.values.map(AnnotatedValue::type) +
+                api.interfaces.map(AnnotatedInterface::type) +
+                Types.primitiveTypes).toSet()
+
         for (value in api.values) {
             for (property in value.properties) {
                 if (!allowedValuePropertyTypes.contains(property.type)) {
                     errors.add(
                         "Error in ${value.type.qualifiedName}.${property.name}: " +
-                            "only primitives and data classes annotated with " +
-                            "@PrivacySandboxValue are supported as properties."
+                            "only primitives, data classes annotated with " +
+                            "@PrivacySandboxValue and interfaces annotated with " +
+                            "@PrivacySandboxInterface are supported as properties."
                     )
                 }
             }
@@ -113,6 +117,7 @@
     private fun validateCallbackMethods() {
         val allowedParameterTypes =
             (api.values.map(AnnotatedValue::type) +
+                api.interfaces.map(AnnotatedInterface::type) +
                 Types.primitiveTypes).toSet()
 
         for (callback in api.callbacks) {
@@ -120,8 +125,9 @@
                 if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
                     errors.add(
                         "Error in ${callback.type.qualifiedName}.${method.name}: " +
-                            "only primitives and data classes annotated with " +
-                            "@PrivacySandboxValue are supported as callback parameter types."
+                            "only primitives, data classes annotated with " +
+                            "@PrivacySandboxValue and interfaces annotated with " +
+                            "@PrivacySandboxInterface are supported as callback parameter types."
                     )
                 }
                 if (method.returnType != Types.unit || method.isSuspend) {
diff --git a/privacysandbox/tools/tools-core/src/main/proto/androidx/privacysandbox/tools/core/privacy_sandbox_tools_protocol.proto b/privacysandbox/tools/tools-core/src/main/proto/androidx/privacysandbox/tools/core/privacy_sandbox_tools_protocol.proto
new file mode 100644
index 0000000..788e3a7
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/proto/androidx/privacysandbox/tools/core/privacy_sandbox_tools_protocol.proto
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+package androidx.privacysandbox.tools.core;
+
+option java_package = "androidx.privacysandbox.tools.core.proto";
+option java_outer_classname = "PrivacySandboxToolsProtocol";
+
+// Metadata about Privacy Sandbox Tools.
+//
+// This message is stored in every SDK API descriptor so consumers can tweak code generation
+// according to what's expected by the compiled SDK.
+message ToolMetadata {
+  int32 code_generation_version = 1;
+}
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlCallbackGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlCallbackGeneratorTest.kt
index 69e30b3..60fdadb 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlCallbackGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlCallbackGeneratorTest.kt
@@ -73,7 +73,6 @@
             .containsExactly(
                 "com.mysdk" to "IMySdk",
                 "com.mysdk" to "IMyCallback",
-                "com.mysdk" to "ICancellationSignal",
             )
 
         val outputTestDataDir = File("src/test/test-data/aidlcallbackgeneratortest/output")
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
index 3adb6d9..780e27c 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
@@ -85,6 +85,8 @@
                 "com.mysdk" to "IMyInterface",
                 "com.mysdk" to "ICancellationSignal",
                 "com.mysdk" to "IMyInterfaceTransactionCallback",
+                "com.mysdk" to "PrivacySandboxThrowableParcel",
+                "com.mysdk" to "ParcelableStackFrame",
             )
 
         val outputTestDataDir = File("src/test/test-data/aidlinterfacegeneratortest/output")
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
index 1921b17..aeb2f9a 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
@@ -76,6 +76,8 @@
                 "com.mysdk" to "IStringTransactionCallback",
                 "com.mysdk" to "IUnitTransactionCallback",
                 "com.mysdk" to "ICancellationSignal",
+                "com.mysdk" to "PrivacySandboxThrowableParcel",
+                "com.mysdk" to "ParcelableStackFrame",
             )
 
         val outputTestDataDir = File("src/test/test-data/aidlservicegeneratortest/output")
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
index bca3e3b..2b5ee15 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
@@ -96,6 +96,8 @@
                 "com.mysdk" to "IUnitTransactionCallback",
                 "com.mysdk" to "IOuterValueTransactionCallback",
                 "com.mysdk" to "ICancellationSignal",
+                "com.mysdk" to "PrivacySandboxThrowableParcel",
+                "com.mysdk" to "ParcelableStackFrame",
             )
 
         val outputTestDataDir = File("src/test/test-data/aidlvaluegeneratortest/output")
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverterTest.kt
similarity index 74%
rename from privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
rename to privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverterTest.kt
index 5c0a149..dc0ee76 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverterTest.kt
@@ -27,8 +27,8 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class BinderCodeConverterTest {
-    private val converter = BinderCodeConverter(
+class ClientBinderCodeConverterTest {
+    private val converter = ClientBinderCodeConverter(
         ParsedApi(
             services = setOf(
                 AnnotatedInterface(
@@ -75,31 +75,4 @@
             ).toString()
         ).isEqualTo("com.mysdk.CallbackClientProxy(callback)")
     }
-
-    @Test
-    fun convertToBinderCode_primitive() {
-        assertThat(
-            converter.convertToBinderCode(
-                Types.int, expression = "5"
-            ).toString()
-        ).isEqualTo("5")
-    }
-
-    @Test
-    fun convertToBinderCode_value() {
-        assertThat(
-            converter.convertToBinderCode(
-                Type(packageName = "com.mysdk", simpleName = "Value"), expression = "value"
-            ).toString()
-        ).isEqualTo("com.mysdk.ValueConverter.toParcelable(value)")
-    }
-
-    @Test
-    fun convertToBinderCode_callback() {
-        assertThat(
-            converter.convertToBinderCode(
-                Type(packageName = "com.mysdk", simpleName = "Callback"), expression = "callback"
-            ).toString()
-        ).isEqualTo("com.mysdk.CallbackStubDelegate(callback)")
-    }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverterTest.kt
similarity index 74%
copy from privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
copy to privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverterTest.kt
index 5c0a149..aeaaa53 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverterTest.kt
@@ -27,8 +27,8 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class BinderCodeConverterTest {
-    private val converter = BinderCodeConverter(
+class ServerBinderCodeConverterTest {
+    private val converter = ServerBinderCodeConverter(
         ParsedApi(
             services = setOf(
                 AnnotatedInterface(
@@ -50,33 +50,6 @@
     )
 
     @Test
-    fun convertToModelCode_primitive() {
-        assertThat(
-            converter.convertToModelCode(
-                Types.int, expression = "5"
-            ).toString()
-        ).isEqualTo("5")
-    }
-
-    @Test
-    fun convertToModelCode_value() {
-        assertThat(
-            converter.convertToModelCode(
-                Type(packageName = "com.mysdk", simpleName = "Value"), expression = "value"
-            ).toString()
-        ).isEqualTo("com.mysdk.ValueConverter.fromParcelable(value)")
-    }
-
-    @Test
-    fun convertToModelCode_callback() {
-        assertThat(
-            converter.convertToModelCode(
-                Type(packageName = "com.mysdk", simpleName = "Callback"), expression = "callback"
-            ).toString()
-        ).isEqualTo("com.mysdk.CallbackClientProxy(callback)")
-    }
-
-    @Test
     fun convertToBinderCode_primitive() {
         assertThat(
             converter.convertToBinderCode(
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
index 25525bc..c3a654d 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
@@ -266,8 +266,9 @@
         val validationResult = ModelValidator.validate(api)
         assertThat(validationResult.isFailure).isTrue()
         assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.Foo.bar: only primitives and data classes annotated with " +
-                "@PrivacySandboxValue are supported as properties."
+            "Error in com.mysdk.Foo.bar: only primitives, data classes annotated with " +
+                "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface " +
+                "are supported as properties."
         )
     }
 
@@ -332,43 +333,9 @@
         val validationResult = ModelValidator.validate(api)
         assertThat(validationResult.isFailure).isTrue()
         assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.MySdkCallback.foo: only primitives and data classes annotated " +
-                "with @PrivacySandboxValue are supported as callback parameter types."
-        )
-    }
-
-    @Test
-    fun callbackReceivingInterface_throws() {
-        val api = ParsedApi(
-            services = setOf(
-                AnnotatedInterface(type = Type(packageName = "com.mysdk", simpleName = "MySdk")),
-            ),
-            callbacks = setOf(
-                AnnotatedInterface(
-                    type = Type(packageName = "com.mysdk", simpleName = "MySdkCallback"),
-                    methods = listOf(
-                        Method(
-                            name = "foo",
-                            parameters = listOf(
-                                Parameter("otherCallback", Type("com.mysdk", "MySdkInterface"))
-                            ),
-                            returnType = Types.unit,
-                            isSuspend = false,
-                        ),
-                    )
-                )
-            ),
-            interfaces = setOf(
-                AnnotatedInterface(
-                    type = Type(packageName = "com.mysdk", simpleName = "MySdkInterface"),
-                    )
-                )
-        )
-        val validationResult = ModelValidator.validate(api)
-        assertThat(validationResult.isFailure).isTrue()
-        assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.MySdkCallback.foo: only primitives and data classes annotated " +
-                "with @PrivacySandboxValue are supported as callback parameter types."
+            "Error in com.mysdk.MySdkCallback.foo: only primitives, data classes annotated " +
+                "with @PrivacySandboxValue and interfaces annotated with " +
+                "@PrivacySandboxInterface are supported as callback parameter types."
         )
     }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlcallbackgeneratortest/output/com/mysdk/ICancellationSignal.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlcallbackgeneratortest/output/com/mysdk/ICancellationSignal.aidl
deleted file mode 100644
index e336980..0000000
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlcallbackgeneratortest/output/com/mysdk/ICancellationSignal.aidl
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.mysdk;
-
-oneway interface ICancellationSignal {
-    void cancel();
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
index a2d36bb..98e2b50 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
@@ -1,6 +1,5 @@
 package com.mysdk;
 
-import com.mysdk.IMyInterface;
 import com.mysdk.IMyInterfaceTransactionCallback;
 
 oneway interface IMyInterface {
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterfaceTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterfaceTransactionCallback.aidl
index a62ec9e..e920d2e 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterfaceTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterfaceTransactionCallback.aidl
@@ -2,9 +2,10 @@
 
 import com.mysdk.ICancellationSignal;
 import com.mysdk.IMyInterface;
+import com.mysdk.PrivacySandboxThrowableParcel;
 
 oneway interface IMyInterfaceTransactionCallback {
     void onCancellable(ICancellationSignal cancellationSignal);
-    void onFailure(int errorCode, String errorMessage);
+    void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
     void onSuccess(IMyInterface result);
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
new file mode 100644
index 0000000..2050d2e
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
@@ -0,0 +1,8 @@
+package com.mysdk;
+
+parcelable ParcelableStackFrame {
+    String declaringClass;
+    String fileName;
+    String methodName;
+    int lineNumber;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
new file mode 100644
index 0000000..716a0f9
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
@@ -0,0 +1,11 @@
+package com.mysdk;
+
+import com.mysdk.ParcelableStackFrame;
+
+parcelable PrivacySandboxThrowableParcel {
+    @nullable(heap=true) PrivacySandboxThrowableParcel cause;
+    ParcelableStackFrame[] stackTrace;
+    PrivacySandboxThrowableParcel[] suppressedExceptions;
+    String errorMessage;
+    String exceptionClass;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IStringTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IStringTransactionCallback.aidl
index 11bba23..1cb6671 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IStringTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IStringTransactionCallback.aidl
@@ -1,9 +1,10 @@
 package com.mysdk;
 
 import com.mysdk.ICancellationSignal;
+import com.mysdk.PrivacySandboxThrowableParcel;
 
 oneway interface IStringTransactionCallback {
     void onCancellable(ICancellationSignal cancellationSignal);
-    void onFailure(int errorCode, String errorMessage);
+    void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
     void onSuccess(String result);
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
index a777df5..3e1856f 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
@@ -1,9 +1,10 @@
 package com.mysdk;
 
 import com.mysdk.ICancellationSignal;
+import com.mysdk.PrivacySandboxThrowableParcel;
 
 oneway interface IUnitTransactionCallback {
     void onCancellable(ICancellationSignal cancellationSignal);
-    void onFailure(int errorCode, String errorMessage);
+    void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
     void onSuccess();
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
new file mode 100644
index 0000000..2050d2e
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
@@ -0,0 +1,8 @@
+package com.mysdk;
+
+parcelable ParcelableStackFrame {
+    String declaringClass;
+    String fileName;
+    String methodName;
+    int lineNumber;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
new file mode 100644
index 0000000..716a0f9
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
@@ -0,0 +1,11 @@
+package com.mysdk;
+
+import com.mysdk.ParcelableStackFrame;
+
+parcelable PrivacySandboxThrowableParcel {
+    @nullable(heap=true) PrivacySandboxThrowableParcel cause;
+    ParcelableStackFrame[] stackTrace;
+    PrivacySandboxThrowableParcel[] suppressedExceptions;
+    String errorMessage;
+    String exceptionClass;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IOuterValueTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IOuterValueTransactionCallback.aidl
index 89a0b1e..929fee2 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IOuterValueTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IOuterValueTransactionCallback.aidl
@@ -2,9 +2,10 @@
 
 import com.mysdk.ICancellationSignal;
 import com.mysdk.ParcelableOuterValue;
+import com.mysdk.PrivacySandboxThrowableParcel;
 
 oneway interface IOuterValueTransactionCallback {
     void onCancellable(ICancellationSignal cancellationSignal);
-    void onFailure(int errorCode, String errorMessage);
+    void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
     void onSuccess(in ParcelableOuterValue result);
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
index a777df5..3e1856f 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
@@ -1,9 +1,10 @@
 package com.mysdk;
 
 import com.mysdk.ICancellationSignal;
+import com.mysdk.PrivacySandboxThrowableParcel;
 
 oneway interface IUnitTransactionCallback {
     void onCancellable(ICancellationSignal cancellationSignal);
-    void onFailure(int errorCode, String errorMessage);
+    void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
     void onSuccess();
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
new file mode 100644
index 0000000..2050d2e
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
@@ -0,0 +1,8 @@
+package com.mysdk;
+
+parcelable ParcelableStackFrame {
+    String declaringClass;
+    String fileName;
+    String methodName;
+    int lineNumber;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
new file mode 100644
index 0000000..716a0f9
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
@@ -0,0 +1,11 @@
+package com.mysdk;
+
+import com.mysdk.ParcelableStackFrame;
+
+parcelable PrivacySandboxThrowableParcel {
+    @nullable(heap=true) PrivacySandboxThrowableParcel cause;
+    ParcelableStackFrame[] stackTrace;
+    PrivacySandboxThrowableParcel[] suppressedExceptions;
+    String errorMessage;
+    String exceptionClass;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index 11df687..dec603f 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -17,10 +17,10 @@
 package androidx.privacysandbox.tools.testing
 
 import androidx.room.compiler.processing.util.DiagnosticLocation
-import androidx.room.compiler.processing.util.compiler.TestCompilationResult
 import androidx.room.compiler.processing.util.DiagnosticMessage
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
 import androidx.room.compiler.processing.util.compiler.compile
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -61,12 +61,15 @@
     fun assertThat(result: TestCompilationResult) = CompilationResultSubject(result)
 }
 
+val TestCompilationResult.resourceOutputDir: File
+    get() = outputClasspath.first().parentFile.resolve("ksp-compiler/resourceOutputDir")
+
 class CompilationResultSubject(private val result: TestCompilationResult) {
     fun succeeds() {
         assertWithMessage(
-            "UnexpectedErrors:\n${getFullErrorMessages()?.joinToString("\n")}"
+            "Unexpected errors:\n${getFullErrorMessages().joinToString("\n")}"
         ).that(
-            result.success
+            result.success && getRawErrorMessages().isEmpty()
         ).isTrue()
     }
 
@@ -99,14 +102,19 @@
         assertThat(getShortErrorMessages()).containsExactly(*errors)
     }
 
+    private fun getRawErrorMessages() =
+        (result.diagnostics[Diagnostic.Kind.ERROR] ?: emptyList()) +
+            (result.diagnostics[Diagnostic.Kind.WARNING] ?: emptyList()) +
+            (result.diagnostics[Diagnostic.Kind.MANDATORY_WARNING] ?: emptyList())
+
     private fun getShortErrorMessages() =
         result.diagnostics[Diagnostic.Kind.ERROR]?.map(DiagnosticMessage::msg)
 
     private fun getFullErrorMessages() =
-        result.diagnostics[Diagnostic.Kind.ERROR]?.map { it.toFormattedMessage() }
+        getRawErrorMessages().map { it.toFormattedMessage() }
 
     private fun DiagnosticMessage.toFormattedMessage() = """
-            |Error: $msg
+            |$kind: $msg
             |${location?.toFormattedLocation()}$
         """.trimMargin()
 
@@ -132,71 +140,69 @@
 // PrivacySandbox platform APIs are not available in AndroidX prebuilts nor are they stable, so
 // while that's the case we use fake stubs to run our compilation tests.
 val syntheticPrivacySandboxSources = listOf(
-    Source.java(
-        "android.app.sdksandbox.SdkSandboxManager", """
-        |package android.app.sdksandbox;
+    Source.kotlin(
+        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt", """
+        |package androidx.privacysandbox.sdkruntime.core
         |
-        |import android.os.Bundle;
-        |import android.os.OutcomeReceiver;
-        |import java.util.concurrent.Executor;
+        |import android.os.IBinder
         |
-        |public final class SdkSandboxManager {
-        |    public void loadSdk(
-        |        String sdkName,
-        |        Bundle params,
-        |        Executor executor,
-        |        OutcomeReceiver<SandboxedSdk, LoadSdkException> receiver) {}
-        |}
-        |""".trimMargin()
-    ),
-    Source.java(
-        "android.app.sdksandbox.SandboxedSdk", """
-        |package android.app.sdksandbox;
+        |@Suppress("UNUSED_PARAMETER")
+        |sealed class SandboxedSdkCompat {
+        |    abstract fun getInterface(): IBinder?
         |
-        |import android.os.IBinder;
-        |
-        |public final class SandboxedSdk {
-        |    public SandboxedSdk(IBinder sdkInterface) {}
-        |    public IBinder getInterface() { return null; }
-        |}
-        |""".trimMargin()
-    ),
-    Source.java(
-        "android.app.sdksandbox.SandboxedSdkProvider", """
-        |package android.app.sdksandbox;
-        |
-        |import android.content.Context;
-        |import android.os.Bundle;
-        |import android.view.View;
-        |
-        |public abstract class SandboxedSdkProvider {
-        |    public abstract SandboxedSdk onLoadSdk(Bundle params) throws LoadSdkException;
-        |    public abstract View getView(
-        |            Context windowContext, Bundle params, int width, int height);
-        |    public final Context getContext() {
-        |        return null;
-        |    }
-        |    public abstract void onDataReceived(
-        |            Bundle data, DataReceivedCallback callback);
-        |    public interface DataReceivedCallback {
-        |        void onDataReceivedSuccess(Bundle params);
-        |        void onDataReceivedError(String errorMessage);
+        |    companion object {
+        |        fun create(binder: IBinder): SandboxedSdkCompat = throw RuntimeException("Stub!")
         |    }
         |}
         |""".trimMargin()
     ),
-    Source.java(
-        "android.app.sdksandbox.LoadSdkException", """
-        |package android.app.sdksandbox;
+    Source.kotlin(
+        "androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt", """
+        |package androidx.privacysandbox.sdkruntime.client
         |
-        |public final class LoadSdkException extends Exception {}
+        |import android.content.Context
+        |import android.os.Bundle
+        |import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+        |
+        |@Suppress("UNUSED_PARAMETER")
+        |class SdkSandboxManagerCompat private constructor() {
+        |    suspend fun loadSdk(
+        |        sdkName: String,
+        |        params: Bundle,
+        |    ): SandboxedSdkCompat = throw RuntimeException("Stub!")
+        |
+        |    companion object {
+        |        fun obtain(context: Context): SdkSandboxManagerCompat =
+        |            throw RuntimeException("Stub!")
+        |    }
+        |}
         |""".trimMargin()
     ),
-    Source.java(
-        "android.app.sdksandbox.SandboxedSdkContext", """
-        |package android.app.sdksandbox;
+    Source.kotlin(
+        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt", """
+        |package androidx.privacysandbox.sdkruntime.core
         |
-        |public final class SandboxedSdkContext {}
+        |import android.content.Context
+        |import android.os.Bundle
+        |import android.view.View
+        |
+        |@Suppress("UNUSED_PARAMETER")
+        |abstract class SandboxedSdkProviderCompat {
+        |   var context: Context? = null
+        |       private set
+        |   fun attachContext(context: Context): Unit = throw RuntimeException("Stub!")
+        |
+        |   abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
+        |
+        |   open fun beforeUnloadSdk() {}
+        |
+        |   abstract fun getView(
+        |       windowContext: Context,
+        |       params: Bundle,
+        |       width: Int,
+        |       height: Int
+        |   ): View
+        |}
         |""".trimMargin()
-    ),
+    )
 )
diff --git a/privacysandbox/ui/OWNERS b/privacysandbox/ui/OWNERS
new file mode 100644
index 0000000..329fc11
--- /dev/null
+++ b/privacysandbox/ui/OWNERS
@@ -0,0 +1,2 @@
+nator@google.com
+akulakov@google.com
diff --git a/privacysandbox/ui/ui-client/api/current.txt b/privacysandbox/ui/ui-client/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-client/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-client/api/res-current.txt b/privacysandbox/ui/ui-client/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ui/ui-client/api/res-current.txt
diff --git a/privacysandbox/ui/ui-client/api/restricted_current.txt b/privacysandbox/ui/ui-client/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-client/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
new file mode 100644
index 0000000..8c3be5e
--- /dev/null
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    // Add dependencies here
+}
+
+android {
+    namespace "androidx.privacysandbox.ui.client"
+}
+
+androidx {
+    name = "androidx.privacysandbox.ui:ui-client"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.PRIVACYSANDBOX_UI
+    inceptionYear = "2022"
+    description = "show UI from an SDKRuntime aware SDK"
+}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md
new file mode 100644
index 0000000..a414c19
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+Privacy Sandbox Ui Client
+
+# Package androidx.privacysandbox.ui.client
+
+This package lets an application host UI created by an SdkRuntime aware SDK.
diff --git a/privacysandbox/ui/ui-core/api/current.txt b/privacysandbox/ui/ui-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-core/api/res-current.txt b/privacysandbox/ui/ui-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ui/ui-core/api/res-current.txt
diff --git a/privacysandbox/ui/ui-core/api/restricted_current.txt b/privacysandbox/ui/ui-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-core/build.gradle b/privacysandbox/ui/ui-core/build.gradle
new file mode 100644
index 0000000..6d1734d
--- /dev/null
+++ b/privacysandbox/ui/ui-core/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    // Add dependencies here
+}
+
+android {
+    namespace "androidx.privacysandbox.ui.core"
+}
+
+androidx {
+    name = "androidx.privacysandbox.ui:ui-core"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.PRIVACYSANDBOX_UI
+    inceptionYear = "2022"
+    description = "contains core definitions for the privacysandbox ui library."
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md
new file mode 100644
index 0000000..aa56a66
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+Privacy Sandbox Ui Core
+
+# Package androidx.privacysandbox.ui.core
+This package contains interface and class definitions shared between Privacy
+Sandbox Ui Client and Privacy Sandbox Ui Provider.
diff --git a/privacysandbox/ui/ui-provider/api/current.txt b/privacysandbox/ui/ui-provider/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-provider/api/res-current.txt b/privacysandbox/ui/ui-provider/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/api/res-current.txt
diff --git a/privacysandbox/ui/ui-provider/api/restricted_current.txt b/privacysandbox/ui/ui-provider/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-provider/build.gradle b/privacysandbox/ui/ui-provider/build.gradle
new file mode 100644
index 0000000..51a6f39
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    // Add dependencies here
+}
+
+android {
+    namespace "androidx.privacysandbox.ui.provider"
+}
+
+androidx {
+    name = "androidx.privacysandbox.ui:ui-provider"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.PRIVACYSANDBOX_UI
+    inceptionYear = "2022"
+    description = "lets an SdkRuntime aware SDK share UI with a client application."
+}
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md
new file mode 100644
index 0000000..cafa9ea
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md
@@ -0,0 +1,8 @@
+# Module root
+
+Privacy Sandbox Ui Provider
+
+# Package androidx.privacysandbox.ui.Provider
+
+This package lets an SdkRuntime aware SDKs share UI to be hosted in their
+client application.
diff --git a/profileinstaller/profileinstaller/api/current.txt b/profileinstaller/profileinstaller/api/current.txt
index 8eed65c..83b7dce 100644
--- a/profileinstaller/profileinstaller/api/current.txt
+++ b/profileinstaller/profileinstaller/api/current.txt
@@ -4,6 +4,7 @@
   public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
     ctor public ProfileInstallReceiver();
     method public void onReceive(android.content.Context, android.content.Intent?);
+    field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
     field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
     field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
     field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
@@ -18,6 +19,9 @@
     field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
     field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
     field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+    field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+    field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+    field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
     field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
     field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
     field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
diff --git a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
index 8eed65c..83b7dce 100644
--- a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
+++ b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
@@ -4,6 +4,7 @@
   public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
     ctor public ProfileInstallReceiver();
     method public void onReceive(android.content.Context, android.content.Intent?);
+    field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
     field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
     field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
     field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
@@ -18,6 +19,9 @@
     field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
     field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
     field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+    field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+    field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+    field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
     field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
     field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
     field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
diff --git a/profileinstaller/profileinstaller/api/restricted_current.txt b/profileinstaller/profileinstaller/api/restricted_current.txt
index 8eed65c..83b7dce 100644
--- a/profileinstaller/profileinstaller/api/restricted_current.txt
+++ b/profileinstaller/profileinstaller/api/restricted_current.txt
@@ -4,6 +4,7 @@
   public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
     ctor public ProfileInstallReceiver();
     method public void onReceive(android.content.Context, android.content.Intent?);
+    field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
     field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
     field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
     field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
@@ -18,6 +19,9 @@
     field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
     field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
     field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+    field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+    field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+    field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
     field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
     field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
     field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/BenchmarkOperation.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/BenchmarkOperation.java
new file mode 100644
index 0000000..c57f7b4
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/BenchmarkOperation.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.profileinstaller;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.io.File;
+
+class BenchmarkOperation {
+    private BenchmarkOperation() {}
+
+    static void dropShaderCache(
+            @NonNull Context context,
+            @NonNull ProfileInstallReceiver.ResultDiagnostics callback
+    ) {
+        File shaderDirectory;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            // shaders started using code cache dir once it was added in N
+            shaderDirectory = Api24ContextHelper.getDeviceProtectedCodeCacheDir(context);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            // getCodeCacheDir was added in L, but not used by platform for shaders until M
+            shaderDirectory = Api21ContextHelper.getCodeCacheDir(context);
+        } else {
+            shaderDirectory = context.getCacheDir();
+        }
+        if (deleteFilesRecursively(shaderDirectory)) {
+            callback.onResultReceived(ProfileInstaller.RESULT_BENCHMARK_OPERATION_SUCCESS, null);
+        } else {
+            callback.onResultReceived(ProfileInstaller.RESULT_BENCHMARK_OPERATION_FAILURE, null);
+        }
+
+    }
+
+    /**
+     * Returns true on success
+     *
+     * If hits a failure to access a directory, returns false but keeps going
+     */
+    static boolean deleteFilesRecursively(File file) {
+        if (file.isDirectory()) {
+            File[] children = file.listFiles();
+            if (children == null) {
+                return false;
+            }
+            boolean success = true;
+            for (File child : children) {
+                success = deleteFilesRecursively(child) && success;
+            }
+            return success;
+        } else {
+            //noinspection ResultOfMethodCallIgnored
+            file.delete();
+            return true;
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    private static class Api21ContextHelper {
+        static File getCodeCacheDir(Context context) {
+            // Code cache dir added in 21
+            return context.getCodeCacheDir();
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    private static class Api24ContextHelper {
+        static File getDeviceProtectedCodeCacheDir(Context context) {
+            // Code device protected storage added in 24
+            return context.createDeviceProtectedStorageContext().getCodeCacheDir();
+        }
+    }
+}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
index 31f7354..294c6ed 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
@@ -53,6 +53,7 @@
      */
     public static final @NonNull String ACTION_SAVE_PROFILE =
             "androidx.profileinstaller.action.SAVE_PROFILE";
+
     /**
      * This is an action constant which requests that {@link ProfileInstaller} manipulate the
      * skip file used during profile installation. This is only useful when the app is being
@@ -62,6 +63,13 @@
             "androidx.profileinstaller.action.SKIP_FILE";
 
     /**
+     * This is an action that triggers actions required for stable benchmarking from an external
+     * tool on user builds, such as clearing the code cache, or triggering garbage collection.
+     */
+    public static final @NonNull String ACTION_BENCHMARK_OPERATION =
+            "androidx.profileinstaller.action.BENCHMARK_OPERATION";
+
+    /**
      * This is the key in the {@link Bundle} of extras, which provides additional information on
      * the operation to be performed.
      */
@@ -76,6 +84,18 @@
      */
     private static final @NonNull String EXTRA_SKIP_FILE_OPERATION_DELETE = "DELETE_SKIP_FILE";
 
+    /**
+     * This is the key in the {@link Bundle} of extras, which provides additional information on
+     * the operation to be performed.
+     */
+    private static final @NonNull String EXTRA_BENCHMARK_OPERATION = "EXTRA_BENCHMARK_OPERATION";
+
+    /**
+     * The value that requests the shader cache be dropped.
+     */
+    private static final @NonNull String EXTRA_BENCHMARK_OPERATION_DROP_SHADER_CACHE =
+            "DROP_SHADER_CACHE";
+
     @Override
     public void onReceive(@NonNull Context context, @Nullable Intent intent) {
         if (intent == null) return;
@@ -96,6 +116,20 @@
             }
         } else if (ACTION_SAVE_PROFILE.equals(action)) {
             saveProfile(new ResultDiagnostics());
+        } else if (ACTION_BENCHMARK_OPERATION.equals(action)) {
+            Bundle extras = intent.getExtras();
+            if (extras != null) {
+                String operation = extras.getString(EXTRA_BENCHMARK_OPERATION);
+                ResultDiagnostics diagnostics = new ResultDiagnostics();
+                if (EXTRA_BENCHMARK_OPERATION_DROP_SHADER_CACHE.equals(operation)) {
+                    BenchmarkOperation.dropShaderCache(context, diagnostics);
+                } else {
+                    diagnostics.onResultReceived(
+                            ProfileInstaller.RESULT_BENCHMARK_OPERATION_UNKNOWN,
+                            null
+                    );
+                }
+            }
         }
     }
 
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index 357b96f..f9c63c5 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -237,7 +237,10 @@
             RESULT_INSTALL_SKIP_FILE_SUCCESS,
             RESULT_DELETE_SKIP_FILE_SUCCESS,
             RESULT_SAVE_PROFILE_SIGNALLED,
-            RESULT_SAVE_PROFILE_SKIPPED
+            RESULT_SAVE_PROFILE_SKIPPED,
+            RESULT_BENCHMARK_OPERATION_SUCCESS,
+            RESULT_BENCHMARK_OPERATION_FAILURE,
+            RESULT_BENCHMARK_OPERATION_UNKNOWN
     })
     public @interface ResultCode {}
 
@@ -321,6 +324,22 @@
     @ResultCode public static final int RESULT_SAVE_PROFILE_SKIPPED = 13;
 
     /**
+     * Indicates that the benchmark operation was successful
+     */
+    @ResultCode public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14;
+
+    /**
+     * Indicates that the benchmark operation failed
+     */
+    @ResultCode public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15;
+
+    /**
+     * Indicates that the benchmark operation was unknown, likely meaning profileinstaller needs
+     * to update to support the operation
+     */
+    @ResultCode public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16;
+
+    /**
      * Check if we've already installed a profile for this app installation.
      *
      * @hide
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerSnappingTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerSnappingTest.java
index 7c8f178..baadf37 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerSnappingTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerSnappingTest.java
@@ -27,6 +27,7 @@
 import androidx.annotation.Nullable;
 import androidx.test.filters.LargeTest;
 
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -73,6 +74,7 @@
         return rv;
     }
 
+    @Ignore // b/244324972
     @Test
     public void snapOnScrollSameView() throws Throwable {
         final Config config = (Config) mConfig.clone();
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
index ffcc174..b56858e 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
@@ -34,11 +34,14 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.StateListDrawable;
 import android.os.Build;
+import android.os.Bundle;
 import android.util.SparseIntArray;
 import android.util.StateSet;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.GridView;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.core.view.AccessibilityDelegateCompat;
@@ -858,6 +861,151 @@
         assertEquals(GridView.class.getName(), info.getClassName());
     }
 
+    @Test
+    public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 0));
+        waitForFirstLayout(recyclerView);
+
+        final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+        assertFalse(nodeInfo.getActionList().contains(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGlm.onInitializeAccessibilityNodeInfo(nodeInfo);
+            }
+        });
+
+        assertFalse(nodeInfo.getActionList().contains(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
+    public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_addedWithNonEmptyList()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 1));
+        waitForFirstLayout(recyclerView);
+
+        final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+        assertFalse(nodeInfo.getActionList().contains(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGlm.onInitializeAccessibilityNodeInfo(nodeInfo);
+            }
+        });
+
+        assertTrue(nodeInfo.getActionList().contains(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+    }
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_withNullArgs_returnsFalse()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        waitForFirstLayout(recyclerView);
+
+        final boolean[] returnValue = {false};
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mGlm.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, null);
+            }
+        });
+
+        assertFalse(returnValue[0]);
+    }
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_noRow_returnsFalse()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        waitForFirstLayout(recyclerView);
+
+        Bundle bundle = new Bundle();
+        bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 10);
+
+        final boolean[] returnValue = {false};
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mGlm.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, bundle);
+            }
+        });
+
+        assertFalse(returnValue[0]);
+    }
+
+    @Test
+    public void performAccessibilityAction_actionScrollToPosition_noColumn_returnsFalse()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        waitForFirstLayout(recyclerView);
+
+        Bundle bundle = new Bundle();
+        bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
+
+        final boolean[] returnValue = {false};
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = mGlm.performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, bundle);
+            }
+        });
+
+        assertFalse(returnValue[0]);
+    }
+
+    @Test
+    public void performAccessibilityAction_withValidRowAndColumn_performsScroll() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        final GridLayoutManager.SpanSizeLookup spanSizeLookup =
+                new GridLayoutManager.SpanSizeLookup() {
+                    @Override
+                    public int getSpanSize(int position) {
+                        if (position % 5 == 0) {
+                            return 2;
+                        }
+                        return 1;
+                    }
+                };
+
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        mGlm.setSpanSizeLookup(spanSizeLookup);
+        /*
+        This generates the following grid, with items 1, 6, 11, etc. (at indices 0, 5, 10, etc.)
+        spanning two rows.
+        1   3   6   8   11  13  16  etc.
+            4       9       14      etc.
+        2   5   7   10  12  15  17  etc.
+        */
+        waitForFirstLayout(recyclerView);
+        mGlm.expectLayout(1);
+
+        Bundle bundle = new Bundle();
+        bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 0);
+        bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 2);
+
+        final boolean[] returnValue = {false};
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                returnValue[0] = recyclerView.getLayoutManager().performAccessibilityAction(
+                        android.R.id.accessibilityActionScrollToPosition, bundle);
+            }
+        });
+        mGlm.waitForLayout(2);
+
+        assertTrue(returnValue[0]);
+        assertEquals(((TextView) mGlm.getChildAt(0)).getText(), "Item (6)");
+    }
+
     public GridLayoutManager.LayoutParams ensureGridLp(View view) {
         ViewGroup.LayoutParams lp = view.getLayoutParams();
         GridLayoutManager.LayoutParams glp;
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
index 4c4aba2..d18f0b9 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
@@ -17,6 +17,7 @@
 
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.SparseIntArray;
@@ -25,6 +26,7 @@
 import android.widget.GridView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 
 import java.util.Arrays;
@@ -40,6 +42,7 @@
     private static final boolean DEBUG = false;
     private static final String TAG = "GridLayoutManager";
     public static final int DEFAULT_SPAN_COUNT = -1;
+
     /**
      * Span size have been changed but we've not done a new layout calculation.
      */
@@ -175,6 +178,57 @@
     }
 
     @Override
+    boolean performAccessibilityAction(int action, @Nullable Bundle args) {
+        if (action == android.R.id.accessibilityActionScrollToPosition) {
+            final int noRow = -1;
+            final int noColumn = -1;
+            if (args != null) {
+                int rowArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT,
+                        noRow);
+                int columnArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT,
+                        noColumn);
+
+                if (rowArg == noRow || columnArg == noColumn) {
+                    return false;
+                }
+
+                int itemCount = mRecyclerView.mAdapter.getItemCount();
+
+                int position = -1;
+                for (int i = 0; i < itemCount; i++) {
+                    // Corresponds to a column value if the orientation is VERTICAL and a row value
+                    // if the orientation is HORIZONTAL
+                    int spanIndex = getSpanIndex(mRecyclerView.mRecycler, mRecyclerView.mState, i);
+
+                    // Corresponds to a row value if the orientation is VERTICAL and a column value
+                    // if the orientation is HORIZONTAL
+                    int spanGroupIndex = getSpanGroupIndex(mRecyclerView.mRecycler,
+                            mRecyclerView.mState, i);
+
+                    if (mOrientation == VERTICAL) {
+                        if (spanIndex == columnArg && spanGroupIndex == rowArg) {
+                            position = i;
+                            break;
+                        }
+                    } else { // horizontal
+                        if (spanIndex == rowArg && spanGroupIndex == columnArg) {
+                            position = i;
+                            break;
+                        }
+                    }
+                }
+
+                if (position > -1) {
+                    scrollToPositionWithOffset(position, 0);
+                    return true;
+                }
+                return false;
+            }
+        }
+        return super.performAccessibilityAction(action, args);
+    }
+
+    @Override
     public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
         if (state.isPreLayout()) {
             cachePreLayoutSpanMapping();
@@ -1452,5 +1506,4 @@
             return mSpanSize;
         }
     }
-
-}
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/build.gradle b/room/room-compiler-processing-testing/build.gradle
index e314215..a26f969 100644
--- a/room/room-compiler-processing-testing/build.gradle
+++ b/room/room-compiler-processing-testing/build.gradle
@@ -37,14 +37,12 @@
 }
 
 /**
- * Create a properties file with versions that can be read from the test helper to setup test
- * projects.
+ * Create a properties file with versions that can be read from the test helper to setup test projects.
  * see: b/178725084
  */
-def testPropsOutDir = project.layout.buildDirectory.dir("test-config")
 def writeTestPropsTask = tasks.register("prepareTestConfiguration", WriteProperties.class) {
     description = "Generates a properties file with the current environment for compilation tests"
-    setOutputFile(testPropsOutDir.map {
+    setOutputFile(project.layout.buildDirectory.dir("test-config").map {
         it.file("androidx.room.compiler.processing.util.CompilationTestCapabilities.Config" +
                 ".properties")
     })
@@ -55,18 +53,11 @@
 java {
     sourceSets {
         main {
-            resources.srcDir(testPropsOutDir)
+            resources.srcDir(writeTestPropsTask.map { it.outputFile.parentFile })
         }
     }
 }
 
-tasks.named("sourceJar").configure {
-    dependsOn(writeTestPropsTask)
-}
-tasks.named("processResources").configure {
-    dependsOn(writeTestPropsTask)
-}
-
 // enable opt in only for tests so that we don't create non experimental APIs by mistake
 // in the source.
 tasks.named("compileTestKotlin", KotlinCompile.class).configure {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
index 18048d5..6f3ad36 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
@@ -32,8 +32,10 @@
 import com.squareup.kotlinpoet.javapoet.JClassName
 import com.squareup.kotlinpoet.javapoet.JParameterizedTypeName
 import com.squareup.kotlinpoet.javapoet.JTypeName
+import com.squareup.kotlinpoet.javapoet.JWildcardTypeName
 import com.squareup.kotlinpoet.javapoet.KClassName
 import com.squareup.kotlinpoet.javapoet.KTypeName
+import com.squareup.kotlinpoet.javapoet.KWildcardTypeName
 import kotlin.reflect.KClass
 
 /**
@@ -105,6 +107,11 @@
         val PRIMITIVE_FLOAT = Float::class.asPrimitiveTypeName()
         val PRIMITIVE_DOUBLE = Double::class.asPrimitiveTypeName()
 
+        val ANY_WILDCARD = XTypeName(
+            java = JWildcardTypeName.subtypeOf(Object::class.java),
+            kotlin = com.squareup.kotlinpoet.STAR
+        )
+
         /**
          * The default [KTypeName] returned by xprocessing APIs when the backend is not KSP.
          */
@@ -126,7 +133,7 @@
          * the equivalent Kotlin and Java type names are represented, [IntArray] and `int[]`
          * respectively.
          */
-        fun getArrayTypeName(componentTypeName: XTypeName): XTypeName {
+        fun getArrayName(componentTypeName: XTypeName): XTypeName {
             val (java, kotlin) = when (componentTypeName) {
                 PRIMITIVE_BOOLEAN ->
                     JArrayTypeName.of(JTypeName.BOOLEAN) to BOOLEAN_ARRAY
@@ -150,6 +157,34 @@
             }
             return XTypeName(java, kotlin)
         }
+
+        /**
+         * Create a contravariant wildcard type name, to use as a consumer site-variance
+         * declaration.
+         *
+         * In Java: `? super <bound>`
+         * In Kotlin `in <bound>
+         */
+        fun getConsumerSuperName(bound: XTypeName): XTypeName {
+            return XTypeName(
+                java = JWildcardTypeName.supertypeOf(bound.java),
+                kotlin = KWildcardTypeName.consumerOf(bound.kotlin)
+            )
+        }
+
+        /**
+         * Create a covariant wildcard type name, to use as a producer site-variance
+         * declaration.
+         *
+         * In Java: `? extends <bound>`
+         * In Kotlin `out <bound>
+         */
+        fun getProducerExtendsName(bound: XTypeName): XTypeName {
+            return XTypeName(
+                java = JWildcardTypeName.subtypeOf(bound.java),
+                kotlin = KWildcardTypeName.producerOf(bound.kotlin)
+            )
+        }
     }
 }
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
index a88bb07..bcdf483 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
@@ -65,7 +65,7 @@
 object RoomTypeNames {
     val STRING_UTIL: XClassName = XClassName.get("$ROOM_PACKAGE.util", "StringUtil")
     val ROOM_DB: XClassName = XClassName.get(ROOM_PACKAGE, "RoomDatabase")
-    val ROOM_DB_KT: ClassName = ClassName.get(ROOM_PACKAGE, "RoomDatabaseKt")
+    val ROOM_DB_KT = XClassName.get(ROOM_PACKAGE, "RoomDatabaseKt")
     val ROOM_DB_CONFIG: ClassName = ClassName.get(ROOM_PACKAGE, "DatabaseConfiguration")
     val INSERTION_ADAPTER: XClassName =
         XClassName.get(ROOM_PACKAGE, "EntityInsertionAdapter")
@@ -247,7 +247,7 @@
 
 object KotlinTypeNames {
     val UNIT = ClassName.get("kotlin", "Unit")
-    val CONTINUATION = ClassName.get("kotlin.coroutines", "Continuation")
+    val CONTINUATION = XClassName.get("kotlin.coroutines", "Continuation")
     val COROUTINE_SCOPE = ClassName.get("kotlinx.coroutines", "CoroutineScope")
     val CHANNEL = ClassName.get("kotlinx.coroutines.channels", "Channel")
     val RECEIVE_CHANNEL = ClassName.get("kotlinx.coroutines.channels", "ReceiveChannel")
@@ -260,6 +260,8 @@
         RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndex")
     val ROOM_SQL_QUERY_ACQUIRE =
         RoomTypeNames.ROOM_SQL_QUERY.companionMember("acquire", isJvmStatic = true)
+    val ROOM_DATABASE_WITH_TRANSACTION =
+        RoomTypeNames.ROOM_DB_KT.packageMember("withTransaction")
 }
 
 val DEFERRED_TYPES = listOf(
@@ -332,29 +334,29 @@
     )
 }
 
-fun Function1TypeSpecBuilder(
-    parameterTypeName: TypeName,
+fun Function1TypeSpec(
+    language: CodeLanguage,
+    parameterTypeName: XTypeName,
     parameterName: String,
-    returnTypeName: TypeName,
-    callBody: MethodSpec.Builder.() -> Unit
-) = TypeSpec.anonymousClassBuilder("").apply {
+    returnTypeName: XTypeName,
+    callBody: XFunSpec.Builder.() -> Unit
+) = XTypeSpec.anonymousClassBuilder(language, "").apply {
     superclass(
-        ParameterizedTypeName.get(
-            Function1::class.typeName,
-            parameterTypeName,
-            returnTypeName
-        )
+        Function1::class.asClassName().parametrizedBy(parameterTypeName, returnTypeName)
     )
-    addMethod(
-        MethodSpec.methodBuilder("invoke").apply {
+    addFunction(
+        XFunSpec.builder(
+            language = language,
+            name = "invoke",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
             addParameter(parameterTypeName, parameterName)
             returns(returnTypeName)
-            addModifiers(Modifier.PUBLIC)
-            addAnnotation(Override::class.java)
             callBody()
         }.build()
     )
-}
+}.build()
 
 /**
  * Generates a 2D array literal where the value at `i`,`j` will be produced by `valueProducer.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index b299f50..043a9896 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -190,7 +190,7 @@
 
     override fun findTransactionMethodBinder(callType: TransactionMethod.CallType) =
         InstantTransactionMethodBinder(
-            TransactionMethodAdapter(executableElement.jvmName, callType)
+            TransactionMethodAdapter(executableElement.name, executableElement.jvmName, callType)
         )
 }
 
@@ -206,7 +206,7 @@
 
     private val continuationParam: XVariableElement by lazy {
         val continuationType = context.processingEnv
-            .requireType(KotlinTypeNames.CONTINUATION.toString()).rawType
+            .requireType(KotlinTypeNames.CONTINUATION).rawType
         executableElement.parameters.last {
             it.type.rawType == continuationType
         }
@@ -300,8 +300,12 @@
 
     override fun findTransactionMethodBinder(callType: TransactionMethod.CallType) =
         CoroutineTransactionMethodBinder(
-            adapter = TransactionMethodAdapter(executableElement.jvmName, callType),
+            adapter = TransactionMethodAdapter(
+                executableElement.name,
+                executableElement.jvmName,
+                callType
+            ),
             continuationParamName = continuationParam.name,
-            useLambdaSyntax = context.processingEnv.jvmVersion >= 8
+            javaLambdaSyntaxAvailable = context.processingEnv.jvmVersion >= 8
         )
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableDeleteOrUpdateMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableDeleteOrUpdateMethodBinder.kt
index 4c8dff9..5df5ca4 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableDeleteOrUpdateMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableDeleteOrUpdateMethodBinder.kt
@@ -17,6 +17,8 @@
 package androidx.room.solver.shortcut.binder
 
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.CallableTypeSpecBuilder
 import androidx.room.solver.CodeGenScope
@@ -49,8 +51,8 @@
 
     override fun convertAndReturn(
         parameters: List<ShortcutQueryParameter>,
-        adapters: Map<String, Pair<XPropertySpec, TypeSpec>>,
-        dbField: FieldSpec,
+        adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>,
+        dbProperty: XPropertySpec,
         scope: CodeGenScope
     ) {
         val adapterScope = scope.fork()
@@ -58,14 +60,14 @@
             adapter?.createDeleteOrUpdateMethodBody(
                 parameters = parameters,
                 adapters = adapters,
-                dbField = dbField,
+                dbProperty = dbProperty,
                 scope = adapterScope
             )
             addCode(adapterScope.builder().build())
         }.build()
 
         scope.builder().apply {
-            addStmntBlock(callableImpl, dbField)
+            addStmntBlock(callableImpl, dbProperty.toJavaPoet())
         }
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/DeleteOrUpdateMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/DeleteOrUpdateMethodBinder.kt
index 08a7d50..2fcce7c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/DeleteOrUpdateMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/DeleteOrUpdateMethodBinder.kt
@@ -17,11 +17,10 @@
 package androidx.room.solver.shortcut.binder
 
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.shortcut.result.DeleteOrUpdateMethodAdapter
 import androidx.room.vo.ShortcutQueryParameter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.TypeSpec
 
 /**
  * Connects the delete or update method, the database and the [DeleteOrUpdateMethodAdapter].
@@ -56,8 +55,8 @@
      */
     abstract fun convertAndReturn(
         parameters: List<ShortcutQueryParameter>,
-        adapters: Map<String, Pair<XPropertySpec, TypeSpec>>,
-        dbField: FieldSpec,
+        adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>,
+        dbProperty: XPropertySpec,
         scope: CodeGenScope
     )
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantDeleteOrUpdateMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantDeleteOrUpdateMethodBinder.kt
index c8de492..7c74c77 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantDeleteOrUpdateMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantDeleteOrUpdateMethodBinder.kt
@@ -17,12 +17,10 @@
 package androidx.room.solver.shortcut.binder
 
 import androidx.room.compiler.codegen.XPropertySpec
-import androidx.room.ext.N
+import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.shortcut.result.DeleteOrUpdateMethodAdapter
 import androidx.room.vo.ShortcutQueryParameter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.TypeSpec
 
 /**
  * Binder that knows how to write instant (blocking) delete and update methods.
@@ -33,17 +31,17 @@
 
     override fun convertAndReturn(
         parameters: List<ShortcutQueryParameter>,
-        adapters: Map<String, Pair<XPropertySpec, TypeSpec>>,
-        dbField: FieldSpec,
+        adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>,
+        dbProperty: XPropertySpec,
         scope: CodeGenScope
     ) {
-        scope.builder().apply {
-            addStatement("$N.assertNotSuspendingTransaction()", dbField)
+        scope.builder.apply {
+            addStatement("%N.assertNotSuspendingTransaction()", dbProperty)
         }
         adapter?.createDeleteOrUpdateMethodBody(
             parameters = parameters,
             adapters = adapters,
-            dbField = dbField,
+            dbProperty = dbProperty,
             scope = scope
         )
     }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
index 7c64f3f..2fad367 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
@@ -17,8 +17,6 @@
 package androidx.room.solver.shortcut.binder
 
 import androidx.room.compiler.codegen.XPropertySpec
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.N
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.shortcut.result.InsertOrUpsertMethodAdapter
 import androidx.room.vo.ShortcutQueryParameter
@@ -35,9 +33,8 @@
         dbProperty: XPropertySpec,
         scope: CodeGenScope
     ) {
-        val dbField = dbProperty.toJavaPoet()
-        scope.builder().apply {
-            addStatement("$N.assertNotSuspendingTransaction()", dbField)
+        scope.builder.apply {
+            addStatement("%N.assertNotSuspendingTransaction()", dbProperty)
         }
         adapter?.createMethodBody(
             parameters = parameters,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/DeleteOrUpdateMethodAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/DeleteOrUpdateMethodAdapter.kt
index 0d164c9..e754baa 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/DeleteOrUpdateMethodAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/DeleteOrUpdateMethodAdapter.kt
@@ -16,21 +16,22 @@
 
 package androidx.room.solver.shortcut.result
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isInt
 import androidx.room.compiler.processing.isKotlinUnit
 import androidx.room.compiler.processing.isVoid
 import androidx.room.compiler.processing.isVoidObject
 import androidx.room.ext.KotlinTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.T
+import androidx.room.ext.isNotKotlinUnit
+import androidx.room.ext.isNotVoid
+import androidx.room.ext.isNotVoidObject
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.ShortcutQueryParameter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
 
 /**
  * Class that knows how to generate a delete or update method body.
@@ -54,8 +55,8 @@
 
     fun createDeleteOrUpdateMethodBody(
         parameters: List<ShortcutQueryParameter>,
-        adapters: Map<String, Pair<XPropertySpec, TypeSpec>>,
-        dbField: FieldSpec,
+        adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>,
+        dbProperty: XPropertySpec,
         scope: CodeGenScope
     ) {
         val resultVar = if (hasResultValue(returnType)) {
@@ -63,45 +64,46 @@
         } else {
             null
         }
-        scope.builder().apply {
+        scope.builder.apply {
             if (resultVar != null) {
-                addStatement("$T $L = 0", TypeName.INT, resultVar)
+                addLocalVariable(
+                    name = resultVar,
+                    typeName = XTypeName.PRIMITIVE_INT,
+                    isMutable = true,
+                    assignExpr = XCodeBlock.of(language, "0")
+                )
             }
-            addStatement("$N.beginTransaction()", dbField)
+            addStatement("%N.beginTransaction()", dbProperty)
             beginControlFlow("try").apply {
                 parameters.forEach { param ->
                     val adapter = adapters.getValue(param.name).first
                     addStatement(
-                        "$L$L.$L($L)",
-                        if (resultVar == null) "" else "$resultVar +=",
-                        adapter.name, param.handleMethodName(), param.name
+                        "%L%L.%L(%L)",
+                        if (resultVar == null) "" else "$resultVar += ",
+                        adapter.name,
+                        param.handleMethodName(),
+                        param.name
                     )
                 }
-                addStatement("$N.setTransactionSuccessful()", dbField)
+                addStatement("%N.setTransactionSuccessful()", dbProperty)
                 if (resultVar != null) {
-                    addStatement("return $L", resultVar)
-                } else if (hasNullReturn(returnType)) {
+                    addStatement("return %L", resultVar)
+                } else if (returnType.isVoidObject()) {
                     addStatement("return null")
-                } else if (hasUnitReturn(returnType)) {
-                    addStatement("return $T.INSTANCE", KotlinTypeNames.UNIT)
+                } else if (returnType.isKotlinUnit() && scope.language == CodeLanguage.JAVA) {
+                    addStatement("return %T.INSTANCE", KotlinTypeNames.UNIT)
                 }
             }
             nextControlFlow("finally").apply {
-                addStatement("$N.endTransaction()", dbField)
+                addStatement("%N.endTransaction()", dbProperty)
             }
             endControlFlow()
         }
     }
 
     private fun hasResultValue(returnType: XType): Boolean {
-        return !(
-            returnType.isVoid() ||
-                returnType.isVoidObject() ||
-                returnType.isKotlinUnit()
-            )
+        return returnType.isNotVoid() &&
+            returnType.isNotVoidObject() &&
+            returnType.isNotKotlinUnit()
     }
-
-    private fun hasNullReturn(returnType: XType) = returnType.isVoidObject()
-
-    private fun hasUnitReturn(returnType: XType) = returnType.isKotlinUnit()
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/InsertOrUpsertMethodAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/InsertOrUpsertMethodAdapter.kt
index 309859e..56d437e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/InsertOrUpsertMethodAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/InsertOrUpsertMethodAdapter.kt
@@ -132,7 +132,6 @@
             )
         }
 
-        @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
         private fun getReturnType(returnType: XType): ReturnType? {
             return if (returnType.isVoid()) {
                 ReturnType.VOID
@@ -250,11 +249,11 @@
         SINGLE_ID("AndReturnId", XTypeName.PRIMITIVE_LONG), // return long
         ID_ARRAY(
             "AndReturnIdsArray",
-            XTypeName.getArrayTypeName(XTypeName.PRIMITIVE_LONG)
+            XTypeName.getArrayName(XTypeName.PRIMITIVE_LONG)
         ), // return long[]
         ID_ARRAY_BOX(
             "AndReturnIdsArrayBox",
-            XTypeName.getArrayTypeName(Long::class.asClassName())
+            XTypeName.getArrayName(Long::class.asClassName())
         ), // return Long[]
         ID_LIST(
             "AndReturnIdsList",
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
index 9d44ef0..caada40 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
@@ -16,20 +16,20 @@
 
 package androidx.room.solver.transaction.binder
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.Function1TypeSpecBuilder
-import androidx.room.ext.KotlinTypeNames.CONTINUATION
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.RoomTypeNames.ROOM_DB_KT
-import androidx.room.ext.T
+import androidx.room.ext.Function1TypeSpec
+import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.RoomMemberNames
+import androidx.room.ext.isNotKotlinUnit
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.transaction.result.TransactionMethodAdapter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.WildcardTypeName
 
 /**
  * Binder that knows how to write suspending transaction wrapper methods.
@@ -37,45 +37,96 @@
 class CoroutineTransactionMethodBinder(
     adapter: TransactionMethodAdapter,
     private val continuationParamName: String,
-    private val useLambdaSyntax: Boolean
+    private val javaLambdaSyntaxAvailable: Boolean
 ) : TransactionMethodBinder(adapter) {
     override fun executeAndReturn(
         returnType: XType,
         parameterNames: List<String>,
-        daoName: ClassName,
-        daoImplName: ClassName,
-        dbField: FieldSpec,
+        daoName: XClassName,
+        daoImplName: XClassName,
+        dbProperty: XPropertySpec,
+        scope: CodeGenScope
+    ) {
+        when (scope.language) {
+            CodeLanguage.JAVA -> executeAndReturnJava(
+                returnType, parameterNames, daoName, daoImplName, dbProperty, scope
+            )
+            CodeLanguage.KOTLIN -> executeAndReturnKotlin(
+                returnType, parameterNames, daoName, daoImplName, dbProperty, scope
+            )
+        }
+    }
+
+    private fun executeAndReturnJava(
+        returnType: XType,
+        parameterNames: List<String>,
+        daoName: XClassName,
+        daoImplName: XClassName,
+        dbProperty: XPropertySpec,
         scope: CodeGenScope
     ) {
         val innerContinuationParamName = "__cont"
         val adapterScope = scope.fork()
         adapter.createDelegateToSuperCode(
-            returnType = returnType,
             parameterNames = parameterNames + innerContinuationParamName,
             daoName = daoName,
             daoImplName = daoImplName,
-            returnStmt = !useLambdaSyntax,
+            returnStmt = !javaLambdaSyntaxAvailable,
             scope = adapterScope
         )
-        val functionImpl: Any = if (useLambdaSyntax) {
-            CodeBlock.of("($L) -> $L", innerContinuationParamName, adapterScope.builder().build())
+        val functionImpl: Any = if (javaLambdaSyntaxAvailable) {
+            XCodeBlock.of(
+                scope.language,
+                "(%L) -> %L",
+                innerContinuationParamName, adapterScope.builder().build()
+            )
         } else {
-            Function1TypeSpecBuilder(
-                parameterTypeName = ParameterizedTypeName.get(
-                    CONTINUATION, WildcardTypeName.supertypeOf(returnType.typeName)
+            Function1TypeSpec(
+                language = scope.language,
+                parameterTypeName = KotlinTypeNames.CONTINUATION.parametrizedBy(
+                    XTypeName.getConsumerSuperName(returnType.asTypeName())
                 ),
                 parameterName = innerContinuationParamName,
-                returnTypeName = ClassName.OBJECT
+                returnTypeName = Any::class.asClassName()
             ) {
-                addStatement(adapterScope.builder().build())
-            }.build()
+                addStatement("%L", adapterScope.generate())
+            }
         }
 
-        scope.builder().apply {
-            addStatement(
-                "return $T.withTransaction($N, $L, $N)",
-                ROOM_DB_KT, dbField, functionImpl, continuationParamName
+        scope.builder.addStatement(
+            "return %M(%N, %L, %L)",
+            RoomMemberNames.ROOM_DATABASE_WITH_TRANSACTION,
+            dbProperty,
+            functionImpl,
+            continuationParamName
+        )
+    }
+
+    private fun executeAndReturnKotlin(
+        returnType: XType,
+        parameterNames: List<String>,
+        daoName: XClassName,
+        daoImplName: XClassName,
+        dbProperty: XPropertySpec,
+        scope: CodeGenScope
+    ) {
+        scope.builder.apply {
+            if (returnType.isNotKotlinUnit()) {
+                add("return ")
+            }
+            beginControlFlow(
+                "%N.%M",
+                dbProperty, RoomMemberNames.ROOM_DATABASE_WITH_TRANSACTION
             )
+            val adapterScope = scope.fork()
+            adapter.createDelegateToSuperCode(
+                parameterNames = parameterNames,
+                daoName = daoName,
+                daoImplName = daoImplName,
+                scope = adapterScope
+            )
+            addStatement("%L", adapterScope.generate())
+            endControlFlow()
         }
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/InstantTransactionMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/InstantTransactionMethodBinder.kt
index 4e41b81..6510c5b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/InstantTransactionMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/InstantTransactionMethodBinder.kt
@@ -16,13 +16,16 @@
 
 package androidx.room.solver.transaction.binder
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.N
+import androidx.room.compiler.processing.isKotlinUnit
+import androidx.room.ext.isNotKotlinUnit
 import androidx.room.ext.isNotVoid
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.transaction.result.TransactionMethodAdapter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
 
 /**
  * Binder that knows how to write instant (blocking) transaction wrapper methods.
@@ -33,39 +36,46 @@
     override fun executeAndReturn(
         returnType: XType,
         parameterNames: List<String>,
-        daoName: ClassName,
-        daoImplName: ClassName,
-        dbField: FieldSpec,
+        daoName: XClassName,
+        daoImplName: XClassName,
+        dbProperty: XPropertySpec,
         scope: CodeGenScope
     ) {
-        scope.builder().apply {
-            addStatement("$N.beginTransaction()", dbField)
+        scope.builder.apply {
+            addStatement("%N.beginTransaction()", dbProperty)
             beginControlFlow("try").apply {
-                val returnsValue = returnType.isNotVoid()
+                val returnsValue = returnType.isNotVoid() && returnType.isNotKotlinUnit()
                 val resultVar = if (returnsValue) {
                     scope.getTmpVar("_result")
                 } else {
                     null
                 }
+                if (resultVar != null) {
+                    addLocalVariable(
+                        name = resultVar,
+                        typeName = returnType.asTypeName()
+                    )
+                }
 
                 val adapterScope = scope.fork()
                 adapter.createDelegateToSuperCode(
-                    returnType = returnType,
                     parameterNames = parameterNames,
                     daoName = daoName,
                     daoImplName = daoImplName,
                     resultVar = resultVar,
                     scope = adapterScope
                 )
-                addStatement(adapterScope.builder().build())
+                addStatement("%L", adapterScope.generate())
 
-                addStatement("$N.setTransactionSuccessful()", dbField)
-                if (returnsValue) {
-                    addStatement("return $N", resultVar)
+                addStatement("%N.setTransactionSuccessful()", dbProperty)
+                if (resultVar != null) {
+                    addStatement("return %N", resultVar)
+                } else if (returnType.isKotlinUnit() && language == CodeLanguage.JAVA) {
+                    addStatement("return %T.INSTANCE", Unit::class.asClassName())
                 }
             }
             nextControlFlow("finally").apply {
-                addStatement("$N.endTransaction()", dbField)
+                addStatement("%N.endTransaction()", dbProperty)
             }
             endControlFlow()
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/TransactionMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/TransactionMethodBinder.kt
index fb5f27b..7b32d8c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/TransactionMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/TransactionMethodBinder.kt
@@ -16,11 +16,11 @@
 
 package androidx.room.solver.transaction.binder
 
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.processing.XType
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.transaction.result.TransactionMethodAdapter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
 
 /**
  * Connects a transaction method, database and a [TransactionMethodAdapter].
@@ -38,9 +38,9 @@
     abstract fun executeAndReturn(
         returnType: XType,
         parameterNames: List<String>,
-        daoName: ClassName,
-        daoImplName: ClassName,
-        dbField: FieldSpec,
+        daoName: XClassName,
+        daoImplName: XClassName,
+        dbProperty: XPropertySpec,
         scope: CodeGenScope
     )
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/result/TransactionMethodAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/result/TransactionMethodAdapter.kt
index 373864d..9c21f73 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/result/TransactionMethodAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/result/TransactionMethodAdapter.kt
@@ -16,14 +16,12 @@
 
 package androidx.room.solver.transaction.result
 
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.ext.DEFAULT_IMPLS_CLASS_NAME
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.TransactionMethod
-import com.squareup.javapoet.ClassName
 
 /**
  * Class that knows how to generate the transaction method delegate code. Callers should take
@@ -31,64 +29,90 @@
  */
 class TransactionMethodAdapter(
     private val methodName: String,
+    private val jvmMethodName: String,
     private val callType: TransactionMethod.CallType
 ) {
     fun createDelegateToSuperCode(
-        returnType: XType,
         parameterNames: List<String>,
-        daoName: ClassName,
-        daoImplName: ClassName,
+        daoName: XClassName,
+        daoImplName: XClassName,
         resultVar: String? = null, // name of result var to assign to, null if none
         returnStmt: Boolean = false, // true or false to prepend statement with 'return'
         scope: CodeGenScope
     ) {
-        scope.builder().apply {
-            val params: MutableList<Any> = mutableListOf()
+        scope.builder.apply {
+            val args = mutableListOf<Any>()
             val format = buildString {
                 if (resultVar != null && returnStmt) {
-                    throw IllegalStateException(
-                        "Can't assign to var and return in the same statement."
-                    )
+                    error("Can't assign to var and return in the same statement.")
                 } else if (resultVar != null) {
-                    append("$T $L = ")
-                    params.add(returnType.typeName)
-                    params.add(resultVar)
+                    append("%L = ")
+                    args.add(resultVar)
                 } else if (returnStmt) {
                     append("return ")
                 }
-                when (callType) {
-                    TransactionMethod.CallType.CONCRETE,
-                    TransactionMethod.CallType.INHERITED_DEFAULT_JAVA8 -> {
-                        append("$T.super.$N(")
-                        params.add(daoImplName)
-                        params.add(methodName)
-                    }
-                    TransactionMethod.CallType.DEFAULT_JAVA8 -> {
-                        append("$T.super.$N(")
-                        params.add(daoName)
-                        params.add(methodName)
-                    }
-                    TransactionMethod.CallType.DEFAULT_KOTLIN -> {
-                        append("$T.$N.$N($T.this")
-                        params.add(daoName)
-                        params.add(DEFAULT_IMPLS_CLASS_NAME)
-                        params.add(methodName)
-                        params.add(daoImplName)
-                    }
+
+                val invokeExpr = when (scope.language) {
+                    CodeLanguage.JAVA -> scope.getJavaInvokeExpr(daoName, daoImplName)
+                    CodeLanguage.KOTLIN -> scope.getKotlinInvokeExpr(daoImplName)
                 }
-                var first = callType != TransactionMethod.CallType.DEFAULT_KOTLIN
-                parameterNames.forEach {
-                    if (first) {
-                        first = false
-                    } else {
+                append("%L")
+                args.add(invokeExpr)
+
+                if (scope.language == CodeLanguage.JAVA &&
+                    callType == TransactionMethod.CallType.DEFAULT_KOTLIN &&
+                    parameterNames.isNotEmpty()
+                ) {
+                    // An invoke to DefaultImpls has an extra 1st param so we need a comma if there
+                    // are more params.
+                    append(", ")
+                }
+                parameterNames.forEachIndexed { i, param ->
+                    append("%L")
+                    args.add(param)
+                    if (i < parameterNames.size - 1) {
                         append(", ")
                     }
-                    append(L)
-                    params.add(it)
                 }
                 append(")")
             }
-            add(format, *params.toTypedArray())
+            add(format, *args.toTypedArray())
         }
     }
+
+    private fun CodeGenScope.getJavaInvokeExpr(
+        daoName: XClassName,
+        daoImplName: XClassName,
+    ): XCodeBlock = when (callType) {
+        TransactionMethod.CallType.CONCRETE,
+        TransactionMethod.CallType.INHERITED_DEFAULT_JAVA8 -> {
+            XCodeBlock.of(
+                language,
+                "%T.super.%N(",
+                daoImplName, jvmMethodName
+            )
+        }
+        TransactionMethod.CallType.DEFAULT_JAVA8 -> {
+            XCodeBlock.of(
+                language,
+                "%T.super.%N(",
+                daoName, jvmMethodName
+            )
+        }
+        TransactionMethod.CallType.DEFAULT_KOTLIN -> {
+            XCodeBlock.of(
+                language,
+                "%T.%N.%N(%T.this",
+                daoName, DEFAULT_IMPLS_CLASS_NAME, jvmMethodName, daoImplName
+            )
+        }
+    }
+
+    private fun CodeGenScope.getKotlinInvokeExpr(
+        daoImplName: XClassName,
+    ): XCodeBlock = XCodeBlock.of(
+        language,
+        "super@%T.%N(",
+        daoImplName, methodName
+    )
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/SingleStatementTypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/SingleStatementTypeConverter.kt
index 084f28a..6efbe4b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/SingleStatementTypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/SingleStatementTypeConverter.kt
@@ -19,7 +19,6 @@
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.processing.XType
 import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.CodeBlock
 
 /**
  * A [TypeConverter] that has only 1 statement (e.g. foo ? bar : baz).
@@ -47,7 +46,7 @@
     }
 
     /**
-     * Returns a [CodeBlock] that will compute the [to] value.
+     * Returns a [XCodeBlock] that will compute the [to] value.
      */
     abstract fun buildStatement(
         inputVarName: String,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index a7ff843..4d378b7 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -28,7 +28,6 @@
 import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
 import androidx.room.compiler.codegen.asClassName
-import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XType
@@ -59,9 +58,6 @@
 import androidx.room.vo.UpdateMethod
 import androidx.room.vo.UpsertionMethod
 import androidx.room.vo.WriteQueryMethod
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.TypeSpec
-import com.squareup.kotlinpoet.javapoet.JWildcardTypeName
 import java.util.Arrays
 import java.util.Collections
 import java.util.Locale
@@ -232,13 +228,7 @@
         ).apply {
             returns(
                 List::class.asClassName().parametrizedBy(
-                    Class::class.asClassName().parametrizedBy(
-                        // TODO(b/249984508): Create XTypeName factory for type variable names
-                        XTypeName(
-                            java = JWildcardTypeName.subtypeOf(Object::class.java),
-                            kotlin = com.squareup.kotlinpoet.STAR
-                        )
-                    )
+                    Class::class.asClassName().parametrizedBy(XTypeName.ANY_WILDCARD)
                 )
             )
             addCode(body)
@@ -300,9 +290,9 @@
         method.methodBinder.executeAndReturn(
             returnType = method.returnType,
             parameterNames = method.parameterNames,
-            daoName = dao.typeName.toJavaPoet(),
-            daoImplName = dao.implTypeName.toJavaPoet(),
-            dbField = dbProperty.toJavaPoet(),
+            daoName = dao.typeName,
+            daoImplName = dao.implTypeName,
+            dbProperty = dbProperty,
             scope = scope
         )
         return overrideWithoutAnnotations(method.element, declaredDao)
@@ -474,7 +464,7 @@
     private fun <T : DeleteOrUpdateShortcutMethod> createShortcutMethods(
         methods: List<T>,
         methodPrefix: String,
-        implCallback: (T, ShortcutEntity) -> TypeSpec
+        implCallback: (T, ShortcutEntity) -> XTypeSpec
     ): List<PreparedStmtQuery> {
         return methods.mapNotNull { method ->
             val entities = method.entities
@@ -503,7 +493,7 @@
 
     private fun createDeleteOrUpdateMethodBody(
         method: DeleteOrUpdateShortcutMethod,
-        adapters: Map<String, Pair<XPropertySpec, TypeSpec>>
+        adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>
     ): XCodeBlock {
         if (adapters.isEmpty() || method.methodBinder == null) {
             return XCodeBlock.builder(codeLanguage).build()
@@ -513,7 +503,7 @@
         method.methodBinder.convertAndReturn(
             parameters = method.parameters,
             adapters = adapters,
-            dbField = dbProperty.toJavaPoet(),
+            dbProperty = dbProperty,
             scope = scope
         )
         return scope.generate()
@@ -545,7 +535,7 @@
 
     private fun createUpsertionMethodBody(
         method: UpsertionMethod,
-        upsertionAdapters: Map<String, Pair<XPropertySpec, CodeBlock>>
+        upsertionAdapters: Map<String, Pair<XPropertySpec, XCodeBlock>>
     ): XCodeBlock {
         if (upsertionAdapters.isEmpty() || method.methodBinder == null) {
             return XCodeBlock.builder(codeLanguage).build()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt
index 1a7220b..aeb8f5b2 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt
@@ -16,26 +16,22 @@
 
 package androidx.room.writer
 
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.RoomTypeNames.DELETE_OR_UPDATE_ADAPTER
-import androidx.room.ext.S
-import androidx.room.ext.SupportDbTypeNames.SQLITE_STMT
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.asClassName
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SupportDbTypeNames
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.FieldWithIndex
 import androidx.room.vo.Fields
 import androidx.room.vo.ShortcutEntity
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier.PUBLIC
 
 class EntityDeletionAdapterWriter private constructor(
     val tableName: String,
-    val pojoTypeName: TypeName,
+    val pojoTypeName: XTypeName,
     val fields: Fields
 ) {
     companion object {
@@ -49,54 +45,50 @@
             }
             return EntityDeletionAdapterWriter(
                 tableName = entity.tableName,
-                pojoTypeName = entity.pojo.typeName.toJavaPoet(),
+                pojoTypeName = entity.pojo.typeName,
                 fields = fieldsToUse
             )
         }
     }
 
-    fun createAnonymous(typeWriter: TypeWriter, dbParam: String): TypeSpec {
-        @Suppress("RemoveSingleExpressionStringTemplate")
-        return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
-            superclass(
-                ParameterizedTypeName.get(
-                    DELETE_OR_UPDATE_ADAPTER.toJavaPoet(),
-                    pojoTypeName
-                )
-            )
-            addMethod(
-                MethodSpec.methodBuilder("createQuery").apply {
-                    addAnnotation(Override::class.java)
-                    returns(ClassName.get("java.lang", "String"))
-                    addModifiers(PUBLIC)
+    fun createAnonymous(typeWriter: TypeWriter, dbParam: String): XTypeSpec {
+        return XTypeSpec.anonymousClassBuilder(
+            typeWriter.codeLanguage, "%L", dbParam
+        ).apply {
+            superclass(RoomTypeNames.DELETE_OR_UPDATE_ADAPTER.parametrizedBy(pojoTypeName))
+            addFunction(
+                XFunSpec.builder(
+                    language = language,
+                    name = "createQuery",
+                    visibility = VisibilityModifier.PUBLIC,
+                    isOverride = true
+                ).apply {
+                    returns(String::class.asClassName())
                     val query = "DELETE FROM `$tableName` WHERE " +
                         fields.columnNames.joinToString(" AND ") { "`$it` = ?" }
-                    addStatement("return $S", query)
+                    addStatement("return %S", query)
                 }.build()
             )
-            addMethod(
-                MethodSpec.methodBuilder("bind").apply {
-                    val bindScope = CodeGenScope(typeWriter)
-                    addAnnotation(Override::class.java)
-                    val stmtParam = "stmt"
-                    addParameter(
-                        ParameterSpec.builder(
-                            SQLITE_STMT.toJavaPoet(),
-                            stmtParam
-                        ).build()
-                    )
-                    val valueParam = "value"
-                    addParameter(ParameterSpec.builder(pojoTypeName, valueParam).build())
-                    returns(TypeName.VOID)
-                    addModifiers(PUBLIC)
+            addFunction(
+                XFunSpec.builder(
+                    language = language,
+                    name = "bind",
+                    visibility = VisibilityModifier.PUBLIC,
+                    isOverride = true
+                ).apply {
+                    val stmtParam = "statement"
+                    addParameter(SupportDbTypeNames.SQLITE_STMT, stmtParam)
+                    val entityParam = "entity"
+                    addParameter(pojoTypeName, entityParam)
                     val mapped = FieldWithIndex.byOrder(fields)
+                    val bindScope = CodeGenScope(typeWriter)
                     FieldReadWriteWriter.bindToStatement(
-                        ownerVar = valueParam,
+                        ownerVar = entityParam,
                         stmtParamVar = stmtParam,
                         fieldsWithIndices = mapped,
                         scope = bindScope
                     )
-                    addCode(bindScope.builder().build())
+                    addCode(bindScope.generate())
                 }.build()
             )
         }.build()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt
index 24984c9..fc17282 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt
@@ -16,24 +16,19 @@
 
 package androidx.room.writer
 
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.RoomTypeNames.DELETE_OR_UPDATE_ADAPTER
-import androidx.room.ext.S
-import androidx.room.ext.SupportDbTypeNames.SQLITE_STMT
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.asClassName
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SupportDbTypeNames
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.FieldWithIndex
 import androidx.room.vo.Fields
 import androidx.room.vo.Pojo
 import androidx.room.vo.ShortcutEntity
 import androidx.room.vo.columnNames
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier.PUBLIC
 
 class EntityUpdateAdapterWriter private constructor(
     val tableName: String,
@@ -51,20 +46,17 @@
             )
     }
 
-    fun createAnonymous(typeWriter: TypeWriter, dbParam: String): TypeSpec {
-        @Suppress("RemoveSingleExpressionStringTemplate")
-        return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
-            superclass(
-                ParameterizedTypeName.get(
-                    DELETE_OR_UPDATE_ADAPTER.toJavaPoet(),
-                    pojo.typeName.toJavaPoet()
-                )
-            )
-            addMethod(
-                MethodSpec.methodBuilder("createQuery").apply {
-                    addAnnotation(Override::class.java)
-                    addModifiers(PUBLIC)
-                    returns(ClassName.get("java.lang", "String"))
+    fun createAnonymous(typeWriter: TypeWriter, dbParam: String): XTypeSpec {
+        return XTypeSpec.anonymousClassBuilder(typeWriter.codeLanguage, "%L", dbParam).apply {
+            superclass(RoomTypeNames.DELETE_OR_UPDATE_ADAPTER.parametrizedBy(pojo.typeName))
+            addFunction(
+                XFunSpec.builder(
+                    language = language,
+                    name = "createQuery",
+                    visibility = VisibilityModifier.PUBLIC,
+                    isOverride = true
+                ).apply {
+                    returns(String::class.asClassName())
                     val pojoCols = pojo.columnNames.joinToString(",") {
                         "`$it` = ?"
                     }
@@ -81,29 +73,24 @@
                         append(" WHERE")
                         append(" $pkFieldsCols")
                     }
-                    addStatement("return $S", query)
+                    addStatement("return %S", query)
                 }.build()
             )
-            addMethod(
-                MethodSpec.methodBuilder("bind").apply {
-                    val bindScope = CodeGenScope(typeWriter)
-                    addAnnotation(Override::class.java)
-                    addModifiers(PUBLIC)
-                    returns(TypeName.VOID)
-                    val stmtParam = "stmt"
-                    addParameter(
-                        ParameterSpec.builder(
-                            SQLITE_STMT.toJavaPoet(),
-                            stmtParam
-                        ).build()
-                    )
-                    val valueParam = "value"
-                    addParameter(
-                        ParameterSpec.builder(pojo.typeName.toJavaPoet(), valueParam).build()
-                    )
+            addFunction(
+                XFunSpec.builder(
+                    language = language,
+                    name = "bind",
+                    visibility = VisibilityModifier.PUBLIC,
+                    isOverride = true
+                ).apply {
+                    val stmtParam = "statement"
+                    addParameter(SupportDbTypeNames.SQLITE_STMT, stmtParam)
+                    val entityParam = "entity"
+                    addParameter(pojo.typeName, entityParam)
                     val mappedField = FieldWithIndex.byOrder(pojo.fields)
+                    val bindScope = CodeGenScope(typeWriter)
                     FieldReadWriteWriter.bindToStatement(
-                        ownerVar = valueParam,
+                        ownerVar = entityParam,
                         stmtParamVar = stmtParam,
                         fieldsWithIndices = mappedField,
                         scope = bindScope
@@ -117,12 +104,12 @@
                         )
                     }
                     FieldReadWriteWriter.bindToStatement(
-                        ownerVar = valueParam,
+                        ownerVar = entityParam,
                         stmtParamVar = stmtParam,
                         fieldsWithIndices = mappedPrimaryKeys,
                         scope = bindScope
                     )
-                    addCode(bindScope.builder().build())
+                    addCode(bindScope.generate())
                 }.build()
             )
         }.build()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpsertionAdapterWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpsertionAdapterWriter.kt
index 35e65c0..7098ed2 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpsertionAdapterWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpsertionAdapterWriter.kt
@@ -16,17 +16,12 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XPropertySpec
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.RoomTypeNames.UPSERTION_ADAPTER
-import androidx.room.ext.T
+import androidx.room.ext.RoomTypeNames
 import androidx.room.vo.Pojo
 import androidx.room.vo.ShortcutEntity
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterizedTypeName
 
-// TODO b/240736981 need to implement the writer's test
 class EntityUpsertionAdapterWriter private constructor(
     val tableName: String,
     val pojo: Pojo
@@ -44,16 +39,17 @@
         entity: ShortcutEntity,
         typeWriter: TypeWriter,
         dbProperty: XPropertySpec
-    ): CodeBlock {
-        val upsertionAdapter = ParameterizedTypeName.get(
-            UPSERTION_ADAPTER.toJavaPoet(), pojo.typeName.toJavaPoet()
-        )
+    ): XCodeBlock {
+        val upsertionAdapter = RoomTypeNames.UPSERTION_ADAPTER.parametrizedBy(pojo.typeName)
         val insertionHelper = EntityInsertionAdapterWriter.create(entity, "")
-            .createAnonymous(typeWriter, dbProperty).toJavaPoet()
+            .createAnonymous(typeWriter, dbProperty)
         val updateHelper = EntityUpdateAdapterWriter.create(entity, "")
             .createAnonymous(typeWriter, dbProperty.name)
-        return CodeBlock.builder().add("new $T($L, $L)",
-            upsertionAdapter, insertionHelper, updateHelper
-        ).build()
+        return XCodeBlock.ofNewInstance(
+            language = typeWriter.codeLanguage,
+            typeName = upsertionAdapter,
+            argsFormat = "%L, %L",
+            args = arrayOf(insertionHelper, updateHelper)
+        )
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index 3d43d33..84a98ce 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -37,7 +37,6 @@
 import androidx.room.ext.RoomPagingTypeNames
 import androidx.room.ext.RoomRxJava2TypeNames
 import androidx.room.ext.RoomRxJava3TypeNames
-import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.RxJava2TypeNames
 import androidx.room.ext.RxJava3TypeNames
 import androidx.room.processor.DatabaseViewProcessor
@@ -305,10 +304,7 @@
     }
 
     val ROOM_DATABASE_KTX by lazy {
-        loadJavaCode(
-            "common/input/RoomDatabaseKt.java",
-            RoomTypeNames.ROOM_DB_KT.toString()
-        )
+        loadKotlinCode("common/input/RoomDatabaseExt.kt")
     }
 }
 
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
index 8be0ffb..49fa7f1 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
@@ -755,6 +755,205 @@
         )
     }
 
+    @Test
+    fun transactionMethodAdapter_interface(
+        @TestParameter("DISABLE", "ALL_COMPATIBILITY", "ALL_INCOMPATIBLE")
+        jvmDefaultMode: JvmDefaultMode
+    ) {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+            import androidx.sqlite.db.SupportSQLiteQuery
+
+            interface BaseDao {
+                @Transaction
+                open fun baseConcrete() {
+                }
+
+                @Transaction
+                open suspend fun baseSuspendConcrete() {
+                }
+            }
+
+            @Dao
+            interface MyDao : BaseDao {
+              @Transaction
+              fun concrete() {
+              }
+
+              @Transaction
+              fun concreteWithReturn(): String {
+                TODO("")
+              }
+
+              @Transaction
+              fun concreteWithParamsAndReturn(text: String, num: Long): String {
+                TODO("")
+              }
+
+              @Transaction
+              fun concreteWithFunctionalParam(block: () -> Unit) {
+              }
+
+              @Transaction
+              suspend fun suspendConcrete() {
+
+              }
+
+              @Transaction
+              suspend fun suspendConcreteWithReturn(): String {
+                TODO("")
+              }
+
+              @Transaction
+              suspend fun suspendConcreteWithSuspendFunctionalParam(block: suspend () -> Unit) {
+              }
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc, COMMON.COROUTINES_ROOM, COMMON.ROOM_DATABASE_KTX),
+            expectedFilePath = getTestGoldenPath(testName),
+            jvmDefaultMode = jvmDefaultMode
+        )
+    }
+
+    @Test
+    fun transactionMethodAdapter_abstractClass() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+            import androidx.sqlite.db.SupportSQLiteQuery
+
+            interface BaseDao {
+                @Transaction
+                open fun baseConcrete() {
+                }
+
+                @Transaction
+                open suspend fun baseSuspendConcrete() {
+                }
+            }
+
+            @Dao
+            abstract class MyDao : BaseDao {
+              @Transaction
+              open fun concrete() {
+              }
+
+              @Transaction
+              open fun concreteInternal() {
+              }
+
+              @Transaction
+              open suspend fun suspendConcrete() {
+
+              }
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc, COMMON.COROUTINES_ROOM, COMMON.ROOM_DATABASE_KTX),
+            expectedFilePath = getTestGoldenPath(testName),
+        )
+    }
+
+    @Test
+    fun deleteOrUpdateMethodAdapter() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Update
+              fun updateEntity(item: MyEntity)
+
+              @Delete
+              fun deleteEntity(item: MyEntity)
+
+              @Update
+              fun updateEntityAndReturnCount(item: MyEntity): Int
+
+              @Delete
+              fun deleteEntityAndReturnCount(item: MyEntity): Int
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Long,
+                val data: String,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun insertOrUpsertMethodAdapter() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Insert
+              fun insertEntity(item: MyEntity)
+
+              @Upsert
+              fun upsertEntity(item: MyEntity)
+
+              @Insert
+              fun insertEntityAndReturnRowId(item: MyEntity): Long
+
+              @Upsert
+              fun upsertEntityAndReturnRowId(item: MyEntity): Long
+
+              @Insert
+              fun insertEntityListAndReturnRowIds(items: List<MyEntity>): List<Long>
+
+              @Upsert
+              fun upsertEntityListAndReturnRowIds(items: List<MyEntity>): List<Long>
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Long,
+                val data: String,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
     private fun getTestGoldenPath(testName: String): String {
         return "kotlinCodeGen/$testName.kt"
     }
diff --git a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseExt.kt b/room/room-compiler/src/test/test-data/common/input/RoomDatabaseExt.kt
new file mode 100644
index 0000000..f3036c5
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/common/input/RoomDatabaseExt.kt
@@ -0,0 +1,10 @@
+@file:JvmName("RoomDatabaseKt")
+
+package androidx.room
+
+import androidx.room.RoomDatabase
+
+@Suppress("UNUSED_PARAMETER")
+public suspend fun <R> RoomDatabase.withTransaction(block: suspend () -> R): R {
+    TODO("")
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
index 7b158a9..fe908dc 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
@@ -39,7 +39,8 @@
     public boolean transactionMethod(final int i, final String s, final long l) {
         __db.beginTransaction();
         try {
-            boolean _result = ComplexDao_Impl.super.transactionMethod(i, s, l);
+            final boolean _result;
+            _result = ComplexDao_Impl.super.transactionMethod(i, s, l);
             __db.setTransactionSuccessful();
             return _result;
         } finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/DeletionDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/DeletionDao.java
index 6f39c4d..f8423b9 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/DeletionDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/DeletionDao.java
@@ -41,44 +41,48 @@
     this.__db = __db;
     this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "DELETE FROM `User` WHERE `uid` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, User value) {
-        stmt.bindLong(1, value.uid);
+      public void bind(@NonNull final SupportSQLiteStatement statement, final User entity) {
+        statement.bindLong(1, entity.uid);
       }
     };
     this.__deletionAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "DELETE FROM `MultiPKeyEntity` WHERE `name` = ? AND `lastName` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
-        if (value.name == null) {
-          stmt.bindNull(1);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              final MultiPKeyEntity entity) {
+        if (entity.name == null) {
+          statement.bindNull(1);
         } else {
-          stmt.bindString(1, value.name);
+          statement.bindString(1, entity.name);
         }
-        if (value.lastName == null) {
-          stmt.bindNull(2);
+        if (entity.lastName == null) {
+          statement.bindNull(2);
         } else {
-          stmt.bindString(2, value.lastName);
+          statement.bindString(2, entity.lastName);
         }
       }
     };
     this.__deletionAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "DELETE FROM `Book` WHERE `bookId` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, Book value) {
-        stmt.bindLong(1, value.bookId);
+      public void bind(@NonNull final SupportSQLiteStatement statement, final Book entity) {
+        statement.bindLong(1, entity.bookId);
       }
     };
     this.__preparedStmtOfDeleteByUid = new SharedSQLiteStatement(__db) {
@@ -142,7 +146,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfUser.handle(user);
+      _total += __deletionAdapterOfUser.handle(user);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -156,7 +160,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfUser.handle(user);
+      _total += __deletionAdapterOfUser.handle(user);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -170,8 +174,8 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfUser.handle(user1);
-      _total +=__deletionAdapterOfUser.handleMultiple(others);
+      _total += __deletionAdapterOfUser.handle(user1);
+      _total += __deletionAdapterOfUser.handleMultiple(others);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -185,7 +189,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfUser.handleMultiple(users);
+      _total += __deletionAdapterOfUser.handleMultiple(users);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -218,7 +222,7 @@
         int _total = 0;
         __db.beginTransaction();
         try {
-          _total +=__deletionAdapterOfUser.handle(user);
+          _total += __deletionAdapterOfUser.handle(user);
           __db.setTransactionSuccessful();
           return _total;
         } finally {
@@ -236,7 +240,7 @@
         int _total = 0;
         __db.beginTransaction();
         try {
-          _total +=__deletionAdapterOfUser.handle(user);
+          _total += __deletionAdapterOfUser.handle(user);
           __db.setTransactionSuccessful();
           return _total;
         } finally {
@@ -252,7 +256,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfMultiPKeyEntity.handle(entity);
+      _total += __deletionAdapterOfMultiPKeyEntity.handle(entity);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpdateDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpdateDao.java
index 0a7c4ef..daa15e6 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpdateDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpdateDao.java
@@ -41,91 +41,96 @@
     this.__db = __db;
     this.__updateAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "UPDATE OR ABORT `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, User value) {
-        stmt.bindLong(1, value.uid);
-        if (value.name == null) {
-          stmt.bindNull(2);
+      public void bind(@NonNull final SupportSQLiteStatement statement, final User entity) {
+        statement.bindLong(1, entity.uid);
+        if (entity.name == null) {
+          statement.bindNull(2);
         } else {
-          stmt.bindString(2, value.name);
+          statement.bindString(2, entity.name);
         }
-        if (value.getLastName() == null) {
-          stmt.bindNull(3);
+        if (entity.getLastName() == null) {
+          statement.bindNull(3);
         } else {
-          stmt.bindString(3, value.getLastName());
+          statement.bindString(3, entity.getLastName());
         }
-        stmt.bindLong(4, value.age);
-        stmt.bindLong(5, value.uid);
+        statement.bindLong(4, entity.age);
+        statement.bindLong(5, entity.uid);
       }
     };
     this.__updateAdapterOfUser_1 = new EntityDeletionOrUpdateAdapter<User>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "UPDATE `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, User value) {
-        stmt.bindLong(1, value.uid);
-        if (value.name == null) {
-          stmt.bindNull(2);
+      public void bind(@NonNull final SupportSQLiteStatement statement, final User entity) {
+        statement.bindLong(1, entity.uid);
+        if (entity.name == null) {
+          statement.bindNull(2);
         } else {
-          stmt.bindString(2, value.name);
+          statement.bindString(2, entity.name);
         }
-        if (value.getLastName() == null) {
-          stmt.bindNull(3);
+        if (entity.getLastName() == null) {
+          statement.bindNull(3);
         } else {
-          stmt.bindString(3, value.getLastName());
+          statement.bindString(3, entity.getLastName());
         }
-        stmt.bindLong(4, value.age);
-        stmt.bindLong(5, value.uid);
+        statement.bindLong(4, entity.age);
+        statement.bindLong(5, entity.uid);
       }
     };
     this.__updateAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "UPDATE OR ABORT `MultiPKeyEntity` SET `name` = ?,`lastName` = ? WHERE `name` = ? AND `lastName` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
-        if (value.name == null) {
-          stmt.bindNull(1);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              final MultiPKeyEntity entity) {
+        if (entity.name == null) {
+          statement.bindNull(1);
         } else {
-          stmt.bindString(1, value.name);
+          statement.bindString(1, entity.name);
         }
-        if (value.lastName == null) {
-          stmt.bindNull(2);
+        if (entity.lastName == null) {
+          statement.bindNull(2);
         } else {
-          stmt.bindString(2, value.lastName);
+          statement.bindString(2, entity.lastName);
         }
-        if (value.name == null) {
-          stmt.bindNull(3);
+        if (entity.name == null) {
+          statement.bindNull(3);
         } else {
-          stmt.bindString(3, value.name);
+          statement.bindString(3, entity.name);
         }
-        if (value.lastName == null) {
-          stmt.bindNull(4);
+        if (entity.lastName == null) {
+          statement.bindNull(4);
         } else {
-          stmt.bindString(4, value.lastName);
+          statement.bindString(4, entity.lastName);
         }
       }
     };
     this.__updateAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "UPDATE OR ABORT `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, Book value) {
-        stmt.bindLong(1, value.bookId);
-        stmt.bindLong(2, value.uid);
-        stmt.bindLong(3, value.bookId);
+      public void bind(@NonNull final SupportSQLiteStatement statement, final Book entity) {
+        statement.bindLong(1, entity.bookId);
+        statement.bindLong(2, entity.uid);
+        statement.bindLong(3, entity.bookId);
       }
     };
     this.__preparedStmtOfAgeUserByUid = new SharedSQLiteStatement(__db) {
@@ -202,7 +207,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfUser.handle(user);
+      _total += __updateAdapterOfUser.handle(user);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -216,8 +221,8 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfUser.handle(user1);
-      _total +=__updateAdapterOfUser.handleMultiple(others);
+      _total += __updateAdapterOfUser.handle(user1);
+      _total += __updateAdapterOfUser.handleMultiple(others);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -231,7 +236,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfUser.handleMultiple(users);
+      _total += __updateAdapterOfUser.handleMultiple(users);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -245,7 +250,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfUser.handle(user);
+      _total += __updateAdapterOfUser.handle(user);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -278,7 +283,7 @@
         int _total = 0;
         __db.beginTransaction();
         try {
-          _total +=__updateAdapterOfUser.handle(user);
+          _total += __updateAdapterOfUser.handle(user);
           __db.setTransactionSuccessful();
           return _total;
         } finally {
@@ -296,7 +301,7 @@
         int _total = 0;
         __db.beginTransaction();
         try {
-          _total +=__updateAdapterOfUser.handle(user);
+          _total += __updateAdapterOfUser.handle(user);
           __db.setTransactionSuccessful();
           return _total;
         } finally {
@@ -312,7 +317,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfMultiPKeyEntity.handle(entity);
+      _total += __updateAdapterOfMultiPKeyEntity.handle(entity);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpsertDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpsertDao.java
index d11d549..1e7255b 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpsertDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpsertDao.java
@@ -49,25 +49,26 @@
             }
         }, new EntityDeletionOrUpdateAdapter<User>(__db) {
             @Override
+            @NonNull
             public String createQuery() {
                 return "UPDATE `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
             }
 
             @Override
-            public void bind(SupportSQLiteStatement stmt, User value) {
-                stmt.bindLong(1, value.uid);
-                if (value.name == null) {
-                    stmt.bindNull(2);
+            public void bind(@NonNull final SupportSQLiteStatement statement, final User entity) {
+                statement.bindLong(1, entity.uid);
+                if (entity.name == null) {
+                    statement.bindNull(2);
                 } else {
-                    stmt.bindString(2, value.name);
+                    statement.bindString(2, entity.name);
                 }
-                if (value.getLastName() == null) {
-                    stmt.bindNull(3);
+                if (entity.getLastName() == null) {
+                    statement.bindNull(3);
                 } else {
-                    stmt.bindString(3, value.getLastName());
+                    statement.bindString(3, entity.getLastName());
                 }
-                stmt.bindLong(4, value.age);
-                stmt.bindLong(5, value.uid);
+                statement.bindLong(4, entity.age);
+                statement.bindLong(5, entity.uid);
             }
         });
         this.__upsertionAdapterOfBook = new EntityUpsertionAdapter<Book>(new EntityInsertionAdapter<Book>(__db) {
@@ -84,15 +85,16 @@
             }
         }, new EntityDeletionOrUpdateAdapter<Book>(__db) {
             @Override
+            @NonNull
             public String createQuery() {
                 return "UPDATE `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
             }
 
             @Override
-            public void bind(SupportSQLiteStatement stmt, Book value) {
-                stmt.bindLong(1, value.bookId);
-                stmt.bindLong(2, value.uid);
-                stmt.bindLong(3, value.bookId);
+            public void bind(@NonNull final SupportSQLiteStatement statement, final Book entity) {
+                statement.bindLong(1, entity.bookId);
+                statement.bindLong(2, entity.uid);
+                statement.bindLong(3, entity.bookId);
             }
         });
     }
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
index 8a7f450..875dfea 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
@@ -39,7 +39,8 @@
     public boolean transactionMethod(final int i, final String s, final long l) {
         __db.beginTransaction();
         try {
-            boolean _result = ComplexDao_Impl.super.transactionMethod(i, s, l);
+            final boolean _result;
+            _result = ComplexDao_Impl.super.transactionMethod(i, s, l);
             __db.setTransactionSuccessful();
             return _result;
         } finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/DeletionDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/DeletionDao.java
index 732aa17..cb765e9 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/DeletionDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/DeletionDao.java
@@ -41,36 +41,42 @@
     this.__db = __db;
     this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "DELETE FROM `User` WHERE `uid` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, User value) {
-        stmt.bindLong(1, value.uid);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              @NonNull final User entity) {
+        statement.bindLong(1, entity.uid);
       }
     };
     this.__deletionAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "DELETE FROM `MultiPKeyEntity` WHERE `name` = ? AND `lastName` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
-        stmt.bindString(1, value.name);
-        stmt.bindString(2, value.lastName);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              @NonNull final MultiPKeyEntity entity) {
+        statement.bindString(1, entity.name);
+        statement.bindString(2, entity.lastName);
       }
     };
     this.__deletionAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "DELETE FROM `Book` WHERE `bookId` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, Book value) {
-        stmt.bindLong(1, value.bookId);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              @NonNull final Book entity) {
+        statement.bindLong(1, entity.bookId);
       }
     };
     this.__preparedStmtOfDeleteByUid = new SharedSQLiteStatement(__db) {
@@ -134,7 +140,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfUser.handle(user);
+      _total += __deletionAdapterOfUser.handle(user);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -148,7 +154,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfUser.handle(user);
+      _total += __deletionAdapterOfUser.handle(user);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -162,8 +168,8 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfUser.handle(user1);
-      _total +=__deletionAdapterOfUser.handleMultiple(others);
+      _total += __deletionAdapterOfUser.handle(user1);
+      _total += __deletionAdapterOfUser.handleMultiple(others);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -177,7 +183,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfUser.handleMultiple(users);
+      _total += __deletionAdapterOfUser.handleMultiple(users);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -210,7 +216,7 @@
         int _total = 0;
         __db.beginTransaction();
         try {
-          _total +=__deletionAdapterOfUser.handle(user);
+          _total += __deletionAdapterOfUser.handle(user);
           __db.setTransactionSuccessful();
           return _total;
         } finally {
@@ -228,7 +234,7 @@
         int _total = 0;
         __db.beginTransaction();
         try {
-          _total +=__deletionAdapterOfUser.handle(user);
+          _total += __deletionAdapterOfUser.handle(user);
           __db.setTransactionSuccessful();
           return _total;
         } finally {
@@ -244,7 +250,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__deletionAdapterOfMultiPKeyEntity.handle(entity);
+      _total += __deletionAdapterOfMultiPKeyEntity.handle(entity);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpdateDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpdateDao.java
index e168f94..bfa4f3b 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpdateDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpdateDao.java
@@ -41,59 +41,67 @@
     this.__db = __db;
     this.__updateAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "UPDATE OR ABORT `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, User value) {
-        stmt.bindLong(1, value.uid);
-        stmt.bindString(2, value.name);
-        stmt.bindString(3, value.getLastName());
-        stmt.bindLong(4, value.age);
-        stmt.bindLong(5, value.uid);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              @NonNull final User entity) {
+        statement.bindLong(1, entity.uid);
+        statement.bindString(2, entity.name);
+        statement.bindString(3, entity.getLastName());
+        statement.bindLong(4, entity.age);
+        statement.bindLong(5, entity.uid);
       }
     };
     this.__updateAdapterOfUser_1 = new EntityDeletionOrUpdateAdapter<User>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "UPDATE `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, User value) {
-        stmt.bindLong(1, value.uid);
-        stmt.bindString(2, value.name);
-        stmt.bindString(3, value.getLastName());
-        stmt.bindLong(4, value.age);
-        stmt.bindLong(5, value.uid);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              @NonNull final User entity) {
+        statement.bindLong(1, entity.uid);
+        statement.bindString(2, entity.name);
+        statement.bindString(3, entity.getLastName());
+        statement.bindLong(4, entity.age);
+        statement.bindLong(5, entity.uid);
       }
     };
     this.__updateAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "UPDATE OR ABORT `MultiPKeyEntity` SET `name` = ?,`lastName` = ? WHERE `name` = ? AND `lastName` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
-        stmt.bindString(1, value.name);
-        stmt.bindString(2, value.lastName);
-        stmt.bindString(3, value.name);
-        stmt.bindString(4, value.lastName);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              @NonNull final MultiPKeyEntity entity) {
+        statement.bindString(1, entity.name);
+        statement.bindString(2, entity.lastName);
+        statement.bindString(3, entity.name);
+        statement.bindString(4, entity.lastName);
       }
     };
     this.__updateAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
       @Override
+      @NonNull
       public String createQuery() {
         return "UPDATE OR ABORT `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
       }
 
       @Override
-      public void bind(SupportSQLiteStatement stmt, Book value) {
-        stmt.bindLong(1, value.bookId);
-        stmt.bindLong(2, value.uid);
-        stmt.bindLong(3, value.bookId);
+      public void bind(@NonNull final SupportSQLiteStatement statement,
+              @NonNull final Book entity) {
+        statement.bindLong(1, entity.bookId);
+        statement.bindLong(2, entity.uid);
+        statement.bindLong(3, entity.bookId);
       }
     };
     this.__preparedStmtOfAgeUserByUid = new SharedSQLiteStatement(__db) {
@@ -170,7 +178,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfUser.handle(user);
+      _total += __updateAdapterOfUser.handle(user);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -184,8 +192,8 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfUser.handle(user1);
-      _total +=__updateAdapterOfUser.handleMultiple(others);
+      _total += __updateAdapterOfUser.handle(user1);
+      _total += __updateAdapterOfUser.handleMultiple(others);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -199,7 +207,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfUser.handleMultiple(users);
+      _total += __updateAdapterOfUser.handleMultiple(users);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -213,7 +221,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfUser.handle(user);
+      _total += __updateAdapterOfUser.handle(user);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
@@ -246,7 +254,7 @@
         int _total = 0;
         __db.beginTransaction();
         try {
-          _total +=__updateAdapterOfUser.handle(user);
+          _total += __updateAdapterOfUser.handle(user);
           __db.setTransactionSuccessful();
           return _total;
         } finally {
@@ -264,7 +272,7 @@
         int _total = 0;
         __db.beginTransaction();
         try {
-          _total +=__updateAdapterOfUser.handle(user);
+          _total += __updateAdapterOfUser.handle(user);
           __db.setTransactionSuccessful();
           return _total;
         } finally {
@@ -280,7 +288,7 @@
     int _total = 0;
     __db.beginTransaction();
     try {
-      _total +=__updateAdapterOfMultiPKeyEntity.handle(entity);
+      _total += __updateAdapterOfMultiPKeyEntity.handle(entity);
       __db.setTransactionSuccessful();
       return _total;
     } finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpsertDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpsertDao.java
index b3d7f2a..8a1374c 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpsertDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpsertDao.java
@@ -42,17 +42,19 @@
             }
         }, new EntityDeletionOrUpdateAdapter<User>(__db) {
             @Override
+            @NonNull
             public String createQuery() {
                 return "UPDATE `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
             }
 
             @Override
-            public void bind(SupportSQLiteStatement stmt, User value) {
-                stmt.bindLong(1, value.uid);
-                stmt.bindString(2, value.name);
-                stmt.bindString(3, value.getLastName());
-                stmt.bindLong(4, value.age);
-                stmt.bindLong(5, value.uid);
+            public void bind(@NonNull final SupportSQLiteStatement statement,
+                    @NonNull final User entity) {
+                statement.bindLong(1, entity.uid);
+                statement.bindString(2, entity.name);
+                statement.bindString(3, entity.getLastName());
+                statement.bindLong(4, entity.age);
+                statement.bindLong(5, entity.uid);
             }
         });
         this.__upsertionAdapterOfBook = new EntityUpsertionAdapter<Book>(new EntityInsertionAdapter<Book>(__db) {
@@ -70,15 +72,17 @@
             }
         }, new EntityDeletionOrUpdateAdapter<Book>(__db) {
             @Override
+            @NonNull
             public String createQuery() {
                 return "UPDATE `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
             }
 
             @Override
-            public void bind(SupportSQLiteStatement stmt, Book value) {
-                stmt.bindLong(1, value.bookId);
-                stmt.bindLong(2, value.uid);
-                stmt.bindLong(3, value.bookId);
+            public void bind(@NonNull final SupportSQLiteStatement statement,
+                    @NonNull final Book entity) {
+                statement.bindLong(1, entity.bookId);
+                statement.bindLong(2, entity.uid);
+                statement.bindLong(3, entity.bookId);
             }
         });
     }
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
new file mode 100644
index 0000000..da2116d4
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
@@ -0,0 +1,95 @@
+import androidx.room.EntityDeletionOrUpdateAdapter
+import androidx.room.RoomDatabase
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+    private val __db: RoomDatabase
+
+    private val __deletionAdapterOfMyEntity: EntityDeletionOrUpdateAdapter<MyEntity>
+
+    private val __updateAdapterOfMyEntity: EntityDeletionOrUpdateAdapter<MyEntity>
+
+    public constructor(__db: RoomDatabase) {
+        this.__db = __db
+        this.__deletionAdapterOfMyEntity = object : EntityDeletionOrUpdateAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String = "DELETE FROM `MyEntity` WHERE `pk` = ?"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk)
+            }
+        }
+        this.__updateAdapterOfMyEntity = object : EntityDeletionOrUpdateAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "UPDATE OR ABORT `MyEntity` SET `pk` = ?,`data` = ? WHERE `pk` = ?"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk)
+                statement.bindString(2, entity.data)
+                statement.bindLong(3, entity.pk)
+            }
+        }
+    }
+
+    public override fun deleteEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __deletionAdapterOfMyEntity.handle(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun deleteEntityAndReturnCount(item: MyEntity): Int {
+        __db.assertNotSuspendingTransaction()
+        var _total: Int = 0
+        __db.beginTransaction()
+        try {
+            _total += __deletionAdapterOfMyEntity.handle(item)
+            __db.setTransactionSuccessful()
+            return _total
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun updateEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __updateAdapterOfMyEntity.handle(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun updateEntityAndReturnCount(item: MyEntity): Int {
+        __db.assertNotSuspendingTransaction()
+        var _total: Int = 0
+        __db.beginTransaction()
+        try {
+            _total += __updateAdapterOfMyEntity.handle(item)
+            __db.setTransactionSuccessful()
+            return _total
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
new file mode 100644
index 0000000..7e60516
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
@@ -0,0 +1,130 @@
+import androidx.room.EntityDeletionOrUpdateAdapter
+import androidx.room.EntityInsertionAdapter
+import androidx.room.EntityUpsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+
+    private val __upsertionAdapterOfMyEntity: EntityUpsertionAdapter<MyEntity>
+
+    public constructor(__db: RoomDatabase) {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`data`) VALUES (?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk)
+                statement.bindString(2, entity.data)
+            }
+        }
+        this.__upsertionAdapterOfMyEntity = EntityUpsertionAdapter<MyEntity>(object :
+            EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT INTO `MyEntity` (`pk`,`data`) VALUES (?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk)
+                statement.bindString(2, entity.data)
+            }
+        }, object : EntityDeletionOrUpdateAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "UPDATE `MyEntity` SET `pk` = ?,`data` = ? WHERE `pk` = ?"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk)
+                statement.bindString(2, entity.data)
+                statement.bindLong(3, entity.pk)
+            }
+        })
+    }
+
+    public override fun insertEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun insertEntityAndReturnRowId(item: MyEntity): Long {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            val _result: Long = __insertionAdapterOfMyEntity.insertAndReturnId(item)
+            __db.setTransactionSuccessful()
+            return _result
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun insertEntityListAndReturnRowIds(items: List<MyEntity>): List<Long> {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            val _result: List<Long> = __insertionAdapterOfMyEntity.insertAndReturnIdsList(items)
+            __db.setTransactionSuccessful()
+            return _result
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun upsertEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __upsertionAdapterOfMyEntity.upsert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun upsertEntityAndReturnRowId(item: MyEntity): Long {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            val _result: Long = __upsertionAdapterOfMyEntity.upsertAndReturnId(item)
+            __db.setTransactionSuccessful()
+            return _result
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun upsertEntityListAndReturnRowIds(items: List<MyEntity>): List<Long> {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            val _result: List<Long> = __upsertionAdapterOfMyEntity.upsertAndReturnIdsList(items)
+            __db.setTransactionSuccessful()
+            return _result
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
new file mode 100644
index 0000000..3658d37
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
@@ -0,0 +1,65 @@
+import androidx.room.RoomDatabase
+import androidx.room.withTransaction
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+    private val __db: RoomDatabase
+
+    public constructor(__db: RoomDatabase) {
+        this.__db = __db
+    }
+
+    public override fun baseConcrete(): Unit {
+        __db.beginTransaction()
+        try {
+            super@MyDao_Impl.baseConcrete()
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override suspend fun baseSuspendConcrete(): Unit {
+        __db.withTransaction {
+            super@MyDao_Impl.baseSuspendConcrete()
+        }
+    }
+
+    public override fun concrete(): Unit {
+        __db.beginTransaction()
+        try {
+            super@MyDao_Impl.concrete()
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun concreteInternal(): Unit {
+        __db.beginTransaction()
+        try {
+            super@MyDao_Impl.concreteInternal()
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override suspend fun suspendConcrete(): Unit {
+        __db.withTransaction {
+            super@MyDao_Impl.suspendConcrete()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
new file mode 100644
index 0000000..15e154e
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
@@ -0,0 +1,104 @@
+import androidx.room.RoomDatabase
+import androidx.room.withTransaction
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Function0
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.coroutines.SuspendFunction0
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+    private val __db: RoomDatabase
+
+    public constructor(__db: RoomDatabase) {
+        this.__db = __db
+    }
+
+    public override fun baseConcrete(): Unit {
+        __db.beginTransaction()
+        try {
+            super@MyDao_Impl.baseConcrete()
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override suspend fun baseSuspendConcrete(): Unit {
+        __db.withTransaction {
+            super@MyDao_Impl.baseSuspendConcrete()
+        }
+    }
+
+    public override fun concrete(): Unit {
+        __db.beginTransaction()
+        try {
+            super@MyDao_Impl.concrete()
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun concreteWithReturn(): String {
+        __db.beginTransaction()
+        try {
+            val _result: String
+            _result = super@MyDao_Impl.concreteWithReturn()
+            __db.setTransactionSuccessful()
+            return _result
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun concreteWithParamsAndReturn(text: String, num: Long): String {
+        __db.beginTransaction()
+        try {
+            val _result: String
+            _result = super@MyDao_Impl.concreteWithParamsAndReturn(text, num)
+            __db.setTransactionSuccessful()
+            return _result
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun concreteWithFunctionalParam(block: Function0<Unit>): Unit {
+        __db.beginTransaction()
+        try {
+            super@MyDao_Impl.concreteWithFunctionalParam(block)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override suspend fun suspendConcrete(): Unit {
+        __db.withTransaction {
+            super@MyDao_Impl.suspendConcrete()
+        }
+    }
+
+    public override suspend fun suspendConcreteWithReturn(): String = __db.withTransaction {
+        super@MyDao_Impl.suspendConcreteWithReturn()
+    }
+
+    public override suspend
+    fun suspendConcreteWithSuspendFunctionalParam(block: SuspendFunction0<Unit>): Unit {
+        __db.withTransaction {
+            super@MyDao_Impl.suspendConcreteWithSuspendFunctionalParam(block)
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/security/security-crypto-ktx/build.gradle b/security/security-crypto-ktx/build.gradle
index e676a24..d25904c 100644
--- a/security/security-crypto-ktx/build.gradle
+++ b/security/security-crypto-ktx/build.gradle
@@ -23,7 +23,7 @@
 }
 
 dependencies {
-    api("androidx.security:security-crypto:1.1.0-alpha03")
+    api(project(":security:security-crypto"))
 
     api(libs.kotlinStdlib)
     androidTestImplementation(libs.junit)
diff --git a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
index b21f7e7..9027376 100644
--- a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
+++ b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
@@ -47,15 +47,37 @@
 
 /**
  * Class used to create and read encrypted files.
+ * <br />
+ * <br />
+ * <b>WARNING</b>: The encrypted file should not be backed up with Auto Backup. When restoring the
+ * file it is likely the key used to encrypt it will no longer be present. You should exclude all
+ * <code>EncryptedFile</code>s from backup using
+ * <a href="https://developer.android.com/guide/topics/data/autobackup#IncludingFiles">backup rules</a>.
+ * Be aware that if you are not explicitly calling <code>setKeysetPrefName()</code> there is also a
+ * silently-created default preferences file created at
+ * <pre>
+ *     ApplicationProvider
+ *          .getApplicationContext()
+ *          .getFilesDir()
+ *          .getParent() + "/shared_prefs/__androidx_security_crypto_encrypted_file_pref__"
+ * </pre>
+ *
+ * This preferences file (or any others created with a custom specified location) also should be
+ * excluded from backups.
+ * <br />
+ * <br />
+ * Basic use of the class:
  *
  * <pre>
- *  String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
+ *  MasterKey masterKey = new MasterKey.Builder(context)
+ *      .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ *      .build();
  *
  *  File file = new File(context.getFilesDir(), "secret_data");
  *  EncryptedFile encryptedFile = EncryptedFile.Builder(
- *      file,
  *      context,
- *      masterKeyAlias,
+ *      file,
+ *      masterKey,
  *      EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
  *  ).build();
  *
diff --git a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
index ad66feb..2060e40 100644
--- a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
+++ b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
@@ -51,14 +51,25 @@
 
 /**
  * An implementation of {@link SharedPreferences} that encrypts keys and values.
+ * <br />
+ * <br />
+ * <b>WARNING</b>: The preference file should not be backed up with Auto Backup. When restoring the
+ * file it is likely the key used to encrypt it will no longer be present. You should exclude all
+ * <code>EncryptedSharedPreference</code>s from backup using
+ * <a href="https://developer.android.com/guide/topics/data/autobackup#IncludingFiles">backup rules</a>.
+ * <br />
+ * <br />
+ * Basic use of the class:
  *
  * <pre>
- *  String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
+ *  MasterKey masterKey = new MasterKey.Builder(context)
+ *      .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ *      .build();
  *
  *  SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
- *      "secret_shared_prefs",
- *      masterKeyAlias,
  *      context,
+ *      "secret_shared_prefs",
+ *      masterKey,
  *      EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
  *      EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
  *  );
diff --git a/settings.gradle b/settings.gradle
index 3696086..bddb08f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -789,6 +789,9 @@
 includeProject(":privacysandbox:tools:tools-apipackager", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools-core", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools-testing", [BuildType.MAIN])
+includeProject(":privacysandbox:ui:ui-client", [BuildType.MAIN])
+includeProject(":privacysandbox:ui:ui-core", [BuildType.MAIN])
+includeProject(":privacysandbox:ui:ui-provider", [BuildType.MAIN])
 includeProject(":profileinstaller:profileinstaller", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":profileinstaller:integration-tests:profile-verification", [BuildType.MAIN])
 includeProject(":profileinstaller:integration-tests:profile-verification-sample", [BuildType.MAIN])
diff --git a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
index c807e23..f419abd 100644
--- a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
+++ b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
@@ -79,4 +79,33 @@
         val cursor2 = db.query("select * from user where idk=1")
         assertThat(cursor2.count).isEqualTo(0)
     }
+
+    @Test
+    fun testFrameWorkSQLiteDatabase_attachDbWorks() {
+        val openHelper2 = FrameworkSQLiteOpenHelper(
+            context,
+            "test2.db",
+            OpenHelperRecoveryTest.EmptyCallback(),
+            useNoBackupDirectory = false,
+            allowDataLossOnRecovery = false
+        )
+        val db1 = openHelper.writableDatabase
+        val db2 = openHelper2.writableDatabase
+
+        db1.execSQL(
+            "ATTACH DATABASE '${db2.path}' AS database2"
+        )
+
+        val cursor = db1.query("pragma database_list")
+        val expected = buildList<Pair<String, String>> {
+            while (cursor.moveToNext()) {
+                add(cursor.getString(1) to cursor.getString(2))
+            }
+        }
+        cursor.close()
+        openHelper2.close()
+
+        val actual = db1.attachedDbs?.map { it.first to it.second }
+        assertThat(expected).isEqualTo(actual)
+    }
 }
\ No newline at end of file
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
index 00257a1..b9aeb34 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
@@ -290,7 +290,8 @@
     override val isWriteAheadLoggingEnabled: Boolean
         get() = SupportSQLiteCompat.Api16Impl.isWriteAheadLoggingEnabled(delegate)
 
-    override val attachedDbs: List<Pair<String, String>>? = delegate.attachedDbs
+    override val attachedDbs: List<Pair<String, String>>?
+        get() = delegate.attachedDbs
 
     override val isDatabaseIntegrityOk: Boolean
         get() = delegate.isDatabaseIntegrityOk
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
index 1648409..95c305a 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
@@ -16,6 +16,7 @@
 
 package androidx.test.uiautomator.testapp;
 
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import android.app.Activity;
@@ -27,10 +28,13 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.Configurator;
 import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObjectNotFoundException;
 import androidx.test.uiautomator.Until;
 
 import org.junit.Before;
+import org.junit.function.ThrowingRunnable;
 import org.junit.runner.RunWith;
 
 @RunWith(AndroidJUnit4.class)
@@ -62,4 +66,17 @@
         assertTrue("Test app not visible after launching activity",
                 mDevice.wait(Until.hasObject(By.pkg(TEST_APP)), TIMEOUT_MS));
     }
+
+    // Helper to verify that an operation throws a UiObjectNotFoundException without waiting for
+    // the full 10s default timeout.
+    protected static void assertUiObjectNotFound(ThrowingRunnable runnable) {
+        Configurator configurator = Configurator.getInstance();
+        long timeout = configurator.getWaitForSelectorTimeout();
+        configurator.setWaitForSelectorTimeout(1_000);
+        try {
+            assertThrows(UiObjectNotFoundException.class, runnable);
+        } finally {
+            configurator.setWaitForSelectorTimeout(timeout);
+        }
+    }
 }
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
index ec2bad0..5459765 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
@@ -355,36 +355,56 @@
     @Test
     public void testSetOrientationLeft() throws Exception {
         launchTestActivity(KeycodeTestActivity.class);
-
-        assertTrue(mDevice.isNaturalOrientation());
-        assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
-
-        mDevice.setOrientationLeft();
-        // Make the device wait for 1 sec for the rotation animation to finish.
-        SystemClock.sleep(1_000);
-        assertFalse(mDevice.isNaturalOrientation());
-        assertEquals(UiAutomation.ROTATION_FREEZE_90, mDevice.getDisplayRotation());
-
-        mDevice.setOrientationNatural();
-        SystemClock.sleep(1_000);
-        assertTrue(mDevice.isNaturalOrientation());
+        try {
+            assertTrue(mDevice.isNaturalOrientation());
+            assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
+            mDevice.setOrientationLeft();
+            // Make the device wait for 1 sec for the rotation animation to finish.
+            SystemClock.sleep(1_000);
+            assertFalse(mDevice.isNaturalOrientation());
+            assertEquals(UiAutomation.ROTATION_FREEZE_90, mDevice.getDisplayRotation());
+            mDevice.setOrientationNatural();
+            SystemClock.sleep(1_000);
+            assertTrue(mDevice.isNaturalOrientation());
+        } finally {
+            mDevice.unfreezeRotation();
+        }
     }
 
     @Test
     public void testSetOrientationRight() throws Exception {
         launchTestActivity(KeycodeTestActivity.class);
+        try {
+            assertTrue(mDevice.isNaturalOrientation());
+            assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
+            mDevice.setOrientationRight();
+            SystemClock.sleep(1_000);
+            assertFalse(mDevice.isNaturalOrientation());
+            assertEquals(UiAutomation.ROTATION_FREEZE_270, mDevice.getDisplayRotation());
+            mDevice.setOrientationNatural();
+            SystemClock.sleep(1_000);
+            assertTrue(mDevice.isNaturalOrientation());
+        } finally {
+            mDevice.unfreezeRotation();
+        }
+    }
 
-        assertTrue(mDevice.isNaturalOrientation());
-        assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
+    @Test
+    public void testIsScreenOn() throws Exception {
+        launchTestActivity(MainActivity.class);
 
-        mDevice.setOrientationRight();
-        SystemClock.sleep(1_000);
-        assertFalse(mDevice.isNaturalOrientation());
-        assertEquals(UiAutomation.ROTATION_FREEZE_270, mDevice.getDisplayRotation());
+        mDevice.wakeUp();
+        assertTrue(mDevice.isScreenOn());
 
-        mDevice.setOrientationNatural();
-        SystemClock.sleep(1_000);
-        assertTrue(mDevice.isNaturalOrientation());
+        try {
+            mDevice.sleep();
+            assertFalse(mDevice.isScreenOn());
+        } finally {
+            mDevice.wakeUp();
+            mDevice.pressMenu();
+            assertTrue("Failed to wake up device and remove lockscreen",
+                    mDevice.hasObject(By.pkg(TEST_APP)));
+        }
     }
 
     @Test
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index 96d56df..64af3dd 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -203,9 +203,9 @@
         // Get the same textView object via different methods.
         UiObject2 textView1 = mDevice.findObject(By.res(TEST_APP, "example_id"));
         UiObject2 textView2 = mDevice.findObject(By.text("TextView with an id"));
-        assertTrue(textView1.equals(textView2));
+        assertEquals(textView1, textView2);
         UiObject2 linearLayout = mDevice.findObject(By.res(TEST_APP, "nested_elements"));
-        assertFalse(textView1.equals(linearLayout));
+        assertNotEquals(textView1, linearLayout);
     }
 
     @Test
@@ -598,22 +598,15 @@
         launchTestActivity(VerticalScrollTestActivity.class);
         assertTrue(mDevice.hasObject(By.res(TEST_APP, "top_text"))); // Initially at top.
 
-        // Scroll down to bottom (20000px) in increments of 5000px.
+        // Scroll down to bottom where is two-screen-height distant from the top.
         UiObject2 scrollView = mDevice.findObject(By.res(TEST_APP, "scroll_view"));
         scrollView.setGestureMargin(SCROLL_MARGIN); // Avoid touching too close to the edges.
+
         Rect bounds = scrollView.getVisibleBounds();
-        float percent = 5_000f / (bounds.height() - 2 * SCROLL_MARGIN);
-        // Scroll to an element 5000px from the top.
+        float percent =
+                (float) (mDevice.getDisplayHeight() * 2 / (bounds.height() - 2 * SCROLL_MARGIN));
         scrollView.scroll(Direction.DOWN, percent);
-        assertTrue(mDevice.hasObject(By.res(TEST_APP, "from_top_5000")));
-        // Scroll to an element 10000px from the top.
-        scrollView.scroll(Direction.DOWN, percent);
-        assertTrue(mDevice.hasObject(By.res(TEST_APP, "from_top_10000")));
-        // Scroll to an element 15000px from the top.
-        scrollView.scroll(Direction.DOWN, percent);
-        assertTrue(mDevice.hasObject(By.res(TEST_APP, "from_top_15000")));
-        // Scroll to the bottom element 20000px from the top.
-        scrollView.scroll(Direction.DOWN, percent);
+
         assertTrue(mDevice.hasObject(By.res(TEST_APP, "bottom_text")));
     }
 
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
index de621a2..ee12f2d 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
@@ -26,7 +26,6 @@
 
 import androidx.test.filters.SdkSuppress;
 import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
 import androidx.test.uiautomator.UiSelector;
 
 import org.junit.Test;
@@ -75,7 +74,7 @@
 
         UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
 
-        assertThrows(UiObjectNotFoundException.class, noNode::getChildCount);
+        assertUiObjectNotFound(noNode::getChildCount);
     }
 
     @Test
@@ -297,13 +296,13 @@
 
         UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
 
-        assertThrows(UiObjectNotFoundException.class, noNode::click);
-        assertThrows(UiObjectNotFoundException.class, noNode::clickAndWaitForNewWindow);
-        assertThrows(UiObjectNotFoundException.class, noNode::clickTopLeft);
-        assertThrows(UiObjectNotFoundException.class, noNode::longClickBottomRight);
-        assertThrows(UiObjectNotFoundException.class, noNode::clickBottomRight);
-        assertThrows(UiObjectNotFoundException.class, noNode::longClick);
-        assertThrows(UiObjectNotFoundException.class, noNode::longClickTopLeft);
+        assertUiObjectNotFound(noNode::click);
+        assertUiObjectNotFound(noNode::clickAndWaitForNewWindow);
+        assertUiObjectNotFound(noNode::clickTopLeft);
+        assertUiObjectNotFound(noNode::longClickBottomRight);
+        assertUiObjectNotFound(noNode::clickBottomRight);
+        assertUiObjectNotFound(noNode::longClick);
+        assertUiObjectNotFound(noNode::longClickTopLeft);
     }
 
     @Test
@@ -383,13 +382,13 @@
 
         UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
 
-        assertThrows(UiObjectNotFoundException.class, noNode::getText);
-        assertThrows(UiObjectNotFoundException.class, noNode::getClassName);
-        assertThrows(UiObjectNotFoundException.class, noNode::getContentDescription);
-        assertThrows(UiObjectNotFoundException.class, () -> noNode.legacySetText("new_text"));
-        assertThrows(UiObjectNotFoundException.class, () -> noNode.setText("new_text"));
-        assertThrows(UiObjectNotFoundException.class, noNode::clearTextField);
-        assertThrows(UiObjectNotFoundException.class, noNode::getPackageName);
+        assertUiObjectNotFound(noNode::getText);
+        assertUiObjectNotFound(noNode::getClassName);
+        assertUiObjectNotFound(noNode::getContentDescription);
+        assertUiObjectNotFound(() -> noNode.legacySetText("new_text"));
+        assertUiObjectNotFound(() -> noNode.setText("new_text"));
+        assertUiObjectNotFound(noNode::clearTextField);
+        assertUiObjectNotFound(noNode::getPackageName);
     }
 
     @Test
@@ -513,15 +512,15 @@
 
         UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
 
-        assertThrows(UiObjectNotFoundException.class, noNode::isChecked);
-        assertThrows(UiObjectNotFoundException.class, noNode::isSelected);
-        assertThrows(UiObjectNotFoundException.class, noNode::isCheckable);
-        assertThrows(UiObjectNotFoundException.class, noNode::isEnabled);
-        assertThrows(UiObjectNotFoundException.class, noNode::isClickable);
-        assertThrows(UiObjectNotFoundException.class, noNode::isFocused);
-        assertThrows(UiObjectNotFoundException.class, noNode::isFocusable);
-        assertThrows(UiObjectNotFoundException.class, noNode::isScrollable);
-        assertThrows(UiObjectNotFoundException.class, noNode::isLongClickable);
+        assertUiObjectNotFound(noNode::isChecked);
+        assertUiObjectNotFound(noNode::isSelected);
+        assertUiObjectNotFound(noNode::isCheckable);
+        assertUiObjectNotFound(noNode::isEnabled);
+        assertUiObjectNotFound(noNode::isClickable);
+        assertUiObjectNotFound(noNode::isFocused);
+        assertUiObjectNotFound(noNode::isFocusable);
+        assertUiObjectNotFound(noNode::isScrollable);
+        assertUiObjectNotFound(noNode::isLongClickable);
     }
 
     @Test
@@ -671,8 +670,8 @@
         UiObject smallArea = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/small_area"));
 
-        assertThrows(UiObjectNotFoundException.class, () -> noNode.pinchOut(100, 10));
-        assertThrows(UiObjectNotFoundException.class, () -> noNode.pinchIn(100, 10));
+        assertUiObjectNotFound(() -> noNode.pinchOut(100, 10));
+        assertUiObjectNotFound(() -> noNode.pinchIn(100, 10));
         assertThrows(IllegalStateException.class, () -> smallArea.pinchOut(100, 10));
         assertThrows(IllegalStateException.class, () -> smallArea.pinchIn(100, 10));
     }
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
index b30e677..a5b7e7c 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
@@ -18,13 +18,11 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import android.widget.TextView;
 
 import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
 import androidx.test.uiautomator.UiScrollable;
 import androidx.test.uiautomator.UiSelector;
 
@@ -65,7 +63,7 @@
                         "This is the bottom");
 
         assertEquals("This is the bottom", target.getText());
-        assertThrows(UiObjectNotFoundException.class,
+        assertUiObjectNotFound(
                 () -> relativeLayout.getChildByDescription(
                         new UiSelector().className(TextView.class),
                         "This is non-existent"));
@@ -82,7 +80,7 @@
                         "This is the top", false);
 
         assertEquals("This is the top", target.getText());
-        assertThrows(UiObjectNotFoundException.class,
+        assertUiObjectNotFound(
                 () -> relativeLayout.getChildByDescription(
                         new UiSelector().className(TextView.class), "This is the bottom",
                         false));
@@ -115,7 +113,7 @@
                         "This is the bottom");
 
         assertEquals("This is the bottom", target.getText());
-        assertThrows(UiObjectNotFoundException.class,
+        assertUiObjectNotFound(
                 () -> relativeLayout.getChildByText(new UiSelector().className(TextView.class),
                         "This is non-existent"));
     }
@@ -131,7 +129,7 @@
                         "This is the top", false);
 
         assertEquals("This is the top", target.getText());
-        assertThrows(UiObjectNotFoundException.class,
+        assertUiObjectNotFound(
                 () -> relativeLayout.getChildByText(new UiSelector().className(TextView.class),
                         "This is the bottom", false));
     }
@@ -143,10 +141,10 @@
         UiScrollable relativeLayout = new UiScrollable(
                 new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
         UiObject target = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
-                + "/from_top_15000"));
+                + "/bottom_text"));
 
         assertFalse(target.exists());
-        assertTrue(relativeLayout.scrollDescriptionIntoView("This is 15000px from the top"));
+        assertTrue(relativeLayout.scrollDescriptionIntoView("This is the bottom"));
         assertTrue(target.exists());
         assertFalse(relativeLayout.scrollDescriptionIntoView("This is non-existent"));
     }
@@ -158,7 +156,7 @@
         UiScrollable relativeLayout = new UiScrollable(
                 new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
         UiObject target = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
-                + "/from_top_15000"));
+                + "/bottom_text"));
         UiObject nonExistentTarget = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/not_exist"));
 
@@ -174,7 +172,7 @@
 
         UiScrollable relativeLayout = new UiScrollable(
                 new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
-        UiSelector target = new UiSelector().resourceId(TEST_APP + ":id/from_top_15000");
+        UiSelector target = new UiSelector().resourceId(TEST_APP + ":id/bottom_text");
         UiSelector nonExistentTarget = new UiSelector().resourceId(TEST_APP + ":id/not_exist");
 
         assertFalse(mDevice.findObject(target).exists());
@@ -190,11 +188,11 @@
         UiScrollable relativeLayout = new UiScrollable(
                 new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
         UiObject target = mDevice.findObject(
-                new UiSelector().resourceId(TEST_APP + ":id/from_top_15000"));
+                new UiSelector().resourceId(TEST_APP + ":id/bottom_text"));
 
         assertTrue(relativeLayout.scrollIntoView(target));
         assertTrue(relativeLayout.ensureFullyVisible(target));
-        assertThrows(UiObjectNotFoundException.class,
+        assertUiObjectNotFound(
                 () -> relativeLayout.ensureFullyVisible(
                         mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"))));
     }
@@ -206,10 +204,10 @@
         UiScrollable relativeLayout = new UiScrollable(
                 new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
         UiObject target = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
-                + "/from_top_15000"));
+                + "/bottom_text"));
 
         assertFalse(target.exists());
-        assertTrue(relativeLayout.scrollTextIntoView("This is 15000px from the top"));
+        assertTrue(relativeLayout.scrollTextIntoView("This is the bottom"));
         assertTrue(target.exists());
         assertFalse(relativeLayout.scrollTextIntoView("This is non-existent"));
     }
@@ -234,7 +232,7 @@
         // Assert throwing exception for `scrollForward(int)` and all its related methods.
         UiScrollable noNode = new UiScrollable(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/no_node"));
-        assertThrows(UiObjectNotFoundException.class, noNode::flingForward);
+        assertUiObjectNotFound(noNode::flingForward);
     }
 
     @Test
@@ -271,7 +269,7 @@
 
         UiScrollable noNode = new UiScrollable(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/no_node"));
-        assertThrows(UiObjectNotFoundException.class, noNode::flingBackward);
+        assertUiObjectNotFound(noNode::flingBackward);
     }
 
     @Test
@@ -305,7 +303,7 @@
         UiObject topText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/top_text"));
 
-        assertTrue(relativeLayout.scrollTextIntoView("This is 15000px from the top"));
+        assertTrue(relativeLayout.scrollTextIntoView("This is the bottom"));
         assertFalse(topText.exists());
         assertTrue(relativeLayout.scrollToBeginning(20, 50));
         assertTrue(topText.exists());
@@ -320,7 +318,7 @@
         UiObject topText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/top_text"));
 
-        assertTrue(relativeLayout.scrollTextIntoView("This is 15000px from the top"));
+        assertTrue(relativeLayout.scrollTextIntoView("This is the bottom"));
         assertFalse(topText.exists());
         assertTrue(relativeLayout.scrollToBeginning(20));
         assertTrue(topText.exists());
@@ -354,7 +352,6 @@
     @Test
     public void testScrollToEnd() throws Exception {
         launchTestActivity(VerticalScrollTestActivity.class);
-        launchTestActivity(VerticalScrollTestActivity.class);
 
         UiScrollable relativeLayout = new UiScrollable(
                 new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/VerticalScrollTestActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/VerticalScrollTestActivity.java
index 8f39574..183f9cd 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/VerticalScrollTestActivity.java
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/VerticalScrollTestActivity.java
@@ -17,10 +17,14 @@
 package androidx.test.uiautomator.testapp;
 
 import android.app.Activity;
+import android.graphics.Point;
 import android.os.Bundle;
+import android.view.Display;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.MotionEvent;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
 
 import androidx.annotation.Nullable;
 
@@ -34,6 +38,15 @@
 
         setContentView(R.layout.vertical_scroll_test_activity);
 
+        // Get the size of the screen.
+        Display display = getWindowManager().getDefaultDisplay();
+        Point displaySize = new Point();
+        display.getSize(displaySize);
+
+        // Set up the scrolling layout whose height is two times of the screen height.
+        RelativeLayout layout = findViewById(R.id.relative_layout);
+        layout.setLayoutParams(new FrameLayout.LayoutParams(displaySize.x, displaySize.y * 2));
+
         mGestureDetector = new GestureDetector(this, new SimpleOnGestureListener() {
             @Override
             public boolean onDown(MotionEvent event) {
diff --git a/test/uiautomator/integration-tests/testapp/src/main/res/layout/vertical_scroll_test_activity.xml b/test/uiautomator/integration-tests/testapp/src/main/res/layout/vertical_scroll_test_activity.xml
index d7d3aee..f2482db 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/res/layout/vertical_scroll_test_activity.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/res/layout/vertical_scroll_test_activity.xml
@@ -28,7 +28,7 @@
         <RelativeLayout
             android:id="@+id/relative_layout"
             android:layout_width="fill_parent"
-            android:layout_height="20000px">
+            android:layout_height="wrap_content">
 
             <TextView
                 android:id="@+id/top_text"
@@ -39,33 +39,6 @@
                 android:text="This is the top" />
 
             <TextView
-                android:id="@+id/from_top_5000"
-                android:layout_width="wrap_content"
-                android:layout_height="50px"
-                android:layout_alignParentTop="true"
-                android:layout_marginTop="5000px"
-                android:contentDescription="This is 5000px from the top"
-                android:text="This is 5000px from the top" />
-
-            <TextView
-                android:id="@+id/from_top_10000"
-                android:layout_width="wrap_content"
-                android:layout_height="50px"
-                android:layout_alignParentTop="true"
-                android:layout_marginTop="10000px"
-                android:contentDescription="This is 10000px from the top"
-                android:text="This is 10000px from the top" />
-
-            <TextView
-                android:id="@+id/from_top_15000"
-                android:layout_width="wrap_content"
-                android:layout_height="50px"
-                android:layout_alignParentTop="true"
-                android:layout_marginTop="15000px"
-                android:contentDescription="This is 15000px from the top"
-                android:text="This is 15000px from the top" />
-
-            <TextView
                 android:id="@+id/bottom_text"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
diff --git a/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java b/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
index 0f9d364..fc46750 100644
--- a/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
+++ b/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
@@ -178,14 +178,6 @@
     }
 
     @Test
-    public void testWakeUpAndShutDownScreen() throws Exception {
-        mDevice.wakeUp();
-        assertTrue(mDevice.isScreenOn());
-        mDevice.sleep();
-        assertFalse(mDevice.isScreenOn());
-    }
-
-    @Test
     @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
     public void testGetUiAutomation_withoutFlags() {
         mDevice.getUiAutomation();
diff --git a/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
index edb0f02..ab49a11 100644
--- a/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
+++ b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
@@ -26,8 +26,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert.assertThat
 import org.junit.After
-import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -85,7 +85,7 @@
         val getReason: (String, String) -> String = { name, code ->
             "$name test language '$code' does not exist on test device"
         }
-        Assert.assertThat(
+        assertThat(
             getReason(
                 "Default",
                 LocaleTestUtils.DEFAULT_TEST_LANGUAGE
@@ -93,12 +93,12 @@
             availableLanguages,
             CoreMatchers.hasItem(LocaleTestUtils.DEFAULT_TEST_LANGUAGE)
         )
-        Assert.assertThat(
+        assertThat(
             getReason("LTR", LocaleTestUtils.LTR_LANGUAGE),
             availableLanguages,
             CoreMatchers.hasItem(LocaleTestUtils.LTR_LANGUAGE)
         )
-        Assert.assertThat(
+        assertThat(
             getReason("RTL", LocaleTestUtils.RTL_LANGUAGE),
             availableLanguages,
             CoreMatchers.hasItem(LocaleTestUtils.RTL_LANGUAGE)
@@ -110,13 +110,13 @@
     }
 
     private fun assertLocaleIs(lang: String, expectRtl: Boolean) {
-        Assert.assertThat(
+        assertThat(
             "Locale should be $lang",
             configuration.language,
             CoreMatchers.equalTo(lang)
         )
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-            Assert.assertThat(
+            assertThat(
                 "Layout direction should be ${if (expectRtl) "RTL" else "LTR"}",
                 configuration.layoutDirection,
                 CoreMatchers.equalTo(if (expectRtl) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR)
@@ -125,7 +125,7 @@
     }
 
     private fun determineDefaultLayoutDirection() {
-        Assert.assertThat(
+        assertThat(
             "Locale must still be the default when determining the default layout direction",
             configuration.language,
             CoreMatchers.equalTo(DEFAULT_LANGUAGE)
@@ -134,4 +134,4 @@
             expectRtlInDefaultLanguage = configuration.layoutDirection == LAYOUT_DIRECTION_RTL
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tracing/tracing-perfetto-binary/build.gradle b/tracing/tracing-perfetto-binary/build.gradle
index 11f4655..041c702 100644
--- a/tracing/tracing-perfetto-binary/build.gradle
+++ b/tracing/tracing-perfetto-binary/build.gradle
@@ -78,6 +78,7 @@
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
+    androidTestImplementation(project(":benchmark:benchmark-common")) // for supported ABI checks
 }
 
 androidx {
diff --git a/tracing/tracing-perfetto-binary/src/androidTest/java/androidx/tracing/perfetto/binary/test/TracingTest.kt b/tracing/tracing-perfetto-binary/src/androidTest/java/androidx/tracing/perfetto/binary/test/TracingTest.kt
index 520ff8d..4b4e2b2 100644
--- a/tracing/tracing-perfetto-binary/src/androidTest/java/androidx/tracing/perfetto/binary/test/TracingTest.kt
+++ b/tracing/tracing-perfetto-binary/src/androidTest/java/androidx/tracing/perfetto/binary/test/TracingTest.kt
@@ -18,14 +18,41 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import dalvik.system.BaseDexClassLoader
+import java.io.File
+import java.util.zip.ZipFile
+import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class TracingTest {
+    /**
+     * The test verifies that the library was assembled and can be found by the system.
+     * We cannot load the library since it contains explicit JNI method registration tied to
+     * the [androidx.tracing.perfetto.jni.PerfettoNative] class.
+     *
+     * Methods of the library are further tested in e.g.:
+     * - [androidx.tracing.perfetto.jni.test.PerfettoNativeTest]
+     * - [androidx.compose.integration.macrobenchmark.TrivialTracingBenchmark]
+     */
     @Test
-    fun test_loadLibrary() {
-        System.loadLibrary("tracing_perfetto")
+    fun test_library_was_created() {
+        // check that the system can resolve the library
+        val nativeLibraryName = System.mapLibraryName("tracing_perfetto")
+
+        // check that the class loader can find the library
+        val classLoader = javaClass.classLoader as BaseDexClassLoader
+        val libraryPath = classLoader.findLibrary("tracing_perfetto")
+        assertTrue(libraryPath.endsWith("/$nativeLibraryName"))
+
+        // check that the APK contains the library file
+        val context = InstrumentationRegistry.getInstrumentation().context
+        val baseApk = File(context.applicationInfo.publicSourceDir!!)
+        assertTrue(ZipFile(baseApk).entries().asSequence().any {
+            it.name.endsWith("/$nativeLibraryName")
+        })
     }
 }
diff --git a/tracing/tracing-perfetto-binary/src/main/cpp/jni/androidx_tracing_perfetto_jni_PerfettoNative.cc b/tracing/tracing-perfetto-binary/src/main/cpp/jni/androidx_tracing_perfetto_jni_PerfettoNative.cc
index b0c2953..faffb2f 100644
--- a/tracing/tracing-perfetto-binary/src/main/cpp/jni/androidx_tracing_perfetto_jni_PerfettoNative.cc
+++ b/tracing/tracing-perfetto-binary/src/main/cpp/jni/androidx_tracing_perfetto_jni_PerfettoNative.cc
@@ -20,13 +20,13 @@
 
 extern "C" {
 
-JNIEXPORT void JNICALL
+static void JNICALL
 Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeRegisterWithPerfetto(
         JNIEnv *env, __unused jclass clazz) {
     tracing_perfetto::RegisterWithPerfetto();
 }
 
-JNIEXPORT void JNICALL
+static void JNICALL
 Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventBegin(
         JNIEnv *env, __unused jclass clazz, jint key, jstring traceInfo) {
     const char *traceInfoUtf = env->GetStringUTFChars(traceInfo, NULL);
@@ -34,16 +34,68 @@
     env->ReleaseStringUTFChars(traceInfo, traceInfoUtf);
 }
 
-JNIEXPORT void JNICALL
-Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventEnd(
-        JNIEnv *env, __unused jclass clazz) {
+static void JNICALL
+Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventEnd() {
     tracing_perfetto::TraceEventEnd();
 }
 
-JNIEXPORT jstring JNICALL
+static jstring JNICALL
 Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeVersion(
         JNIEnv *env, __unused jclass clazz) {
     return env->NewStringUTF(tracing_perfetto::Version());
 }
 } // extern "C"
 
+// Explicitly registering native methods using CriticalNative / FastNative as per:
+// https://source.android.com/devices/tech/dalvik/improvements#faster-native-methods.
+// Note: this applies to Android 8 - 11. In Android 12+, this is recommended (to avoid slow lookups
+// on first use), but not necessary.
+
+static JNINativeMethod sMethods[] = {
+        {"nativeRegisterWithPerfetto",
+                "()V",
+                reinterpret_cast<void *>(
+                    Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeRegisterWithPerfetto)
+        },
+        {"nativeTraceEventBegin",
+                "(ILjava/lang/String;)V",
+                reinterpret_cast<void *>(
+                    Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventBegin)
+        },
+        {"nativeTraceEventEnd",
+                "()V",
+                reinterpret_cast<void *>(
+                    Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventEnd)
+        },
+        {"nativeVersion",
+                "()Ljava/lang/String;",
+                reinterpret_cast<void *>(
+                    Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeVersion)
+        },
+};
+
+jint JNI_OnLoad(JavaVM *vm, void * /* reserved */) {
+    JNIEnv *env = NULL;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)) {
+        PERFETTO_LOG("JNI_OnLoad failure when trying to register native methods for tracing.");
+        return JNI_ERR;
+    }
+    jclass clazz = env->FindClass("androidx/tracing/perfetto/jni/PerfettoNative");
+    if (clazz == NULL) {
+        PERFETTO_LOG("Cannot find PerfettoNative class when trying to register native methods for "
+                     "tracing.");
+        return JNI_ERR;
+    }
+
+    int result = env->RegisterNatives(clazz, sMethods, 4);
+    env->DeleteLocalRef(clazz);
+
+    if (result != 0) {
+        PERFETTO_LOG("Failure when trying to call RegisterNatives to register native methods for "
+                     "tracing.");
+        return JNI_ERR;
+    }
+
+    PERFETTO_LOG("Successfully registered native methods for tracing.");
+    return JNI_VERSION_1_6;
+}
\ No newline at end of file
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
index 419e348..00e6c5a 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
@@ -16,6 +16,8 @@
 package androidx.tracing.perfetto.jni
 
 import androidx.tracing.perfetto.security.SafeLibLoader
+import dalvik.annotation.optimization.CriticalNative
+import dalvik.annotation.optimization.FastNative
 import java.io.File
 
 internal object PerfettoNative {
@@ -25,10 +27,10 @@
     object Metadata {
         const val version = "1.0.0-alpha07"
         val checksums = mapOf(
-            "arm64-v8a" to "abb5fe78f243999cab6fc8ea05e6d0743df3582e7ea4782f5ef2a677d8a0bde7",
-            "armeabi-v7a" to "d40c8487f7bca0917dcd6c0626d179394ef4b28d6a2fed809bc34e8ea15a100e",
-            "x86" to "38e8e5872e2916b993ca5d37894d7973934730bc427af20de4a52f0656a9ce0f",
-            "x86_64" to "ef545e11ceb45bf0eb611d12867bf9b9c5c9361d7e1865f8723a7505d3107ab8",
+            "arm64-v8a" to "3d9fa02a04459e3480f28ae7f3a8ced30f181f02a0e6bc6caf85704faea84857",
+            "armeabi-v7a" to "9eb7bf76a94fce79e682204c9e702c872d15c85f2ebd48de61aa541e7e6de034",
+            "x86" to "6eb72683bf58eeb0175f094d88bc0e00744b1a593dc7eaac9ee15168b1ab9234",
+            "x86_64" to "eabf725464c24c9709d8e2ea073233e5712949bb65aac5c4972528d813b55c74",
         )
     }
 
@@ -36,8 +38,17 @@
 
     fun loadLib(file: File, loader: SafeLibLoader) = loader.loadLib(file, Metadata.checksums)
 
+    @JvmStatic
     external fun nativeRegisterWithPerfetto()
+
+    @FastNative
+    @JvmStatic
     external fun nativeTraceEventBegin(key: Int, traceInfo: String)
+
+    @CriticalNative
+    @JvmStatic
     external fun nativeTraceEventEnd()
+
+    @JvmStatic
     external fun nativeVersion(): String
 }
diff --git a/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/CriticalNative.java b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/CriticalNative.java
new file mode 100644
index 0000000..13fbfd2
--- /dev/null
+++ b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/CriticalNative.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dalvik.annotation.optimization;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Applied to native methods to enable an ART runtime built-in optimization:
+ * methods that are annotated this way can speed up JNI transitions for methods that contain no
+ * objects (in parameters or return values, or as an implicit {@code this}).
+ *
+ * <p>
+ * The native implementation must exclude the {@code JNIEnv} and {@code jclass} parameters from its
+ * function signature. As an additional limitation, the method must be explicitly registered with
+ * {@code RegisterNatives} instead of relying on the built-in dynamic JNI linking.
+ * </p>
+ *
+ * <p>
+ * Performance of JNI transitions:
+ * <ul>
+ * <li>Regular JNI cost in nanoseconds: 115
+ * <li>Fast {@code (!)} JNI cost in nanoseconds: 60
+ * <li>{@literal @}{@link FastNative} cost in nanoseconds: 35
+ * <li>{@literal @}{@code CriticalNative} cost in nanoseconds: 25
+ * </ul>
+ * (Measured on angler-userdebug in 07/2016).
+ * </p>
+ *
+ * <p>
+ * A similar annotation, {@literal @}{@link FastNative}, exists with similar performance guarantees.
+ * However, unlike {@code @CriticalNative} it supports non-statics, object return values, and object
+ * parameters. If a method absolutely must have access to a {@code jobject}, then use
+ * {@literal @}{@link FastNative} instead of this.
+ * </p>
+ *
+ * <p>
+ * This has the side-effect of disabling all garbage collections while executing a critical native
+ * method. Use with extreme caution. Any long-running methods must not be marked with
+ * {@code @CriticalNative} (including usually-fast but generally unbounded methods)!
+ * </p>
+ *
+ * <p>
+ * <b>Deadlock Warning:</b> As a rule of thumb, do not acquire any locks during a critical native
+ * call if they aren't also locally released [before returning to managed code].
+ * </p>
+ *
+ * <p>
+ * Say some code does:
+ *
+ * <code>
+ * critical_native_call_to_grab_a_lock();
+ * does_some_java_work();
+ * critical_native_call_to_release_a_lock();
+ * </code>
+ *
+ * <p>
+ * This code can lead to deadlocks. Say thread 1 just finishes
+ * {@code critical_native_call_to_grab_a_lock()} and is in {@code does_some_java_work()}.
+ * GC kicks in and suspends thread 1. Thread 2 now is in
+ * {@code critical_native_call_to_grab_a_lock()} but is blocked on grabbing the
+ * native lock since it's held by thread 1. Now thread suspension can't finish
+ * since thread 2 can't be suspended since it's doing CriticalNative JNI.
+ * </p>
+ *
+ * <p>
+ * Normal natives don't have the issue since once it's executing in native code,
+ * it is considered suspended from the runtime's point of view.
+ * CriticalNative natives however don't do the state transition done by the normal natives.
+ * </p>
+ *
+ * <p>
+ * This annotation has no effect when used with non-native methods.
+ * The runtime must throw a {@code VerifierError} upon class loading if this is used with a native
+ * method that contains object parameters, an object return value, or a non-static.
+ * </p>
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@Retention(RetentionPolicy.CLASS)  // Save memory, don't instantiate as an object at runtime.
+@Target(ElementType.METHOD)
+public @interface CriticalNative {}
+
diff --git a/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/FastNative.java b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/FastNative.java
new file mode 100644
index 0000000..3eb3745
--- /dev/null
+++ b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/FastNative.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dalvik.annotation.optimization;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An ART runtime built-in optimization for "native" methods to speed up JNI transitions.
+ *
+ * <p>
+ * This has the side-effect of disabling all garbage collections while executing a fast native
+ * method. Use with extreme caution. Any long-running methods must not be marked with
+ * {@code @FastNative} (including usually-fast but generally unbounded methods)!</p>
+ *
+ * <p><b>Deadlock Warning:</b>As a rule of thumb, do not acquire any locks during a fast native
+ * call if they aren't also locally released [before returning to managed code].</p>
+ *
+ * <p>
+ * Say some code does:
+ *
+ * <code>
+ * fast_jni_call_to_grab_a_lock();
+ * does_some_java_work();
+ * fast_jni_call_to_release_a_lock();
+ * </code>
+ *
+ * <p>
+ * This code can lead to deadlocks. Say thread 1 just finishes
+ * {@code fast_jni_call_to_grab_a_lock()} and is in {@code does_some_java_work()}.
+ * GC kicks in and suspends thread 1. Thread 2 now is in {@code fast_jni_call_to_grab_a_lock()}
+ * but is blocked on grabbing the native lock since it's held by thread 1.
+ * Now thread suspension can't finish since thread 2 can't be suspended since it's doing
+ * FastNative JNI.
+ * </p>
+ *
+ * <p>
+ * Normal JNI doesn't have the issue since once it's in native code,
+ * it is considered suspended from java's point of view.
+ * FastNative JNI however doesn't do the state transition done by JNI.
+ * </p>
+ *
+ * <p>
+ * Note that even in FastNative methods you <b>are</b> allowed to
+ * allocate objects and make upcalls into Java code. A call from Java to
+ * a FastNative function and back to Java is equivalent to a call from one Java
+ * method to another. What's forbidden in a FastNative method is blocking
+ * the calling thread in some non-Java code and thereby preventing the thread
+ * from responding to requests from the garbage collector to enter the suspended
+ * state.
+ * </p>
+ *
+ * <p>
+ * Has no effect when used with non-native methods.
+ * </p>
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@Retention(RetentionPolicy.CLASS)  // Save memory, don't instantiate as an object at runtime.
+@Target(ElementType.METHOD)
+public @interface FastNative {}
+
diff --git a/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/README.md b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/README.md
new file mode 100644
index 0000000..e604041
--- /dev/null
+++ b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/README.md
@@ -0,0 +1,9 @@
+# Rationale
+
+`FastNative` and `CriticalNative` annotations are duplicated from the platform, so that the platform
+detects their presence, and applies them, even though they're not part of platform public API.
+
+# Next steps
+
+Once the annotations become public in the platform, remove duplicate files and use the platform.
+Tracking item: b/35664282
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
index 796e46e..870a8dc 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.Modifier
@@ -500,6 +501,7 @@
         assertThat(state.selectedOption).isNotEqualTo(initialOption)
     }
 
+    @Test
     fun scrolls_from_non_canonical_option_works() {
         lateinit var scope: CoroutineScope
         val pickerDriver = PickerDriver(separationSign = 1)
@@ -534,6 +536,27 @@
         pickerDriver.verifyCenterItemIsCentered()
     }
 
+    @Test
+    fun rememberPickerState_updates_after_new_inputs() {
+        val numberOfOptions = 10
+        lateinit var selectedOption: MutableState<Int>
+        rule.setContent {
+            selectedOption = remember { mutableStateOf(1) }
+            val pickerState = rememberPickerState(
+                initialNumberOfOptions = numberOfOptions,
+                initiallySelectedOption = selectedOption.value
+            )
+            Text(text = "${pickerState.selectedOption}")
+        }
+
+        // Update selected option to a new value - should also update the PickerState instance,
+        // and then recompose with the new value in the Text element.
+        selectedOption.value = 2
+        rule.waitForIdle()
+
+        rule.onNodeWithText("2").assertExists()
+    }
+
     private fun scroll_snaps(
         separationSign: Int = 0,
         touchInput: (TouchInjectionScope).() -> Unit,
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PlaceholderTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PlaceholderTest.kt
index 8284aad..158cf60 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PlaceholderTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PlaceholderTest.kt
@@ -21,15 +21,19 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
 import org.junit.Rule
 import org.junit.Test
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.max
 
 class PlaceholderTest {
     @get:Rule
@@ -237,17 +241,11 @@
                 expectedShimmerColor
             )
 
-        // Move the start of the next placeholder animation loop and them advance the clock 200
-        // milliseconds (PLACEHOLDER_PROGRESSION_DURATION_MS / 4) to show the shimmer.
-        //
-        // We choose (PLACEHOLDER_PROGRESSION_DURATION_MS / 4) as this should put the center of the
-        // shimmer gradiant at the top left (0,0) of the screen and as we have placed the component
-        // at the top of the screen this should ensure we have some shimmer gradiant cast over the
-        // component regardless of the screen size/shape. So should work for round, square or
-        // rectangular screens.
+        // Move the start of the next placeholder animation loop and them advance the clock 267
+        // milliseconds (PLACEHOLDER_PROGRESSION_DURATION_MS / 3) to show the shimmer.
         placeholderState.moveToStartOfNextAnimationLoop()
         placeholderState.advanceFrameMillisAndCheckState(
-            PLACEHOLDER_PROGRESSION_DURATION_MS / 4,
+            PLACEHOLDER_PROGRESSION_DURATION_MS / 3,
             PlaceholderStage.ShowPlaceholder
         )
 
@@ -286,6 +284,90 @@
     @RequiresApi(Build.VERSION_CODES.O)
     @OptIn(ExperimentalWearMaterialApi::class)
     @Test
+    fun wipeoff_takes_background_offset_into_account() {
+        var contentReady = false
+        lateinit var placeholderState: PlaceholderState
+        var expectedBackgroundColor = Color.Transparent
+        var expectedBackgroundPlaceholderColor: Color = Color.Transparent
+        rule.setContentWithTheme {
+            placeholderState = rememberPlaceholderState {
+                contentReady
+            }
+            val maxScreenDimensionPx = with(LocalDensity.current) {
+                Dp(max(screenHeightDp(), screenWidthDp()).toFloat()).toPx()
+            }
+            // Set the offset to be 50% of the screen
+            placeholderState.backgroundOffset =
+                Offset(maxScreenDimensionPx / 2f, maxScreenDimensionPx / 2f)
+            expectedBackgroundColor = MaterialTheme.colors.primary
+            expectedBackgroundPlaceholderColor = MaterialTheme.colors.surface
+
+            Chip(
+                modifier = Modifier
+                    .testTag("test-item")
+                    .fillMaxWidth(),
+                content = {},
+                >
+                colors = PlaceholderDefaults.placeholderChipColors(
+                    originalChipColors = ChipDefaults.primaryChipColors(),
+                    placeholderState = placeholderState,
+                ),
+                border = ChipDefaults.chipBorder()
+            )
+        }
+
+        placeholderState.initializeTestFrameMillis()
+
+        // Check the background color is correct
+        rule.onNodeWithTag("test-item")
+            .captureToImage()
+            .assertContainsColor(expectedBackgroundPlaceholderColor, 80f)
+        // Check that there is primary color showing
+        rule.onNodeWithTag("test-item")
+            .captureToImage()
+            .assertDoesNotContainColor(
+                expectedBackgroundColor
+            )
+
+        // Prepare to start to wipe off and show contents.
+        contentReady = true
+
+        placeholderState
+            .advanceToNextPlaceholderAnimationLoopAndCheckStage(PlaceholderStage.WipeOff)
+
+        // Check that placeholder background is still visible
+        rule.onNodeWithTag("test-item")
+            .captureToImage()
+            .assertContainsColor(expectedBackgroundPlaceholderColor, 80f)
+
+        // Move forward by 25% of the wipe-off and confirm that no wipe-off has happened yet due
+        // to our offset
+        placeholderState.advanceFrameMillisAndCheckState(
+            PLACEHOLDER_WIPE_OFF_PROGRESSION_DURATION_MS / 4,
+            PlaceholderStage.WipeOff
+        )
+
+        // Check that placeholder background is still visible
+        rule.onNodeWithTag("test-item")
+            .captureToImage()
+            .assertContainsColor(expectedBackgroundPlaceholderColor, 80f)
+
+        // Now move the end of the wipe-off and confirm that the proper chip background is visible
+        placeholderState.advanceFrameMillisAndCheckState(
+            PLACEHOLDER_PROGRESSION_DURATION_MS -
+                (PLACEHOLDER_PROGRESSION_DURATION_MS / 4),
+            PlaceholderStage.WipeOff
+        )
+
+        // Check that normal chip background is now visible
+        rule.onNodeWithTag("test-item")
+            .captureToImage()
+            .assertContainsColor(expectedBackgroundColor, 80f)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @OptIn(ExperimentalWearMaterialApi::class)
+    @Test
     fun placeholder_background_is_correct_color() {
         var expectedPlaceholderBackgroundColor = Color.Transparent
         var expectedBackgroundColor = Color.Transparent
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
index de17093a..41d50142 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
@@ -1152,7 +1152,7 @@
     public fun outlinedChipBorder(
         borderColor: Color = MaterialTheme.colors.primaryVariant.copy(alpha = 0.6f),
         disabledBorderColor: Color = borderColor.copy(alpha = ContentAlpha.disabled),
-        borderWidth: Dp = 2.dp
+        borderWidth: Dp = 1.dp
     ): ChipBorder {
         return DefaultChipBorder(
             borderStroke = BorderStroke(borderWidth, borderColor),
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
index 622ebf0..f2d2d66 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
@@ -324,9 +324,14 @@
     initialNumberOfOptions: Int,
     initiallySelectedOption: Int = 0,
     repeatItems: Boolean = true
-): PickerState = rememberSaveable(saver = PickerState.Saver) {
-    PickerState(initialNumberOfOptions, initiallySelectedOption, repeatItems)
-}
+): PickerState = rememberSaveable(
+        initialNumberOfOptions,
+        initiallySelectedOption,
+        repeatItems,
+        saver = PickerState.Saver
+    ) {
+        PickerState(initialNumberOfOptions, initiallySelectedOption, repeatItems)
+    }
 
 /**
  * A state object that can be hoisted to observe item selection.
@@ -428,7 +433,6 @@
                 )
             },
             restore = { saved ->
-                @Suppress("UNCHECKED_CAST")
                 PickerState(
                     initialNumberOfOptions = saved[0] as Int,
                     initiallySelectedOption = saved[1] as Int,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Placeholder.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Placeholder.kt
index 5312727..b697c4c 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Placeholder.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Placeholder.kt
@@ -15,6 +15,8 @@
  */
 package androidx.wear.compose.material
 
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.Easing
 import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
@@ -58,6 +60,23 @@
  * that needs to be displayed in a component is not yet available, e.g. it is loading
  * asynchronously.
  *
+ * A [PlaceholderState] should be created for each component that has placeholder data. The
+ * state is used to coordinate all of the different placeholder effects and animations.
+ *
+ * Placeholder has a number of different effects designed to work together.
+ * [Modifier.placeholder] draws a placeholder shape on top of content that is waiting to load. There
+ * can be multiple placeholders in a component.
+ * [Modifier.placeholderShimmer] does a shimmer animation over the whole component that includes the
+ * placeholders. There should only be one placeholderShimmer for each component.
+ *
+ * Background placeholder effects are used to mask the background of components like chips and cards
+ * until all of the data has loaded. Use [PlaceholderDefaults.placeholderChipColors]
+ * [PlaceholderDefaults.placeholderBackgroundBrush] and
+ * [PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush] to draw the component
+ * background.
+ *
+ * Once all of the components content is loaded the shimmer will stop and a wipe off animation will
+ * remove the placeholders.
  */
 @ExperimentalWearMaterialApi
 @Stable
@@ -65,6 +84,21 @@
     private val isContentReady: () -> Boolean,
     private val maxScreenDimension: Float,
 ) {
+
+    /**
+     * The offset to apply for any background placeholder animations. This is the global offset of
+     * the component which is having its background painted with
+     * [PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush],
+     * [PlaceholderDefaults.placeholderBackgroundBrush] or
+     * [PlaceholderDefaults.placeholderChipColors].
+     *
+     * The offset values should be retrieved with [OnGloballyPositionedModifier].
+     *
+     * The offset is used to coordinate placeholder effects such as wipe-off between the difference
+     * placeholder layers.
+     */
+    internal var backgroundOffset: Offset = Offset.Zero
+
     /**
      * Start the animation of the placeholder state.
      */
@@ -79,13 +113,39 @@
     }
 
     /**
-     * The current value of the placeholder visual effect gradient progression. The progression
-     * is a 45 degree angle sweep across the whole screen running from outside of the Top|Left of
-     * the screen to Bottom|Right used as the anchor for shimmer and wipe-off gradient effects.
+     * The current value of the placeholder wipe-off visual effect gradient progression. The
+     * progression is a 45 degree angle sweep across the whole screen running from outside of the
+     * Top|Left of the screen to Bottom|Right used as the anchor for wipe-off gradient effects.
      *
      * The progression represents the x and y coordinates in pixels of the Top|Left part of the
-     * gradient that flows across the screen. The progression will start at -gradientWidth and
-     * progress to the maximum screen dimension (max of height/width to create a 45 degree angle).
+     * gradient that flows across the screen. The progression will start at -maxScreenDimension (max
+     * of height/width to create a 45 degree angle) * 1.5f and progress to the
+     * maximumScreenDimension * 1.5f.
+     *
+     * The time taken for this progression to reach the edge of visible screen is
+     * [PLACEHOLDER_WIPE_OFF_PROGRESSION_DURATION_MS]
+     */
+    internal val placeholderWipeOffProgression: Float by derivedStateOf {
+        val absoluteProgression =
+            (frameMillis.value.mod(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS).coerceAtMost(
+                PLACEHOLDER_PROGRESSION_DURATION_MS).toFloat() /
+                PLACEHOLDER_WIPE_OFF_PROGRESSION_DURATION_MS).coerceAtMost(1f)
+        val progression =
+            lerp(-maxScreenDimension * 1.5f, maxScreenDimension * 1.5f, absoluteProgression)
+        progression
+    }
+
+    /**
+     * The current value of the placeholder visual effect gradient progression. The
+     * progression is a 45 degree angle sweep across the whole screen running from outside of the
+     * Top|Left of the screen to Bottom|Right used as the anchor for wipe-off gradient effects.
+     *
+     * The progression represents the x and y coordinates in pixels of the Top|Left part of the
+     * gradient that flows across the screen. The progression will start at -maxScreenDimension (max
+     * of height/width to create a 45 degree angle) * 1.5f and progress to the
+     * maximumScreenDimension * 1.5f.
+     *
+     * The time taken for this progression is [PLACEHOLDER_PROGRESSION_DURATION_MS]
      */
     @ExperimentalWearMaterialApi
     public val placeholderProgression: Float by derivedStateOf {
@@ -93,8 +153,9 @@
             (frameMillis.value.mod(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS).coerceAtMost(
                 PLACEHOLDER_PROGRESSION_DURATION_MS).toFloat() /
                 PLACEHOLDER_PROGRESSION_DURATION_MS)
-        val progression = lerp(-maxScreenDimension, maxScreenDimension, absoluteProgression)
-        progression
+        val progression =
+            lerp(-maxScreenDimension * 1.5f, maxScreenDimension * 1.5f, absoluteProgression)
+        progressionInterpolator.transform(progression)
     }
 
     /**
@@ -148,14 +209,34 @@
     internal val frameMillis = mutableStateOf(0L)
 
     private var startOfNextPlaceholderAnimation = 0L
+
+    private val progressionInterpolator: Easing = CubicBezierEasing(0.3f, 0f, 0.5f, 1f)
 }
 
 /**
  * Creates a [PlaceholderState] that is remembered across compositions. To start placeholder
  * animations run [PlaceholderState.startPlaceholderAnimation].
  *
- * @param isContentReady a lambda to determine whether all of the data/content has been loaded
- * and is ready to be displayed.
+ *  A [PlaceholderState] should be created for each component that has placeholder data. The
+ * state is used to coordinate all of the different placeholder effects and animations.
+ *
+ * Placeholder has a number of different effects designed to work together.
+ * [Modifier.placeholder] draws a placeholder shape on top of content that is waiting to load. There
+ * can be multiple placeholders in a component.
+ * [Modifier.placeholderShimmer] does a shimmer animation over the whole component that includes the
+ * placeholders. There should only be one placeholderShimmer for each component.
+ *
+ * Background placeholder effects are used to mask the background of components like chips and cards
+ * until all of the data has loaded. Use [PlaceholderDefaults.placeholderChipColors]
+ * [PlaceholderDefaults.placeholderBackgroundBrush] and
+ * [PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush] to draw the component
+ * background.
+ *
+ * Once all of the components content is loaded, [isContentReady] is `true` the shimmer will stop
+ * and a wipe off animation will remove the placeholders to reveal the content.
+ *
+ * @param isContentReady a lambda to determine whether all of the data/content has been loaded for a
+ * given component and is ready to be displayed.
  */
 @ExperimentalWearMaterialApi
 @Composable
@@ -416,7 +497,7 @@
         return PlaceholderBackgroundPainter(
             painter = null,
             placeholderState = placeholderState,
-            color = color
+            color = color,
         )
     }
 }
@@ -470,12 +551,14 @@
             color
         ),
         start = Offset(
-            x = placeholderState.placeholderProgression - offset.x,
-            y = placeholderState.placeholderProgression - offset.y
+            x = placeholderState.placeholderWipeOffProgression - offset.x,
+            y = placeholderState.placeholderWipeOffProgression - offset.y
         ),
         end = Offset(
-            x = placeholderState.placeholderProgression - offset.x + placeholderState.gradientWidth,
-            y = placeholderState.placeholderProgression - offset.y + placeholderState.gradientWidth
+            x = placeholderState.placeholderWipeOffProgression -
+                offset.x + placeholderState.gradientWidth,
+            y = placeholderState.placeholderWipeOffProgression -
+                offset.y + placeholderState.gradientWidth
         )
     )
 }
@@ -492,10 +575,6 @@
     private var alpha: Float = 1.0f
 ) : Painter() {
     override fun DrawScope.onDraw() {
-        val offset = Offset(
-            this.center.x - (this.size.width / 2f),
-            this.center.y - (this.size.height / 2f)
-        )
         // Due to anti aliasing we can not use a SolidColor brush over the top of the background
         // painter without seeing some background color bleeding through. As a result we use
         // the colorFilter to tint the normal background painter instead - b/253667329
@@ -503,7 +582,7 @@
             PlaceholderStage.WipeOff -> {
                 wipeOffBrush(
                     color,
-                    offset,
+                    placeholderState.backgroundOffset,
                     placeholderState
                 ) to null
             }
@@ -667,6 +746,10 @@
     alpha: Float = 1.0f,
     val shape: Shape
 ) : AbstractPlaceholderModifier(alpha, shape) {
+    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+        placeholderState.backgroundOffset = coordinates.positionInRoot()
+        super.onGloballyPositioned(coordinates)
+    }
     override fun generateBrush(offset: Offset): Brush? {
         return if (placeholderState.placeholderStage == PlaceholderStage.ShowPlaceholder) {
             Brush.linearGradient(
@@ -711,6 +794,7 @@
 }
 
 internal const val PLACEHOLDER_PROGRESSION_DURATION_MS = 800L
+internal const val PLACEHOLDER_WIPE_OFF_PROGRESSION_DURATION_MS = 250L
 internal const val PLACEHOLDER_DELAY_BETWEEN_PROGRESSIONS_MS = 800L
 internal const val PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS =
     PLACEHOLDER_PROGRESSION_DURATION_MS + PLACEHOLDER_DELAY_BETWEEN_PROGRESSIONS_MS
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
index d4f0e89..87deb2d 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
@@ -61,12 +61,9 @@
     scrollState: LazyListState,
     itemIndex: Int = 0,
     offset: Dp = 0.dp,
-): Modifier {
-    val targetItem = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
-    return if (targetItem != null) {
-        scrollAway { -targetItem.offset - offset.toPx() }
-    } else {
-        ignore()
+): Modifier = scrollAway {
+    scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+        -it.offset - offset.toPx()
     }
 }
 
@@ -84,55 +81,46 @@
     scrollState: ScalingLazyListState,
     itemIndex: Int = 1,
     offset: Dp = 0.dp,
-): Modifier {
-    val targetItem = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
-    return if (targetItem != null) {
-        scrollAway { -targetItem.offset - offset.toPx() }
-    } else {
-        ignore()
+): Modifier = scrollAway {
+    scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+        -it.offset - offset.toPx()
     }
 }
 
-private fun Modifier.scrollAway(yPxFn: Density.() -> Float): Modifier = this.then(
-    object : LayoutModifier {
-        override fun MeasureScope.measure(
-            measurable: Measurable,
-            constraints: Constraints
-        ): MeasureResult {
-            val placeable = measurable.measure(constraints)
-            return layout(placeable.width, placeable.height) {
+private fun Modifier.scrollAway(yPxFn: Density.() -> Float?): Modifier =
+    this.then(
+        object : LayoutModifier {
+            override fun MeasureScope.measure(
+                measurable: Measurable,
+                constraints: Constraints
+            ): MeasureResult {
                 val yPx = yPxFn()
-                val progress: Float = (yPx / maxScrollOut.toPx()).coerceIn(0f, 1f)
-                val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
-                val offsetY = -(maxOffset.toPx() * progress).toInt()
+                if (yPx == null) {
+                    return object : MeasureResult {
+                        override val width = 0
+                        override val height = 0
+                        override val alignmentLines = mapOf<AlignmentLine, Int>()
+                        override fun placeChildren() {}
+                    }
+                } else {
+                    val placeable = measurable.measure(constraints)
+                    return layout(placeable.width, placeable.height) {
+                        val progress: Float = (yPx / maxScrollOut.toPx()).coerceIn(0f, 1f)
+                        val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
+                        val offsetY = -(maxOffset.toPx() * progress).toInt()
 
-                placeable.placeWithLayer(0, offsetY) {
-                    alpha = motionFraction
-                    scaleX = motionFraction
-                    scaleY = motionFraction
-                    transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
+                        placeable.placeWithLayer(0, offsetY) {
+                            alpha = motionFraction
+                            scaleX = motionFraction
+                            scaleY = motionFraction
+                            transformOrigin =
+                                TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
+                        }
+                    }
                 }
             }
         }
-    }
-)
-
-// Trivial modifier that neither measures nor places the content.
-private fun Modifier.ignore(): Modifier = this.then(
-    object : LayoutModifier {
-        override fun MeasureScope.measure(
-            measurable: Measurable,
-            constraints: Constraints
-        ): MeasureResult {
-            return object : MeasureResult {
-                override val width = 0
-                override val height = 0
-                override val alignmentLines = mapOf<AlignmentLine, Int>()
-                override fun placeChildren() {}
-            }
-        }
-    }
-)
+    )
 
 // The scroll motion effects take place between 0dp and 36dp.
 internal val maxScrollOut = 36.dp
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
index 05ff127..7264351 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
@@ -16,15 +16,15 @@
 
 package androidx.wear.compose.material
 
-import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -38,11 +38,10 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
-import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -53,12 +52,12 @@
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
-import kotlin.math.PI
+import androidx.compose.ui.util.lerp
+import kotlin.math.max
+import kotlin.math.min
 import kotlin.math.roundToInt
-import kotlin.math.sin
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.isActive
 
@@ -101,7 +100,7 @@
     state: SwipeToDismissBoxState,
     modifier: Modifier = Modifier,
     backgroundScrimColor: Color = MaterialTheme.colors.background,
-    contentScrimColor: Color = contentColorFor(backgroundScrimColor),
+    contentScrimColor: Color = MaterialTheme.colors.background,
     backgroundKey: Any = SwipeToDismissKeys.Background,
     contentKey: Any = SwipeToDismissKeys.Content,
     hasBackground: Boolean = true,
@@ -118,61 +117,78 @@
                 state = state.swipeableState,
                 enabled = hasBackground,
                 anchors = anchors(maxWidth),
-                thresholds = { _, _ -> FractionalThreshold(SwipeThreshold) },
+                thresholds = { _, _ -> FractionalThreshold(SWIPE_THRESHOLD) },
                 resistance = ResistanceConfig(
                     basis = maxWidth,
-                    factorAtMin = TotalResistance,
-                    factorAtMax = TotalResistance,
+                    factorAtMin = TOTAL_RESISTANCE,
+                    factorAtMax = TOTAL_RESISTANCE,
                 ),
                 orientation = Orientation.Horizontal,
             )
     ) {
-        val dismissAnimatable = remember { Animatable(0f) }
+        // Use remember { derivedStateOf{ ... } } idiom to re-use modifiers where possible.
+
+        var squeezeMode by remember {
+            mutableStateOf(true)
+        }
 
         LaunchedEffect(state.isAnimationRunning) {
             if (state.targetValue == SwipeToDismissValue.Dismissed) {
-                dismissAnimatable.animateTo(1f, SpringSpec())
-            } else {
-                // because SwipeToDismiss remains alive, it worth resetting animation to 0
-                // when [targetValue] becomes [Original] again
-                dismissAnimatable.snapTo(0f)
+                squeezeMode = false
             }
         }
 
-        // Use remember { derivedStateOf{ ... } } idiom to re-use modifiers where possible.
+        LaunchedEffect(state.targetValue) {
+            if (!squeezeMode && state.targetValue == SwipeToDismissValue.Default) {
+                squeezeMode = true
+            }
+        }
+
         val isRound = isRoundDevice()
         val modifiers by remember(isRound, backgroundScrimColor) {
             derivedStateOf {
-                val squeezeMotion =
-                    SqueezeMotion(state.swipeableState.offset.value.roundToInt(), maxWidth)
+                val progress = (state.swipeableState.offset.value / maxWidth).coerceIn(0f, 1f)
+                val scale = lerp(SCALE_MAX, SCALE_MIN, progress).coerceIn(SCALE_MIN, SCALE_MAX)
+                val squeezeOffset = max(0f, (1f - scale) * maxWidth / 2f)
+                val slideOffset = lerp(squeezeOffset, maxWidth, max(0f, progress - 0.7f) / 0.3f)
+
+                val translationX = if (squeezeMode) squeezeOffset else slideOffset
+
+                val backgroundAlpha =
+                    lerp(
+                        MAX_BACKGROUND_SCRIM_ALPHA,
+                        MIN_BACKGROUND_SCRIM_ALPHA,
+                        BACKGROUND_SCRIM_EASING.transform(progress)
+                    )
+                val contentScrimAlpha = min(MAX_CONTENT_SCRIM_ALPHA, progress / 2f)
 
                 Modifiers(
-                    contentForeground =
-                    Modifier.offset { IntOffset(squeezeMotion.contentOffset, 0) }
+                    contentForeground = Modifier
                         .fillMaxSize()
-                        .scale(squeezeMotion.scale(dismissAnimatable.value))
+                        .graphicsLayer(
+                            translationX = translationX,
+                            scaleX = scale,
+                            scaleY = scale,
+                        )
                         .then(
-                            if (isRound && squeezeMotion.contentOffset > 0) {
+                            if (isRound && translationX > 0) {
                                 Modifier.clip(CircleShape)
                             } else {
                                 Modifier
                             }
                         )
-                        .alpha(1 - dismissAnimatable.value)
                         .background(backgroundScrimColor),
                     scrimForeground =
-                    Modifier.background(
-                        contentScrimColor.copy(alpha = squeezeMotion.contentScrimAlpha)
-                    ).fillMaxSize(),
-                    scrimBackground =
-                    Modifier.matchParentSize()
+                    Modifier
                         .background(
-                            backgroundScrimColor
-                                .copy(
-                                    alpha = squeezeMotion.backgroundScrimAlpha(
-                                        dismissAnimatable.value
-                                    )
-                                )
+                            contentScrimColor.copy(alpha = contentScrimAlpha)
+                        )
+                        .fillMaxSize(),
+                    scrimBackground =
+                    Modifier
+                        .matchParentSize()
+                        .background(
+                            backgroundScrimColor.copy(alpha = backgroundAlpha)
                         )
                 )
             }
@@ -313,9 +329,9 @@
         get() = swipeableState.isAnimationRunning
 
     internal fun edgeNestedScrollConnection(
-        swipeState: State<SwipeState>
+        edgeSwipeState: State<EdgeSwipeState>
     ): NestedScrollConnection =
-        swipeableState.edgeNestedScrollConnection(swipeState)
+        swipeableState.edgeNestedScrollConnection(edgeSwipeState)
 
     /**
      * Set the state without any animation and suspend until it's set
@@ -326,14 +342,14 @@
 
     private companion object {
         private fun <T> SwipeableState<T>.edgeNestedScrollConnection(
-            swipeState: State<SwipeState>
+            edgeSwipeState: State<EdgeSwipeState>
         ): NestedScrollConnection =
             object : NestedScrollConnection {
                 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                     val delta = available.x
                     // If swipeState = SwipeState.SWIPING_TO_DISMISS - perform swipeToDismiss
                     // drag and consume everything
-                    return if (swipeState.value == SwipeState.SwipingToDismiss &&
+                    return if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss &&
                         source == NestedScrollSource.Drag
                     ) {
                         performDrag(delta)
@@ -352,8 +368,8 @@
                 override suspend fun onPreFling(available: Velocity): Velocity {
                     val toFling = available.x
                     // Consumes fling by SwipeToDismiss
-                    return if (swipeState.value == SwipeState.SwipingToDismiss ||
-                        swipeState.value == SwipeState.SwipeToDismissInProgress
+                    return if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss ||
+                        edgeSwipeState.value == EdgeSwipeState.SwipeToDismissInProgress
                     ) {
                         performFling(velocity = toFling)
                         available
@@ -386,7 +402,7 @@
  */
 @Composable
 public fun rememberSwipeToDismissBoxState(
-    animationSpec: AnimationSpec<Float> = SwipeToDismissBoxDefaults.AnimationSpec,
+    animationSpec: AnimationSpec<Float> = SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC,
     confirmStateChange: (SwipeToDismissValue) -> Boolean = { true },
 ): SwipeToDismissBoxState {
     return remember(animationSpec, confirmStateChange) {
@@ -474,10 +490,10 @@
         }
     ) {
         // Tracks the current swipe status
-        val swipeState = remember { mutableStateOf(SwipeState.WaitingForTouch) }
+        val edgeSwipeState = remember { mutableStateOf(EdgeSwipeState.WaitingForTouch) }
         val nestedScrollConnection =
             remember(swipeToDismissBoxState) {
-                swipeToDismissBoxState.edgeNestedScrollConnection(swipeState)
+                swipeToDismissBoxState.edgeNestedScrollConnection(edgeSwipeState)
             }
 
         val nestedPointerInput: suspend PointerInputScope.() -> Unit = {
@@ -492,21 +508,21 @@
                             // the next touch. If it lands to the left of the first, we consider
                             // it as a swipe left and set the state to SwipingToPage. Otherwise,
                             // set the state to SwipingToDismiss
-                            when (swipeState.value) {
-                                SwipeState.SwipeToDismissInProgress,
-                                SwipeState.WaitingForTouch -> {
-                                    swipeState.value =
+                            when (edgeSwipeState.value) {
+                                EdgeSwipeState.SwipeToDismissInProgress,
+                                EdgeSwipeState.WaitingForTouch -> {
+                                    edgeSwipeState.value =
                                         if (change.position.x < edgeWidth.toPx())
-                                            SwipeState.EdgeClickedWaitingForDirection
+                                            EdgeSwipeState.EdgeClickedWaitingForDirection
                                         else
-                                            SwipeState.SwipingToPage
+                                            EdgeSwipeState.SwipingToPage
                                 }
-                                SwipeState.EdgeClickedWaitingForDirection -> {
-                                    swipeState.value =
+                                EdgeSwipeState.EdgeClickedWaitingForDirection -> {
+                                    edgeSwipeState.value =
                                         if (change.position.x < change.previousPosition.x)
-                                            SwipeState.SwipingToPage
+                                            EdgeSwipeState.SwipingToPage
                                         else
-                                            SwipeState.SwipingToDismiss
+                                            EdgeSwipeState.SwipingToDismiss
                                 }
                                 else -> {} // Do nothing
                             }
@@ -514,11 +530,11 @@
                             // or to SwipeToDismissInProgress if current
                             // state is SwipingToDismiss
                             if (change.changedToUp()) {
-                                swipeState.value =
-                                    if (swipeState.value == SwipeState.SwipingToDismiss)
-                                        SwipeState.SwipeToDismissInProgress
+                                edgeSwipeState.value =
+                                    if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss)
+                                        EdgeSwipeState.SwipeToDismissInProgress
                                     else
-                                        SwipeState.WaitingForTouch
+                                        EdgeSwipeState.WaitingForTouch
                             }
                         }
                     }
@@ -532,7 +548,7 @@
 /**
  * An enum which represents a current state of swipe action.
  */
-internal enum class SwipeState {
+internal enum class EdgeSwipeState {
     // Waiting for touch, edge was not touched before.
     WaitingForTouch,
 
@@ -551,70 +567,6 @@
 }
 
 /**
- * A class which is responsible for squeezing animation and all computations related to it
- */
-private class SqueezeMotion(
-    private val offsetPx: Int,
-    private val maxWidth: Float
-) {
-    private val scaleDelta = 0.2f
-    private val dismissScaleDelta = 0.05f
-    private val offsetFactor = scaleDelta / 2
-    private val contentScrimMaxAlpha = 0.07f
-    private val backgroundScrimMinAlpha = 0.65f
-
-    private val progress = calculateProgress(offsetPx.toFloat(), maxWidth)
-
-    /**
-     * [scale] can change from 1 to 1-[scaleDelta] - [dismissScaleDelta]
-     * As [progress] goes from 0 to 1 and [finalAnimationProgress] from 0 to 1,
-     * [scale] decreases accordingly up to a 1 - [scaleDelta] - [dismissScaleDelta]
-     */
-    fun scale(finalAnimationProgress: Float): Float =
-        (1.0 - progress * scaleDelta - finalAnimationProgress * dismissScaleDelta).toFloat()
-
-    /**
-     * As [progress] goes from 0 to 1, [contentOffset] changes from 0 to maxWidth
-     * mutiplied by [offsetFactor].
-     * If [offsetPx] negative ( <= 0) it remains the same.
-     * This helps to have a resistance animation if page is swiped to the opposite
-     * direction.
-     */
-    val contentOffset: Int
-        get() = if (offsetPx > 0)
-            (maxWidth * progress * offsetFactor).toInt()
-        else
-            offsetPx
-
-    /**
-     * As [progress] goes from 0 to 1, [contentScrimAlpha] changes from 0%
-     * to [contentScrimMaxAlpha].
-     */
-    val contentScrimAlpha: Float
-        get() = contentScrimMaxAlpha * progress
-
-    /**
-     * As [progress] goes from 0 to 1, [backgroundScrimAlpha] is decreasing from 100% to
-     * [backgroundScrimMinAlpha] value. [finalAnimationProgress] makes background completely
-     * transparent by changing its value from [backgroundScrimMinAlpha] to 0.
-     */
-    fun backgroundScrimAlpha(finalAnimationProgress: Float): Float =
-        1 - (1 - backgroundScrimMinAlpha) * progress -
-            backgroundScrimMinAlpha * finalAnimationProgress
-
-    /**
-     *  Computes a progress, which is in range from 0 to 1. As offset value changes from 0 to basis,
-     * the progress changes by sin function
-     */
-    private fun calculateProgress(
-        offset: Float,
-        basis: Float
-    ): Float = if (offset > 0)
-        sin((offset / basis).coerceIn(-1f, 1f) * PI.toFloat() / 2)
-    else 0f
-}
-
-/**
  * Class to enable calculating group of modifiers in a single, memoised block.
  */
 private data class Modifiers(
@@ -630,5 +582,13 @@
         maxWidth to SwipeToDismissValue.Dismissed
     )
 
-private val SwipeThreshold = 0.5f
-private val TotalResistance = 1000f
\ No newline at end of file
+private const val SWIPE_THRESHOLD = 0.5f
+private const val TOTAL_RESISTANCE = 1000f
+private const val SCALE_MAX = 1f
+private const val SCALE_MIN = 0.7f
+private const val MAX_CONTENT_SCRIM_ALPHA = 0.3f
+private const val MAX_BACKGROUND_SCRIM_ALPHA = 0.75f
+private const val MIN_BACKGROUND_SCRIM_ALPHA = 0f
+private val BACKGROUND_SCRIM_EASING = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
+private val SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC =
+    TweenSpec<Float>(200, 0, LinearOutSlowInEasing)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
index 145e8b0..b87eb398 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
@@ -371,7 +371,8 @@
 
     Box {
         Chip(
-            modifier = modifier.fillMaxWidth(),
+            modifier = modifier
+                .fillMaxWidth(),
             >
             label = {
                 Text(
@@ -463,7 +464,7 @@
                 },
                 enabled = true,
                 colors = PlaceholderDefaults.placeholderChipColors(
-                    placeholderState = chipPlaceholderState,
+                    placeholderState = chipPlaceholderState
                 )
             )
         }
@@ -609,7 +610,7 @@
                         MaterialTheme.shapes.large
                     ),
                 backgroundPainter = PlaceholderDefaults.placeholderBackgroundBrush(
-                    placeholderState = cardPlaceholderState,
+                    placeholderState = cardPlaceholderState
                 )
             ) {
                 Spacer(modifier = Modifier.height(4.dp))
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index c30e3fa..26ca147 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -42,15 +42,6 @@
 import androidx.test.screenshot.assertAgainstGolden
 import androidx.wear.watchface.BoundingArc
 import androidx.wear.watchface.CanvasComplication
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationText
-import androidx.wear.watchface.complications.data.ComplicationType
-import androidx.wear.watchface.complications.data.LongTextComplicationData
-import androidx.wear.watchface.complications.data.PlainComplicationText
-import androidx.wear.watchface.complications.data.RangedValueComplicationData
-import androidx.wear.watchface.complications.data.ShortTextComplicationData
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.ComplicationSlot
 import androidx.wear.watchface.ComplicationSlotBoundsType
@@ -73,24 +64,35 @@
 import androidx.wear.watchface.client.WatchFaceClientExperimental
 import androidx.wear.watchface.client.WatchFaceControlClient
 import androidx.wear.watchface.client.WatchUiState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.ComplicationExperimental
+import androidx.wear.watchface.complications.data.ComplicationText
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.LongTextComplicationData
 import androidx.wear.watchface.complications.data.NoDataComplicationData
+import androidx.wear.watchface.complications.data.PlainComplicationText
+import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.ShortTextComplicationData
 import androidx.wear.watchface.control.IInteractiveWatchFace
 import androidx.wear.watchface.control.WatchFaceControlService
-import androidx.wear.watchface.samples.BLUE_STYLE
-import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
-import androidx.wear.watchface.samples.COMPLICATIONS_STYLE_SETTING
-import androidx.wear.watchface.samples.DRAW_HOUR_PIPS_STYLE_SETTING
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.BLUE_STYLE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.COLOR_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.COMPLICATIONS_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.CONFIGURABLE_DATA_SOURCE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.CONFIGURABLE_DATA_SOURCE_PKG
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.DRAW_HOUR_PIPS_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.GREEN_STYLE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.LEFT_COMPLICATION
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.NO_COMPLICATIONS
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.WATCH_HAND_LENGTH_STYLE_SETTING
 import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService
-import androidx.wear.watchface.samples.GREEN_STYLE
-import androidx.wear.watchface.samples.LEFT_COMPLICATION
-import androidx.wear.watchface.samples.NO_COMPLICATIONS
 import androidx.wear.watchface.samples.R
-import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING
 import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleData
@@ -100,6 +102,12 @@
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
 import androidx.wear.watchface.style.WatchFaceLayer
 import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
@@ -119,12 +127,6 @@
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.MockitoAnnotations
-import java.time.Instant
-import java.time.ZoneId
-import java.time.ZonedDateTime
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
 
 private const val CONNECT_TIMEOUT_MILLIS = 500L
 private const val DESTROY_TIMEOUT_MILLIS = 500L
@@ -359,7 +361,7 @@
         headlessInstance.close()
     }
 
-    @Suppress("DEPRECATION") // defaultDataSourceType
+    @Suppress("DEPRECATION", "NewApi") // defaultDataSourceType
     @Test
     fun headlessComplicationDetails() {
         val headlessInstance = service.createHeadlessWatchFaceClient(
@@ -388,6 +390,8 @@
         )
         assertThat(leftComplicationDetails.supportedTypes).containsExactly(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.LONG_TEXT,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
@@ -411,6 +415,8 @@
         )
         assertThat(rightComplicationDetails.supportedTypes).containsExactly(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.LONG_TEXT,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
@@ -601,8 +607,11 @@
         assertThat(flavorA.style.userStyleMap.containsKey("color_style_setting"))
         assertThat(flavorA.style.userStyleMap.containsKey("watch_hand_length_style_setting"))
         assertThat(flavorA.complications.containsKey(EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID))
-        assertThat(flavorA.complications.containsKey(
-            EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID))
+        assertThat(
+            flavorA.complications.containsKey(
+                EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+            )
+        )
 
         headlessInstance.close()
     }
@@ -704,7 +713,7 @@
         }
     }
 
-    @Suppress("DEPRECATION") // defaultDataSourceType
+    @Suppress("DEPRECATION", "newApi") // defaultDataSourceType & ComplicationType
     @Test
     fun interactiveWatchFaceClient_ComplicationDetails() {
         val deferredInteractiveInstance = handlerCoroutineScope.async {
@@ -740,6 +749,8 @@
         )
         assertThat(leftComplicationDetails.supportedTypes).containsExactly(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.LONG_TEXT,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
@@ -769,6 +780,8 @@
         )
         assertThat(rightComplicationDetails.supportedTypes).containsExactly(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.LONG_TEXT,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
@@ -1080,23 +1093,23 @@
         )
         (wallpaperService as TestExampleCanvasAnalogWatchFaceService)
             .watchFace.renderer.additionalContentDescriptionLabels = listOf(
-                Pair(
-                    0,
-                    ContentDescriptionLabel(
-                        PlainComplicationText.Builder("Before").build(),
-                        Rect(10, 10, 20, 20),
-                        pendingIntent1
-                    )
-                ),
-                Pair(
-                    20000,
-                    ContentDescriptionLabel(
-                        PlainComplicationText.Builder("After").build(),
-                        Rect(30, 30, 40, 40),
-                        pendingIntent2
-                    )
+            Pair(
+                0,
+                ContentDescriptionLabel(
+                    PlainComplicationText.Builder("Before").build(),
+                    Rect(10, 10, 20, 20),
+                    pendingIntent1
+                )
+            ),
+            Pair(
+                20000,
+                ContentDescriptionLabel(
+                    PlainComplicationText.Builder("After").build(),
+                    Rect(30, 30, 40, 40),
+                    pendingIntent2
                 )
             )
+        )
 
         val sysUiInterface =
             service.getInteractiveWatchFaceClientInstance("testId")!!
@@ -1273,8 +1286,8 @@
                     androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType(
                         DefaultComplicationDataSourcePolicy(
                             ComponentName(
-                                androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE_PKG,
-                                androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE
+                                CONFIGURABLE_DATA_SOURCE_PKG,
+                                CONFIGURABLE_DATA_SOURCE
                             ),
                             ComplicationType.SHORT_TEXT,
                             SystemDataSources.DATA_SOURCE_DAY_OF_WEEK,
@@ -1308,8 +1321,8 @@
                     androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType(
                         DefaultComplicationDataSourcePolicy(
                             ComponentName(
-                                androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE_PKG,
-                                androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE
+                                CONFIGURABLE_DATA_SOURCE_PKG,
+                                CONFIGURABLE_DATA_SOURCE
                             ),
                             ComplicationType.SHORT_TEXT,
                             SystemDataSources.DATA_SOURCE_DAY_OF_WEEK,
@@ -2448,7 +2461,8 @@
                 canvas: Canvas,
                 bounds: Rect,
                 zonedDateTime: ZonedDateTime
-            ) {}
+            ) {
+            }
         }
         return WatchFace(WatchFaceType.DIGITAL, renderer)
     }
@@ -2495,7 +2509,7 @@
                             enabled = true,
                             nameResourceId = R.string.left_complication_screen_name,
                             screenReaderNameResourceId =
-                                R.string.left_complication_screen_reader_name
+                            R.string.left_complication_screen_reader_name
                         )
                     )
                 )
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
index b65515b..7a59c11 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
@@ -27,16 +27,16 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.wear.watchface.BoundingArc
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
-import androidx.wear.watchface.client.WatchFaceMetadataClient
-import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.ComplicationSlotBoundsType
+import androidx.wear.watchface.client.WatchFaceMetadataClient
+import androidx.wear.watchface.complications.SystemDataSources
 import androidx.wear.watchface.complications.data.ComplicationExperimental
-import androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE
-import androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE_PKG
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.control.WatchFaceControlService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.CONFIGURABLE_DATA_SOURCE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.CONFIGURABLE_DATA_SOURCE_PKG
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleSetting
 import com.google.common.truth.Truth
@@ -134,6 +134,7 @@
     }
 
     @Test
+    @Suppress("NewApi")
     public fun getComplicationSlotMetadataMap() {
         val service = createWatchFaceMetadataClient(exampleWatchFaceComponentName)
         val complicationSlotMetadataMap = service.getComplicationSlotMetadataMap()
@@ -156,6 +157,8 @@
         ).isEqualTo(ComplicationType.SHORT_TEXT)
         Truth.assertThat(leftComplicationMetadata.supportedTypes).containsExactly(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.LONG_TEXT,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
@@ -181,6 +184,8 @@
         ).isEqualTo(ComplicationType.SHORT_TEXT)
         Truth.assertThat(rightComplicationMetadata.supportedTypes).containsExactly(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.LONG_TEXT,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
@@ -365,7 +370,8 @@
         // check settings' defaults
         val userStyleMap = flavor.style.userStyleMap
         Truth.assertThat(userStyleMap.keys).containsExactly(
-            "color_style_setting", "watch_hand_length_style_setting")
+            "color_style_setting", "watch_hand_length_style_setting"
+        )
 
         val userStyle = UserStyle(flavor.style, schema)
         Truth.assertThat(userStyle[UserStyleSetting.Id("color_style_setting")]!!.id)
@@ -376,7 +382,8 @@
 
         Truth.assertThat(complications.keys).containsExactly(
             EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
-            EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID)
+            EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+        )
 
         val left = complications[EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID]!!
         val right = complications[EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID]!!
@@ -384,19 +391,27 @@
         Truth.assertThat(left.primaryDataSource).isNull()
         Truth.assertThat(left.secondaryDataSource).isNull()
         Truth.assertThat(left.systemDataSourceFallback).isEqualTo(
-            SystemDataSources.DATA_SOURCE_DAY_OF_WEEK)
+            SystemDataSources.DATA_SOURCE_DAY_OF_WEEK
+        )
         Truth.assertThat(left.systemDataSourceFallbackDefaultType).isEqualTo(
-            ComplicationType.SHORT_TEXT)
+            ComplicationType.SHORT_TEXT
+        )
 
-        Truth.assertThat(right.primaryDataSource).isEqualTo(ComponentName(
-            CONFIGURABLE_DATA_SOURCE_PKG, CONFIGURABLE_DATA_SOURCE))
+        Truth.assertThat(right.primaryDataSource).isEqualTo(
+            ComponentName(
+                CONFIGURABLE_DATA_SOURCE_PKG, CONFIGURABLE_DATA_SOURCE
+            )
+        )
         Truth.assertThat(right.primaryDataSourceDefaultType).isEqualTo(
-            ComplicationType.SHORT_TEXT)
+            ComplicationType.SHORT_TEXT
+        )
         Truth.assertThat(right.secondaryDataSource).isNull()
         Truth.assertThat(right.systemDataSourceFallback).isEqualTo(
-            SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET)
+            SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET
+        )
         Truth.assertThat(right.systemDataSourceFallbackDefaultType).isEqualTo(
-            ComplicationType.SHORT_TEXT)
+            ComplicationType.SHORT_TEXT
+        )
     }
 
     @Test
@@ -405,20 +420,24 @@
             WatchFaceMetadataClient.isXmlVersionCompatible(
                 context,
                 context.resources,
-                context.packageName)).isTrue()
+                context.packageName
+            )
+        ).isTrue()
         Truth.assertThat(
             WatchFaceMetadataClient.isXmlVersionCompatible(
                 context,
                 context.resources,
                 context.packageName,
                 OutdatedWatchFaceControlTestService::class.java.name
-            )).isFalse()
+            )
+        ).isFalse()
         Truth.assertThat(
             WatchFaceMetadataClient.isXmlVersionCompatible(
                 context,
                 context.resources,
                 "non.existing.package",
                 "non.existing.package.Service"
-            )).isFalse()
+            )
+        ).isFalse()
     }
 }
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/AndroidManifest.xml b/wear/watchface/watchface-complications-data-source-samples/src/main/AndroidManifest.xml
index 6215d9d..49a47bc 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/AndroidManifest.xml
@@ -100,6 +100,82 @@
             </intent-filter>
         </service>
 
+        <service
+            android:name=
+                "androidx.wear.watchface.complications.datasource.samples.ColorRampDataSourceService"
+            android:label="@string/color_ramp_data_source_name"
+            android:exported="true"
+            android:icon="@drawable/circle"
+            android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+            <meta-data
+                android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+                android:value="RANGED_VALUE"/>
+            <meta-data
+                android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+                android:value="0"/>
+            <intent-filter>
+                <action android:name=
+                    "android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
+            </intent-filter>
+        </service>
+
+        <service
+            android:name=
+                "androidx.wear.watchface.complications.datasource.samples.NonInterpolatedColorRampDataSourceService"
+            android:label="@string/color_ramp2_data_source_name"
+            android:exported="true"
+            android:icon="@drawable/circle"
+            android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+            <meta-data
+                android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+                android:value="RANGED_VALUE"/>
+            <meta-data
+                android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+                android:value="0"/>
+            <intent-filter>
+                <action android:name=
+                    "android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
+            </intent-filter>
+        </service>
+
+        <service
+            android:name=
+                "androidx.wear.watchface.complications.datasource.samples.GoalProgressDataSourceService"
+            android:label="@string/goal_progress_data_source_name"
+            android:exported="true"
+            android:icon="@drawable/circle"
+            android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+            <meta-data
+                android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+                android:value="GOAL_PROGRESS"/>
+            <meta-data
+                android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+                android:value="0"/>
+            <intent-filter>
+                <action android:name=
+                    "android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
+            </intent-filter>
+        </service>
+
+        <service
+            android:name=
+                "androidx.wear.watchface.complications.datasource.samples.WeightedElementDataSourceService"
+            android:label="@string/weighted_elements_data_source_name"
+            android:exported="true"
+            android:icon="@drawable/circle"
+            android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+            <meta-data
+                android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+                android:value="WEIGHTED_ELEMENTS"/>
+            <meta-data
+                android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+                android:value="0"/>
+            <intent-filter>
+                <action android:name=
+                    "android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
+            </intent-filter>
+        </service>
+
         <service android:name=".ConfigurableDataSourceService"
             android:label="@string/configurable_data_source_name"
             android:exported="true"
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/AsynchronousDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/AsynchronousDataSourceService.kt
index 33d4247..d718dbb 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/AsynchronousDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/AsynchronousDataSourceService.kt
@@ -20,6 +20,7 @@
 import android.text.SpannableString
 import android.text.Spanned
 import android.text.style.ForegroundColorSpan
+import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
 import androidx.wear.watchface.complications.datasource.ComplicationRequest
 import androidx.wear.watchface.complications.data.ComplicationText
@@ -67,7 +68,7 @@
         }
     }
 
-    override fun getPreviewData(type: ComplicationType) = when (type) {
+    override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
         ComplicationType.SHORT_TEXT ->
             ShortTextComplicationData.Builder(
                 plainText("# 123"),
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/BackgroundDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/BackgroundDataSourceService.kt
index 7fa3f3e..29d15de 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/BackgroundDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/BackgroundDataSourceService.kt
@@ -19,6 +19,7 @@
 import android.content.ComponentName
 import android.os.Handler
 import android.os.Looper
+import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
 import androidx.wear.watchface.complications.datasource.ComplicationRequest
 import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester
@@ -80,7 +81,7 @@
         )
     }
 
-    override fun getPreviewData(type: ComplicationType) = when (type) {
+    override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
         ComplicationType.SHORT_TEXT ->
             ShortTextComplicationData.Builder(
                 plainText("# 123"),
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ColorRampDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ColorRampDataSourceService.kt
new file mode 100644
index 0000000..9b82b9a
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ColorRampDataSourceService.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.complications.datasource.samples
+
+import android.graphics.Color
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.watchface.complications.data.ColorRamp
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.RangedValueTypes
+import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+
+/** Trivial example of serving [RangedValueComplicationData] with an interpolated color ramp. */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class ColorRampDataSourceService : ComplicationDataSourceService() {
+
+    override fun onComplicationRequest(
+        request: ComplicationRequest,
+        listener: ComplicationRequestListener
+    ) {
+        listener.onComplicationData(
+            RangedValueComplicationData.Builder(
+                value = 75f,
+                min = 0.0f,
+                max = 100.0f,
+                plainText("Many colors")
+            ).setText(plainText("Colors"))
+                .setValueType(RangedValueTypes.SCORE)
+                .setColorRamp(
+                    ColorRamp(
+                        intArrayOf(
+                            Color.GREEN,
+                            Color.YELLOW,
+                            Color.argb(255, 255, 255, 0),
+                            Color.RED,
+                            Color.argb(255, 255, 0, 255),
+                            Color.argb(255, 92, 64, 51)
+                        ),
+                        interpolated = true
+                    )
+                )
+                .build()
+        )
+    }
+
+    override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+        ComplicationType.RANGED_VALUE ->
+            RangedValueComplicationData.Builder(
+                value = 10f,
+                min = 0.0f,
+                max = 100.0f,
+                plainText("Many colors")
+            ).setText(plainText("Colors"))
+                .setValueType(RangedValueTypes.SCORE)
+                .setColorRamp(
+                    ColorRamp(
+                        intArrayOf(
+                            Color.GREEN,
+                            Color.YELLOW,
+                            Color.argb(255, 255, 255, 0),
+                            Color.RED,
+                            Color.argb(255, 255, 0, 255),
+                            Color.argb(255, 92, 64, 51)
+                        ),
+                        interpolated = true
+                    )
+                )
+                .build()
+
+        else -> null
+    }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ConfigurableDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ConfigurableDataSourceService.kt
index e9df7f6..6ab1aca 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ConfigurableDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ConfigurableDataSourceService.kt
@@ -38,11 +38,9 @@
         listener.onComplicationData(makeComplicationData(value))
     }
 
-    override fun getPreviewData(type: ComplicationType): ComplicationData? {
-        return when (type) {
-            ComplicationType.SHORT_TEXT -> makeComplicationData(DEFAULT_VALUE)
-            else -> null
-        }
+    override fun getPreviewData(type: ComplicationType) = when (type) {
+        ComplicationType.SHORT_TEXT -> makeComplicationData(DEFAULT_VALUE)
+        else -> null
     }
 
     private fun makeComplicationData(value: Int): ComplicationData {
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/GoalProgressDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/GoalProgressDataSourceService.kt
new file mode 100644
index 0000000..3108039
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/GoalProgressDataSourceService.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.complications.datasource.samples
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.GoalProgressComplicationData
+import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+
+/** Trivial example of serving [GoalProgressComplicationData]. */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class GoalProgressDataSourceService : ComplicationDataSourceService() {
+
+    override fun onComplicationRequest(
+        request: ComplicationRequest,
+        listener: ComplicationRequestListener
+    ) {
+        listener.onComplicationData(
+            GoalProgressComplicationData.Builder(
+                value = 12345.0f,
+                targetValue = 10000.0f,
+                plainText("12345 steps")
+            ).setText(plainText("12345"))
+                .setTitle(plainText("Steps"))
+                .build()
+        )
+    }
+
+    override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+        ComplicationType.GOAL_PROGRESS ->
+            GoalProgressComplicationData.Builder(
+                value = 1024.0f,
+                targetValue = 10000.0f,
+                plainText("Steps complication")
+            ).setText(plainText("1024"))
+                .setTitle(plainText("Steps"))
+                .build()
+
+        else -> null
+    }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ImmediateDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ImmediateDataSourceService.kt
index a2d7957..0481ad4 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ImmediateDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ImmediateDataSourceService.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.watchface.complications.datasource.samples
 
+import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
 import androidx.wear.watchface.complications.datasource.ComplicationRequest
 import androidx.wear.watchface.complications.data.ComplicationText
@@ -49,7 +50,7 @@
         )
     }
 
-    override fun getPreviewData(type: ComplicationType) = when (type) {
+    override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
         ComplicationType.SHORT_TEXT ->
             ShortTextComplicationData.Builder(
                 plainText("# 123"),
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/NonInterpolatedColorRampDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/NonInterpolatedColorRampDataSourceService.kt
new file mode 100644
index 0000000..c00411d
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/NonInterpolatedColorRampDataSourceService.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.complications.datasource.samples
+
+import android.graphics.Color
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.watchface.complications.data.ColorRamp
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.RangedValueTypes
+import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+
+/** Trivial example of serving [RangedValueComplicationData] with a non-interpolated ramp. */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class NonInterpolatedColorRampDataSourceService : ComplicationDataSourceService() {
+
+    override fun onComplicationRequest(
+        request: ComplicationRequest,
+        listener: ComplicationRequestListener
+    ) {
+        listener.onComplicationData(
+            RangedValueComplicationData.Builder(
+                value = 75f,
+                min = 0.0f,
+                max = 100.0f,
+                plainText("Example")
+            ).setText(plainText("Example"))
+                .setValueType(RangedValueTypes.SCORE)
+                .setColorRamp(
+                    ColorRamp(
+                        intArrayOf(Color.GREEN, Color.YELLOW, Color.RED),
+                        interpolated = false
+                    )
+                )
+                .build()
+        )
+    }
+
+    override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+        ComplicationType.RANGED_VALUE ->
+            RangedValueComplicationData.Builder(
+                value = 10f,
+                min = 0.0f,
+                max = 100.0f,
+                plainText("Example")
+            ).setText(plainText("Example"))
+                .setValueType(RangedValueTypes.SCORE)
+                .setColorRamp(
+                    ColorRamp(
+                        intArrayOf(Color.GREEN, Color.YELLOW, Color.RED),
+                        interpolated = false
+                    )
+                )
+                .build()
+
+        else -> null
+    }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/SynchronousDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/SynchronousDataSourceService.kt
index 1e9acb7..f4a625e 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/SynchronousDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/SynchronousDataSourceService.kt
@@ -74,21 +74,19 @@
         )
     }
 
-    override fun getPreviewData(type: ComplicationType): ComplicationData? {
-        return when (type) {
-            ComplicationType.SHORT_TEXT ->
-                ShortTextComplicationData.Builder(
-                    plainText("S 10"),
-                    ComplicationText.EMPTY
-                ).build()
+    override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+        ComplicationType.SHORT_TEXT ->
+            ShortTextComplicationData.Builder(
+                plainText("S 10"),
+                ComplicationText.EMPTY
+            ).build()
 
-            ComplicationType.LONG_TEXT ->
-                LongTextComplicationData.Builder(
-                    plainText("Secs 10"),
-                    ComplicationText.EMPTY
-                ).build()
+        ComplicationType.LONG_TEXT ->
+            LongTextComplicationData.Builder(
+                plainText("Secs 10"),
+                ComplicationText.EMPTY
+            ).build()
 
-            else -> null
-        }
+        else -> null
     }
 }
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/WeightedElementDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/WeightedElementDataSourceService.kt
new file mode 100644
index 0000000..073af3f
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/WeightedElementDataSourceService.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.complications.datasource.samples
+
+import android.graphics.Color
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.WeightedElementsComplicationData
+import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+
+/** Trivial example of serving [WeightedElementsComplicationData]. */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WeightedElementDataSourceService : ComplicationDataSourceService() {
+
+    override fun onComplicationRequest(
+        request: ComplicationRequest,
+        listener: ComplicationRequestListener
+    ) {
+        listener.onComplicationData(
+            WeightedElementsComplicationData.Builder(
+                listOf(
+                    WeightedElementsComplicationData.Element(1.0f, Color.RED),
+                    WeightedElementsComplicationData.Element(1.0f, Color.GREEN),
+                    WeightedElementsComplicationData.Element(2.0f, Color.BLUE),
+                    WeightedElementsComplicationData.Element(3.0f, Color.YELLOW)
+                ),
+                plainText("Example weighted elements")
+            ).setText(plainText("Calories"))
+                .build()
+        )
+    }
+
+    override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+        ComplicationType.WEIGHTED_ELEMENTS ->
+            WeightedElementsComplicationData.Builder(
+                listOf(
+                    WeightedElementsComplicationData.Element(1.0f, Color.RED),
+                    WeightedElementsComplicationData.Element(2.0f, Color.GREEN),
+                    WeightedElementsComplicationData.Element(3.0f, Color.BLUE),
+                ),
+                plainText("Example weighted elements")
+            ).setText(plainText("Calories"))
+                .build()
+
+        else -> null
+    }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/res/values/strings.xml b/wear/watchface/watchface-complications-data-source-samples/src/main/res/values/strings.xml
index 814f7ca..84c8731 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/res/values/strings.xml
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/res/values/strings.xml
@@ -22,6 +22,10 @@
     <string name="configurable_data_source_name" translatable="false">Configurable</string>
     <string name="immediate_data_source_name" translatable="false">Immediate</string>
     <string name="synchronous_data_source_name" translatable="false">Synchronous</string>
+    <string name="color_ramp_data_source_name" translatable="false">Color Ramp</string>
+    <string name="color_ramp2_data_source_name" translatable="false">Non interpolated color ramp</string>
+    <string name="goal_progress_data_source_name" translatable="false">Goal Progress</string>
+    <string name="weighted_elements_data_source_name" translatable="false">Weighted Elements</string>
 
     <string name="config_title" translatable="false">Configuration</string>
     <string name="config_button_title" translatable="false">Generate</string>
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index d6cae24..1aea238 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -1,6 +1,14 @@
 // Signature format: 4.0
 package androidx.wear.watchface.complications.data {
 
+  public final class ColorRamp {
+    ctor public ColorRamp(@ColorInt int[] colors, boolean interpolated);
+    method public int[] getColors();
+    method public boolean getInterpolated();
+    property public final int[] colors;
+    property public final boolean interpolated;
+  }
+
   public abstract sealed class ComplicationData {
     method public final android.content.ComponentName? getDataSource();
     method public final int getDisplayPolicy();
@@ -50,6 +58,7 @@
     method public static androidx.wear.watchface.complications.data.ComplicationType valueOf(String name) throws java.lang.IllegalArgumentException;
     method public static androidx.wear.watchface.complications.data.ComplicationType[] values();
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType EMPTY;
+    enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType LONG_TEXT;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType MONOCHROMATIC_IMAGE;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NOT_CONFIGURED;
@@ -59,6 +68,7 @@
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType RANGED_VALUE;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SHORT_TEXT;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SMALL_IMAGE;
+    enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType WEIGHTED_ELEMENTS;
   }
 
   public final class CountDownTimeReference {
@@ -81,6 +91,42 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+    method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+    method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
+    method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
+    method public float getTargetValue();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getText();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
+    method public float getValue();
+    property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+    property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
+    property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
+    property public final float targetValue;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? text;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+    property public final float value;
+    field public static final float PLACEHOLDER;
+    field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
+  }
+
+  public static final class GoalProgressComplicationData.Builder {
+    ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
+    method public final T setDataSource(android.content.ComponentName? dataSource);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+  }
+
   public final class ImageKt {
   }
 
@@ -214,6 +260,7 @@
   }
 
   public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+    method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
     method public float getMax();
     method public float getMin();
@@ -222,6 +269,8 @@
     method public androidx.wear.watchface.complications.data.ComplicationText? getText();
     method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
     method public float getValue();
+    method public int getValueType();
+    property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
     property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
     property public final float max;
     property public final float min;
@@ -230,6 +279,7 @@
     property public final androidx.wear.watchface.complications.data.ComplicationText? text;
     property public final androidx.wear.watchface.complications.data.ComplicationText? title;
     property public final float value;
+    property public final int valueType;
     field public static final float PLACEHOLDER;
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
@@ -237,6 +287,7 @@
   public static final class RangedValueComplicationData.Builder {
     ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
+    method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
     method public final T setDataSource(android.content.ComponentName? dataSource);
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -246,6 +297,13 @@
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+    method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
+  }
+
+  public final class RangedValueTypes {
+    field public static final androidx.wear.watchface.complications.data.RangedValueTypes INSTANCE;
+    field public static final int SCORE = 1; // 0x1
+    field public static final int UNDEFINED = 0; // 0x0
   }
 
   public final class ShortTextComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
@@ -392,6 +450,53 @@
   public final class TypeKt {
   }
 
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+    method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+    method public int getElementBackgroundColor();
+    method public java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> getElements();
+    method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
+    method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getText();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
+    property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+    property public final int elementBackgroundColor;
+    property public final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements;
+    property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
+    property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? text;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+    field public static final androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Companion Companion;
+    field public static final int MAX_NUM_ELEMENTS = 20; // 0x14
+    field public static final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> PLACEHOLDER;
+    field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
+  }
+
+  public static final class WeightedElementsComplicationData.Builder {
+    ctor public WeightedElementsComplicationData.Builder(java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData build();
+    method public final T setDataSource(android.content.ComponentName? dataSource);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setElementBackgroundColor(@ColorInt int elementBackgroundColor);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+  }
+
+  public static final class WeightedElementsComplicationData.Companion {
+  }
+
+  public static final class WeightedElementsComplicationData.Element {
+    ctor public WeightedElementsComplicationData.Element(@FloatRange(from=0.0, fromInclusive=false) float weight, @ColorInt int color);
+    method public int getColor();
+    method public float getWeight();
+    property public final int color;
+    property public final float weight;
+  }
+
 }
 
 package androidx.wear.watchface.utility {
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 8f00efe..6a990c8 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.wear.watchface.complications.data {
 
-  @androidx.wear.watchface.complications.data.ComplicationExperimental public final class ColorRamp {
+  public final class ColorRamp {
     ctor public ColorRamp(@ColorInt int[] colors, boolean interpolated);
     method public int[] getColors();
     method public boolean getInterpolated();
@@ -61,19 +61,17 @@
     method public static androidx.wear.watchface.complications.data.ComplicationType valueOf(String name) throws java.lang.IllegalArgumentException;
     method public static androidx.wear.watchface.complications.data.ComplicationType[] values();
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType EMPTY;
-    enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
-    enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType LIST;
+    enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType LONG_TEXT;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType MONOCHROMATIC_IMAGE;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NOT_CONFIGURED;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NO_DATA;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NO_PERMISSION;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType PHOTO_IMAGE;
-    enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType PROTO_LAYOUT;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType RANGED_VALUE;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SHORT_TEXT;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SMALL_IMAGE;
-    enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType WEIGHTED_ELEMENTS;
+    enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType WEIGHTED_ELEMENTS;
   }
 
   public final class CountDownTimeReference {
@@ -96,8 +94,8 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
-  @androidx.wear.watchface.complications.data.ComplicationExperimental public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
-    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+    method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
     method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
     method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
@@ -105,7 +103,7 @@
     method public androidx.wear.watchface.complications.data.ComplicationText? getText();
     method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
     method public float getValue();
-    property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
+    property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
     property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
     property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
     property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
@@ -120,7 +118,7 @@
   public static final class GoalProgressComplicationData.Builder {
     ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
-    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
     method public final T setDataSource(android.content.ComponentName? dataSource);
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -135,34 +133,6 @@
   public final class ImageKt {
   }
 
-  @androidx.wear.watchface.complications.data.ComplicationExperimental public final class ListComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
-    method public java.util.List<androidx.wear.watchface.complications.data.ComplicationData> getComplicationList();
-    method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
-    method public androidx.wear.watchface.complications.data.ListComplicationData.StyleHint getStyleHint();
-    property public final java.util.List<androidx.wear.watchface.complications.data.ComplicationData> complicationList;
-    property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
-    property public final androidx.wear.watchface.complications.data.ListComplicationData.StyleHint styleHint;
-    field public static final int MAX_ITEMS = 5; // 0x5
-    field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
-  }
-
-  public static final class ListComplicationData.Builder {
-    ctor public ListComplicationData.Builder(java.util.List<? extends androidx.wear.watchface.complications.data.ComplicationData> complicationList, androidx.wear.watchface.complications.data.ListComplicationData.StyleHint styleHint, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
-    method public androidx.wear.watchface.complications.data.ListComplicationData build();
-    method public final T setDataSource(android.content.ComponentName? dataSource);
-    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
-    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
-    method public androidx.wear.watchface.complications.data.ListComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
-    method public androidx.wear.watchface.complications.data.ListComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
-  }
-
-  @androidx.wear.watchface.complications.data.ComplicationExperimental public enum ListComplicationData.StyleHint {
-    method public static androidx.wear.watchface.complications.data.ListComplicationData.StyleHint valueOf(String name) throws java.lang.IllegalArgumentException;
-    method public static androidx.wear.watchface.complications.data.ListComplicationData.StyleHint[] values();
-    enum_constant public static final androidx.wear.watchface.complications.data.ListComplicationData.StyleHint ColumnOfRows;
-    enum_constant public static final androidx.wear.watchface.complications.data.ListComplicationData.StyleHint RowOfColumns;
-  }
-
   public final class LongTextComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
     method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
@@ -292,31 +262,8 @@
     method public androidx.wear.watchface.complications.data.PlainComplicationText build();
   }
 
-  @androidx.wear.watchface.complications.data.ComplicationExperimental public final class ProtoLayoutComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
-    method public androidx.wear.tiles.LayoutElementBuilders.Layout getAmbientLayout();
-    method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
-    method public androidx.wear.tiles.LayoutElementBuilders.Layout getInteractiveLayout();
-    method public androidx.wear.tiles.ResourceBuilders.Resources getLayoutResources();
-    property public final androidx.wear.tiles.LayoutElementBuilders.Layout ambientLayout;
-    property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
-    property public final androidx.wear.tiles.LayoutElementBuilders.Layout interactiveLayout;
-    property public final androidx.wear.tiles.ResourceBuilders.Resources layoutResources;
-    field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
-  }
-
-  public static final class ProtoLayoutComplicationData.Builder {
-    ctor public ProtoLayoutComplicationData.Builder(byte[] ambientLayout, byte[] interactiveLayout, byte[] layoutResources, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
-    ctor public ProtoLayoutComplicationData.Builder(androidx.wear.tiles.LayoutElementBuilders.Layout ambientLayout, androidx.wear.tiles.LayoutElementBuilders.Layout interactiveLayout, androidx.wear.tiles.ResourceBuilders.Resources resources, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
-    method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData build();
-    method public final T setDataSource(android.content.ComponentName? dataSource);
-    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
-    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
-    method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
-    method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
-  }
-
   public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
-    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
+    method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
     method public float getMax();
     method public float getMin();
@@ -325,8 +272,8 @@
     method public androidx.wear.watchface.complications.data.ComplicationText? getText();
     method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
     method public float getValue();
-    method @androidx.wear.watchface.complications.data.ComplicationExperimental public int getValueType();
-    property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
+    method public int getValueType();
+    property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
     property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
     property public final float max;
     property public final float min;
@@ -335,7 +282,7 @@
     property public final androidx.wear.watchface.complications.data.ComplicationText? text;
     property public final androidx.wear.watchface.complications.data.ComplicationText? title;
     property public final float value;
-    property @androidx.wear.watchface.complications.data.ComplicationExperimental public final int valueType;
+    property public final int valueType;
     field public static final float PLACEHOLDER;
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
@@ -343,7 +290,7 @@
   public static final class RangedValueComplicationData.Builder {
     ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
-    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
+    method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
     method public final T setDataSource(android.content.ComponentName? dataSource);
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -353,10 +300,10 @@
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
-    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
+    method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
   }
 
-  @androidx.wear.watchface.complications.data.ComplicationExperimental public final class RangedValueTypes {
+  public final class RangedValueTypes {
     field public static final androidx.wear.watchface.complications.data.RangedValueTypes INSTANCE;
     field public static final int SCORE = 1; // 0x1
     field public static final int UNDEFINED = 0; // 0x0
@@ -506,7 +453,7 @@
   public final class TypeKt {
   }
 
-  @androidx.wear.watchface.complications.data.ComplicationExperimental public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
     method public int getElementBackgroundColor();
     method public java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> getElements();
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index e27cc62..4f6ce72 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -1,6 +1,14 @@
 // Signature format: 4.0
 package androidx.wear.watchface.complications.data {
 
+  public final class ColorRamp {
+    ctor public ColorRamp(@ColorInt int[] colors, boolean interpolated);
+    method public int[] getColors();
+    method public boolean getInterpolated();
+    property public final int[] colors;
+    property public final boolean interpolated;
+  }
+
   public abstract sealed class ComplicationData {
     method public final android.content.ComponentName? getDataSource();
     method public final int getDisplayPolicy();
@@ -50,6 +58,7 @@
     method public static androidx.wear.watchface.complications.data.ComplicationType valueOf(String name) throws java.lang.IllegalArgumentException;
     method public static androidx.wear.watchface.complications.data.ComplicationType[] values();
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType EMPTY;
+    enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType LONG_TEXT;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType MONOCHROMATIC_IMAGE;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NOT_CONFIGURED;
@@ -59,6 +68,7 @@
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType RANGED_VALUE;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SHORT_TEXT;
     enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SMALL_IMAGE;
+    enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType WEIGHTED_ELEMENTS;
   }
 
   public final class CountDownTimeReference {
@@ -81,6 +91,42 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+    method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+    method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
+    method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
+    method public float getTargetValue();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getText();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
+    method public float getValue();
+    property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+    property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
+    property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
+    property public final float targetValue;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? text;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+    property public final float value;
+    field public static final float PLACEHOLDER;
+    field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
+  }
+
+  public static final class GoalProgressComplicationData.Builder {
+    ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
+    method public final T setDataSource(android.content.ComponentName? dataSource);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
+    method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+  }
+
   public final class ImageKt {
   }
 
@@ -215,6 +261,7 @@
   }
 
   public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+    method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
     method public float getMax();
     method public float getMin();
@@ -223,6 +270,8 @@
     method public androidx.wear.watchface.complications.data.ComplicationText? getText();
     method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
     method public float getValue();
+    method public int getValueType();
+    property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
     property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
     property public final float max;
     property public final float min;
@@ -231,6 +280,7 @@
     property public final androidx.wear.watchface.complications.data.ComplicationText? text;
     property public final androidx.wear.watchface.complications.data.ComplicationText? title;
     property public final float value;
+    property public final int valueType;
     field public static final float PLACEHOLDER;
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
@@ -238,6 +288,7 @@
   public static final class RangedValueComplicationData.Builder {
     ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
+    method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
     method public final T setDataSource(android.content.ComponentName? dataSource);
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -247,6 +298,13 @@
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+    method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
+  }
+
+  public final class RangedValueTypes {
+    field public static final androidx.wear.watchface.complications.data.RangedValueTypes INSTANCE;
+    field public static final int SCORE = 1; // 0x1
+    field public static final int UNDEFINED = 0; // 0x0
   }
 
   public final class ShortTextComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
@@ -395,6 +453,53 @@
   public final class TypeKt {
   }
 
+  @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+    method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+    method public int getElementBackgroundColor();
+    method public java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> getElements();
+    method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
+    method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getText();
+    method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
+    property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+    property public final int elementBackgroundColor;
+    property public final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements;
+    property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
+    property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? text;
+    property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+    field public static final androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Companion Companion;
+    field public static final int MAX_NUM_ELEMENTS = 20; // 0x14
+    field public static final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> PLACEHOLDER;
+    field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
+  }
+
+  public static final class WeightedElementsComplicationData.Builder {
+    ctor public WeightedElementsComplicationData.Builder(java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData build();
+    method public final T setDataSource(android.content.ComponentName? dataSource);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setElementBackgroundColor(@ColorInt int elementBackgroundColor);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
+    method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
+    method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+  }
+
+  public static final class WeightedElementsComplicationData.Companion {
+  }
+
+  public static final class WeightedElementsComplicationData.Element {
+    ctor public WeightedElementsComplicationData.Element(@FloatRange(from=0.0, fromInclusive=false) float weight, @ColorInt int color);
+    method public int getColor();
+    method public float getWeight();
+    property public final int color;
+    property public final float weight;
+  }
+
 }
 
 package androidx.wear.watchface.utility {
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index c574d08..a9e5904 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -33,7 +33,6 @@
     implementation("androidx.core:core:1.1.0")
     implementation("androidx.preference:preference:1.1.0")
     implementation("androidx.annotation:annotation:1.2.0")
-    implementation("androidx.wear.tiles:tiles:1.1.0-alpha08")
     testImplementation(libs.testCore)
     testImplementation(libs.testRunner)
     testImplementation(libs.testRules)
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
index f174b51..5c954de 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -93,6 +93,8 @@
             TYPE_LARGE_IMAGE,
             TYPE_NO_PERMISSION,
             TYPE_NO_DATA,
+            TYPE_GOAL_PROGRESS,
+            TYPE_WEIGHTED_ELEMENTS,
             EXP_TYPE_PROTO_LAYOUT,
             EXP_TYPE_LIST
     })
@@ -230,14 +232,6 @@
      */
     public static final int TYPE_NO_PERMISSION = 9;
 
-    // The following types are experimental, and they have negative IDs.
-
-    /** Type that specifies a proto layout based complication. */
-    public static final int EXP_TYPE_PROTO_LAYOUT = -11;
-
-    /** Type that specifies a list of complication values. E.g. to support linear 3. */
-    public static final int EXP_TYPE_LIST = -12;
-
     /**
      * Type used for complications which indicate progress towards a goal. The value may be
      * accompanied by an icon and/or short text and title.
@@ -250,7 +244,7 @@
      * text</i> fields are optional for this type, but at least one must be defined. The watch face
      * may choose which of these fields to display, if any.
      */
-    public static final int EXP_TYPE_GOAL_PROGRESS = -13;
+    public static final int TYPE_GOAL_PROGRESS = 13;
 
     /**
      * Type used for complications to display a series of weighted values e.g. in a pie chart. The
@@ -263,7 +257,15 @@
      * text</i> fields are optional for this type, but at least one must be defined. The watch face
      * may choose which of these fields to display, if any.
      */
-    public static final int EXP_TYPE_WEIGHTED_ELEMENTS = -14;
+    public static final int TYPE_WEIGHTED_ELEMENTS = 14;
+
+    // The following types are experimental, and they have negative IDs.
+
+    /** Type that specifies a proto layout based complication. */
+    public static final int EXP_TYPE_PROTO_LAYOUT = -11;
+
+    /** Type that specifies a list of complication values. E.g. to support linear 3. */
+    public static final int EXP_TYPE_LIST = -12;
 
     /** @hide */
     @IntDef({IMAGE_STYLE_PHOTO, IMAGE_STYLE_ICON})
@@ -289,8 +291,13 @@
      */
     public static final int IMAGE_STYLE_ICON = 2;
 
+    private static final String FIELD_COLOR_RAMP = "COLOR_RAMP";
+    private static final String FIELD_COLOR_RAMP_INTERPOLATED = "COLOR_RAMP_INTERPOLATED";
     private static final String FIELD_DATA_SOURCE = "FIELD_DATA_SOURCE";
     private static final String FIELD_DISPLAY_POLICY = "DISPLAY_POLICY";
+    private static final String FIELD_ELEMENT_BACKGROUND_COLOR = "ELEMENT_BACKGROUND_COLOR";
+    private static final String FIELD_ELEMENT_COLORS = "ELEMENT_COLORS";
+    private static final String FIELD_ELEMENT_WEIGHTS = "ELEMENT_WEIGHTS";
     private static final String FIELD_END_TIME = "END_TIME";
     private static final String FIELD_ICON = "ICON";
     private static final String FIELD_ICON_BURN_IN_PROTECTION = "ICON_BURN_IN_PROTECTION";
@@ -311,6 +318,7 @@
     private static final String FIELD_START_TIME = "START_TIME";
     private static final String FIELD_TAP_ACTION = "TAP_ACTION";
     private static final String FIELD_TAP_ACTION_LOST = "FIELD_TAP_ACTION_LOST";
+    private static final String FIELD_TARGET_VALUE = "TARGET_VALUE";
     private static final String FIELD_TIMELINE_START_TIME = "TIMELINE_START_TIME";
     private static final String FIELD_TIMELINE_END_TIME = "TIMELINE_END_TIME";
     private static final String FIELD_TIMELINE_ENTRIES = "TIMELINE";
@@ -319,11 +327,6 @@
     private static final String FIELD_VALUE_TYPE = "VALUE_TYPE";
 
     // Experimental fields, these are subject to change without notice.
-    private static final String EXP_FIELD_COLOR_RAMP = "EXP_COLOR_RAMP";
-    private static final String EXP_FIELD_COLOR_RAMP_INTERPOLATED = "EXP_COLOR_RAMP_INTERPOLATED";
-    private static final String EXP_FIELD_ELEMENT_BACKGROUND_COLOR = "EXP_ELEMENT_BACKGROUND_COLOR";
-    private static final String EXP_FIELD_ELEMENT_COLORS = "EXP_ELEMENT_COLORS";
-    private static final String EXP_FIELD_ELEMENT_WEIGHTS = "EXP_ELEMENT_WEIGHTS";
     private static final String EXP_FIELD_LIST_ENTRIES = "EXP_LIST_ENTRIES";
     private static final String EXP_FIELD_LIST_ENTRY_TYPE = "EXP_LIST_ENTRY_TYPE";
     private static final String EXP_FIELD_LIST_STYLE_HINT = "EXP_LIST_STYLE_HINT";
@@ -332,7 +335,6 @@
             "EXP_FIELD_PROTO_LAYOUT_INTERACTIVE";
     private static final String EXP_FIELD_PROTO_LAYOUT_RESOURCES =
             "EXP_FIELD_PROTO_LAYOUT_RESOURCES";
-    private static final String EXP_FIELD_TARGET_VALUE = "EXP_TARGET_VALUE";
 
     // Originally it was planned to support both content and image content descriptions.
     private static final String FIELD_CONTENT_DESCRIPTION = "IMAGE_CONTENT_DESCRIPTION";
@@ -354,8 +356,8 @@
         set.add(TYPE_NO_DATA);
         set.add(EXP_TYPE_PROTO_LAYOUT);
         set.add(EXP_TYPE_LIST);
-        set.add(EXP_TYPE_GOAL_PROGRESS);
-        set.add(EXP_TYPE_WEIGHTED_ELEMENTS);
+        set.add(TYPE_GOAL_PROGRESS);
+        set.add(TYPE_WEIGHTED_ELEMENTS);
         return set;
     }
 
@@ -379,10 +381,10 @@
                 EXP_FIELD_PROTO_LAYOUT_INTERACTIVE,
                 EXP_FIELD_PROTO_LAYOUT_RESOURCES});
         map.put(EXP_TYPE_LIST, new String[]{EXP_FIELD_LIST_ENTRIES});
-        map.put(EXP_TYPE_GOAL_PROGRESS, new String[]{FIELD_VALUE, EXP_FIELD_TARGET_VALUE});
-        map.put(EXP_TYPE_WEIGHTED_ELEMENTS, new String[]{EXP_FIELD_ELEMENT_WEIGHTS,
-                EXP_FIELD_ELEMENT_COLORS,
-                EXP_FIELD_ELEMENT_BACKGROUND_COLOR});
+        map.put(TYPE_GOAL_PROGRESS, new String[]{FIELD_VALUE, FIELD_TARGET_VALUE});
+        map.put(TYPE_WEIGHTED_ELEMENTS, new String[]{FIELD_ELEMENT_WEIGHTS,
+                FIELD_ELEMENT_COLORS,
+                FIELD_ELEMENT_BACKGROUND_COLOR});
         return map;
     }
 
@@ -429,8 +431,8 @@
                 FIELD_TAP_ACTION,
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
-                EXP_FIELD_COLOR_RAMP,
-                EXP_FIELD_COLOR_RAMP_INTERPOLATED,
+                FIELD_COLOR_RAMP,
+                FIELD_COLOR_RAMP_INTERPOLATED,
                 FIELD_PERSISTENCE_POLICY,
                 FIELD_DISPLAY_POLICY,
                 FIELD_VALUE_TYPE});
@@ -503,7 +505,7 @@
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
                 FIELD_DISPLAY_POLICY});
-        map.put(EXP_TYPE_GOAL_PROGRESS, new String[]{
+        map.put(TYPE_GOAL_PROGRESS, new String[]{
                 FIELD_SHORT_TEXT,
                 FIELD_SHORT_TITLE,
                 FIELD_ICON,
@@ -514,11 +516,11 @@
                 FIELD_TAP_ACTION,
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
-                EXP_FIELD_COLOR_RAMP,
-                EXP_FIELD_COLOR_RAMP_INTERPOLATED,
+                FIELD_COLOR_RAMP,
+                FIELD_COLOR_RAMP_INTERPOLATED,
                 FIELD_PERSISTENCE_POLICY,
                 FIELD_DISPLAY_POLICY});
-        map.put(EXP_TYPE_WEIGHTED_ELEMENTS, new String[]{
+        map.put(TYPE_WEIGHTED_ELEMENTS, new String[]{
                 FIELD_SHORT_TEXT,
                 FIELD_SHORT_TITLE,
                 FIELD_ICON,
@@ -640,10 +642,10 @@
             if (isFieldValidForType(FIELD_MAX_VALUE, type)) {
                 oos.writeFloat(mComplicationData.getRangedMaxValue());
             }
-            if (isFieldValidForType(EXP_FIELD_TARGET_VALUE, type)) {
+            if (isFieldValidForType(FIELD_TARGET_VALUE, type)) {
                 oos.writeFloat(mComplicationData.getTargetValue());
             }
-            if (isFieldValidForType(EXP_FIELD_COLOR_RAMP, type)) {
+            if (isFieldValidForType(FIELD_COLOR_RAMP, type)) {
                 int[] colors = mComplicationData.getColorRamp();
                 if (colors != null) {
                     oos.writeBoolean(true);
@@ -655,7 +657,7 @@
                     oos.writeBoolean(false);
                 }
             }
-            if (isFieldValidForType(EXP_FIELD_COLOR_RAMP_INTERPOLATED, type)) {
+            if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type)) {
                 Boolean isColorRampSmoothShaded = mComplicationData.isColorRampInterpolated();
                 if (isColorRampSmoothShaded != null) {
                     oos.writeBoolean(true);
@@ -664,7 +666,7 @@
                     oos.writeBoolean(false);
                 }
             }
-            if (isFieldValidForType(EXP_FIELD_ELEMENT_WEIGHTS, type)) {
+            if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type)) {
                 float[] weights = mComplicationData.getElementWeights();
                 if (weights != null) {
                     oos.writeBoolean(true);
@@ -676,7 +678,7 @@
                     oos.writeBoolean(false);
                 }
             }
-            if (isFieldValidForType(EXP_FIELD_ELEMENT_COLORS, type)) {
+            if (isFieldValidForType(FIELD_ELEMENT_COLORS, type)) {
                 int[] colors = mComplicationData.getElementColors();
                 if (colors != null) {
                     oos.writeBoolean(true);
@@ -688,7 +690,7 @@
                     oos.writeBoolean(false);
                 }
             }
-            if (isFieldValidForType(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
+            if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
                 oos.writeInt(mComplicationData.getElementBackgroundColor());
             }
             if (isFieldValidForType(FIELD_START_TIME, type)) {
@@ -842,38 +844,38 @@
             if (isFieldValidForType(FIELD_MAX_VALUE, type)) {
                 fields.putFloat(FIELD_MAX_VALUE, ois.readFloat());
             }
-            if (isFieldValidForType(EXP_FIELD_TARGET_VALUE, type)) {
-                fields.putFloat(EXP_FIELD_TARGET_VALUE, ois.readFloat());
+            if (isFieldValidForType(FIELD_TARGET_VALUE, type)) {
+                fields.putFloat(FIELD_TARGET_VALUE, ois.readFloat());
             }
-            if (isFieldValidForType(EXP_FIELD_COLOR_RAMP, type) && ois.readBoolean()) {
+            if (isFieldValidForType(FIELD_COLOR_RAMP, type) && ois.readBoolean()) {
                 int numColors = ois.readInt();
                 int[] colors = new int[numColors];
                 for (int i = 0; i < numColors; ++i) {
                     colors[i] = ois.readInt();
                 }
-                fields.putIntArray(EXP_FIELD_COLOR_RAMP, colors);
+                fields.putIntArray(FIELD_COLOR_RAMP, colors);
             }
-            if (isFieldValidForType(EXP_FIELD_COLOR_RAMP_INTERPOLATED, type) && ois.readBoolean()) {
-                fields.putBoolean(EXP_FIELD_COLOR_RAMP_INTERPOLATED, ois.readBoolean());
+            if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type) && ois.readBoolean()) {
+                fields.putBoolean(FIELD_COLOR_RAMP_INTERPOLATED, ois.readBoolean());
             }
-            if (isFieldValidForType(EXP_FIELD_ELEMENT_WEIGHTS, type) && ois.readBoolean()) {
+            if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type) && ois.readBoolean()) {
                 int numWeights = ois.readInt();
                 float[] weights = new float[numWeights];
                 for (int i = 0; i < numWeights; ++i) {
                     weights[i] = ois.readFloat();
                 }
-                fields.putFloatArray(EXP_FIELD_ELEMENT_WEIGHTS, weights);
+                fields.putFloatArray(FIELD_ELEMENT_WEIGHTS, weights);
             }
-            if (isFieldValidForType(EXP_FIELD_ELEMENT_COLORS, type) && ois.readBoolean()) {
+            if (isFieldValidForType(FIELD_ELEMENT_COLORS, type) && ois.readBoolean()) {
                 int numColors = ois.readInt();
                 int[] colors = new int[numColors];
                 for (int i = 0; i < numColors; ++i) {
                     colors[i] = ois.readInt();
                 }
-                fields.putIntArray(EXP_FIELD_ELEMENT_COLORS, colors);
+                fields.putIntArray(FIELD_ELEMENT_COLORS, colors);
             }
-            if (isFieldValidForType(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
-                fields.putInt(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, ois.readInt());
+            if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
+                fields.putInt(FIELD_ELEMENT_BACKGROUND_COLOR, ois.readInt());
             }
             if (isFieldValidForType(FIELD_START_TIME, type)) {
                 fields.putLong(FIELD_START_TIME, ois.readLong());
@@ -1253,7 +1255,7 @@
      */
     public boolean hasTargetValue() {
         try {
-            return isFieldValidForType(EXP_FIELD_TARGET_VALUE, mType);
+            return isFieldValidForType(FIELD_TARGET_VALUE, mType);
         } catch (BadParcelableException e) {
             return false;
         }
@@ -1262,26 +1264,26 @@
     /**
      * Returns the <i>value</i> field for this complication.
      *
-     * <p>Valid only if the type of this complication data is {@link #EXP_TYPE_GOAL_PROGRESS}.
+     * <p>Valid only if the type of this complication data is {@link #TYPE_GOAL_PROGRESS}.
      * Otherwise returns zero.
      */
     public float getTargetValue() {
-        checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_TARGET_VALUE, mType);
-        return mFields.getFloat(EXP_FIELD_TARGET_VALUE);
+        checkFieldValidForTypeWithoutThrowingException(FIELD_TARGET_VALUE, mType);
+        return mFields.getFloat(FIELD_TARGET_VALUE);
     }
 
     /**
      * Returns the colors for the progress bar.
      *
      * <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE} or
-     * {@link #EXP_TYPE_GOAL_PROGRESS}.
+     * {@link #TYPE_GOAL_PROGRESS}.
      */
     @ColorInt
     @Nullable
     public int[] getColorRamp() {
-        checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_COLOR_RAMP, mType);
-        if (mFields.containsKey(EXP_FIELD_COLOR_RAMP)) {
-            return mFields.getIntArray(EXP_FIELD_COLOR_RAMP);
+        checkFieldValidForTypeWithoutThrowingException(FIELD_COLOR_RAMP, mType);
+        if (mFields.containsKey(FIELD_COLOR_RAMP)) {
+            return mFields.getIntArray(FIELD_COLOR_RAMP);
         }
         return null;
     }
@@ -1290,16 +1292,16 @@
      * Returns either a boolean where: true means the color ramp colors should be smoothly
      * interpolatded; false means the color ramp should be rendered in equal sized blocks of
      * solid color; null means this value wasn't set, i.e. the complication is not of type
-     * {@link #TYPE_RANGED_VALUE} or {@link #EXP_TYPE_GOAL_PROGRESS}.
+     * {@link #TYPE_RANGED_VALUE} or {@link #TYPE_GOAL_PROGRESS}.
      *
      * <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE} or
-     * {@link #EXP_TYPE_GOAL_PROGRESS}.
+     * {@link #TYPE_GOAL_PROGRESS}.
      */
     @Nullable
     public Boolean isColorRampInterpolated() {
-        checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_COLOR_RAMP_INTERPOLATED, mType);
-        if (mFields.containsKey(EXP_FIELD_COLOR_RAMP_INTERPOLATED)) {
-            return mFields.getBoolean(EXP_FIELD_COLOR_RAMP_INTERPOLATED);
+        checkFieldValidForTypeWithoutThrowingException(FIELD_COLOR_RAMP_INTERPOLATED, mType);
+        if (mFields.containsKey(FIELD_COLOR_RAMP_INTERPOLATED)) {
+            return mFields.getBoolean(FIELD_COLOR_RAMP_INTERPOLATED);
         }
         return null;
     }
@@ -1683,37 +1685,37 @@
     /**
      * Returns the element weights for this complication.
      *
-     * <p>Valid only if the type of this complication data is {@link #EXP_TYPE_WEIGHTED_ELEMENTS}.
+     * <p>Valid only if the type of this complication data is {@link #TYPE_WEIGHTED_ELEMENTS}.
      * Otherwise returns null.
      */
     @Nullable
     public float[] getElementWeights() {
-        checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_ELEMENT_WEIGHTS, mType);
-        return mFields.getFloatArray(EXP_FIELD_ELEMENT_WEIGHTS);
+        checkFieldValidForTypeWithoutThrowingException(FIELD_ELEMENT_WEIGHTS, mType);
+        return mFields.getFloatArray(FIELD_ELEMENT_WEIGHTS);
     }
 
     /**
      * Returns the element colors for this complication.
      *
-     * <p>Valid only if the type of this complication data is {@link #EXP_TYPE_WEIGHTED_ELEMENTS}.
+     * <p>Valid only if the type of this complication data is {@link #TYPE_WEIGHTED_ELEMENTS}.
      * Otherwise returns null.
      */
     @Nullable
     public int[] getElementColors() {
-        checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_ELEMENT_COLORS, mType);
-        return mFields.getIntArray(EXP_FIELD_ELEMENT_COLORS);
+        checkFieldValidForTypeWithoutThrowingException(FIELD_ELEMENT_COLORS, mType);
+        return mFields.getIntArray(FIELD_ELEMENT_COLORS);
     }
 
     /**
      * Returns the background color to use between elements for this complication.
      *
-     * <p>Valid only if the type of this complication data is {@link #EXP_TYPE_WEIGHTED_ELEMENTS}.
+     * <p>Valid only if the type of this complication data is {@link #TYPE_WEIGHTED_ELEMENTS}.
      * Otherwise returns 0.
      */
     @ColorInt
     public int getElementBackgroundColor() {
-        checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, mType);
-        return mFields.getInt(EXP_FIELD_ELEMENT_BACKGROUND_COLOR);
+        checkFieldValidForTypeWithoutThrowingException(FIELD_ELEMENT_BACKGROUND_COLOR, mType);
+        return mFields.getInt(FIELD_ELEMENT_BACKGROUND_COLOR);
     }
 
     /**
@@ -1978,12 +1980,12 @@
 
         /**
          * Sets the <i>value</i> field. This is required for the {@link #TYPE_RANGED_VALUE} type,
-         * and the {@link #EXP_TYPE_GOAL_PROGRESS} type. For {@link #TYPE_RANGED_VALUE} value must
-         * be in the range [min .. max] for {@link #EXP_TYPE_GOAL_PROGRESS} value must be >= and may
+         * and the {@link #TYPE_GOAL_PROGRESS} type. For {@link #TYPE_RANGED_VALUE} value must
+         * be in the range [min .. max] for {@link #TYPE_GOAL_PROGRESS} value must be >= and may
          * be greater than target value.
          *
          * <p>Both the {@link #TYPE_RANGED_VALUE} complication and the
-         * {@link #EXP_TYPE_GOAL_PROGRESS} complication visually present a single value, which is
+         * {@link #TYPE_GOAL_PROGRESS} complication visually present a single value, which is
          * usually a percentage. E.g. you have completed 70% of today's  target of 10000 steps.
          *
          * <p>Returns this Builder to allow chaining.
@@ -2040,8 +2042,8 @@
 
         /**
          * Sets the <i>targetValue</i> field. This is required for the
-         * {@link #EXP_TYPE_GOAL_PROGRESS} type, and is not valid for any other type. A
-         * {@link #EXP_TYPE_GOAL_PROGRESS} complication visually presents a single value, which
+         * {@link #TYPE_GOAL_PROGRESS} type, and is not valid for any other type. A
+         * {@link #TYPE_GOAL_PROGRESS} complication visually presents a single value, which
          * is usually a percentage. E.g. you have completed 70% of today's target of 10000 steps.
          *
          * <p>Returns this Builder to allow chaining.
@@ -2050,7 +2052,7 @@
          */
         @NonNull
         public Builder setTargetValue(float targetValue) {
-            putFloatField(EXP_FIELD_TARGET_VALUE, targetValue);
+            putFloatField(FIELD_TARGET_VALUE, targetValue);
             return this;
         }
 
@@ -2377,7 +2379,7 @@
          */
         @NonNull
         public Builder setColorRamp(@Nullable int[] colorRamp) {
-            putOrRemoveField(EXP_FIELD_COLOR_RAMP, colorRamp);
+            putOrRemoveField(FIELD_COLOR_RAMP, colorRamp);
             return this;
         }
 
@@ -2389,7 +2391,7 @@
          */
         @NonNull
         public Builder setColorRampIsSmoothShaded(@Nullable Boolean isSmoothShaded) {
-            putOrRemoveField(EXP_FIELD_COLOR_RAMP_INTERPOLATED, isSmoothShaded);
+            putOrRemoveField(FIELD_COLOR_RAMP_INTERPOLATED, isSmoothShaded);
             return this;
         }
 
@@ -2424,7 +2426,7 @@
          */
         @NonNull
         public Builder setElementWeights(@Nullable float[] elementWeights) {
-            putOrRemoveField(EXP_FIELD_ELEMENT_WEIGHTS, elementWeights);
+            putOrRemoveField(FIELD_ELEMENT_WEIGHTS, elementWeights);
             return this;
         }
 
@@ -2435,7 +2437,7 @@
          */
         @NonNull
         public Builder setElementColors(@Nullable int[] elementColors) {
-            putOrRemoveField(EXP_FIELD_ELEMENT_COLORS, elementColors);
+            putOrRemoveField(FIELD_ELEMENT_COLORS, elementColors);
             return this;
         }
 
@@ -2446,7 +2448,7 @@
          */
         @NonNull
         public Builder setElementBackgroundColor(@ColorInt int elementBackgroundColor) {
-            putOrRemoveField(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, elementBackgroundColor);
+            putOrRemoveField(FIELD_ELEMENT_BACKGROUND_COLOR, elementBackgroundColor);
             return this;
         }
 
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index bdf8831..717fa98 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -27,8 +27,6 @@
 import androidx.annotation.FloatRange
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
-import androidx.wear.tiles.LayoutElementBuilders
-import androidx.wear.tiles.ResourceBuilders
 import java.time.Instant
 
 /** The wire format for [ComplicationData]. */
@@ -267,7 +265,6 @@
         null
     )
 
-    @OptIn(ComplicationExperimental::class)
     val contentDescription: ComplicationText? =
         when (placeholder) {
             is ShortTextComplicationData -> placeholder.contentDescription
@@ -859,16 +856,19 @@
  * overrides the normal watch face colors when there's a particular semantic meaning. E.g. red to
  * blue for a ranged value representing temperature.
  *
+ * Note this is a subset of the functionality of [android.graphics.LinearGradient] and the x & y
+ * coordinates for the ramp are not known to the complication data source.
+ *
  * @property colors The colors to render the progress bar with. For [RangedValueComplicationData]
  * the first color corresponds to [RangedValueComplicationData.min] and the last color to
  * [RangedValueComplicationData.max]. For [GoalProgressComplicationData] the first color corresponds
  * to zero and the last color to [GoalProgressComplicationData.targetValue]. A maximum of 7 colors
- * may be specified. When rendered the colors must be evenly spread along the progress bar.
+ * may be specified. When rendered the colors must be evenly spread along the progress bar. The
+ * colors must be meaningful to the user, e.g. blue = cold, red/yellow = warm.
  * @property interpolated If `true` then the colors should be smoothly interpolated when rendering
  * the progress bar. If `false` the colors should be rendered as equal sized regions of solid color,
  * resulting in a noticeable step between each color.
  */
-@ComplicationExperimental
 public class ColorRamp(
     @ColorInt val colors: IntArray,
     @get:JvmName("isInterpolated")
@@ -908,7 +908,6 @@
  * renderer may wish to visually differentiate between the different types, for example rendering a
  * dot on a line/arc to show the value for a [SCORE].
  */
-@ComplicationExperimental
 object RangedValueTypes {
     /** The ranged value's semantic hasn't been defined, but it's most commonly a percentage. */
     const val UNDEFINED = 0
@@ -921,7 +920,6 @@
 }
 
 /** @hide */
-@OptIn(ComplicationExperimental::class)
 @IntDef(
     value = [
         RangedValueTypes.UNDEFINED,
@@ -977,9 +975,11 @@
  * placeholder rather than rendering normally, its suggested it should be rendered as a light grey
  * box.
  * @property contentDescription The content description field for accessibility.
+ * @property colorRamp Optional hint to render the value with the specified [ColorRamp]. When
+ * present the renderer may choose to use the ColorRamp when rendering the progress bar.
+ * @property valueType The semantic meaning of [value], see [RangedValueType] for more details.
  */
-public class RangedValueComplicationData @OptIn(ComplicationExperimental::class)
-internal constructor(
+public class RangedValueComplicationData internal constructor(
     public val value: Float,
     public val min: Float,
     public val max: Float,
@@ -992,8 +992,8 @@
     validTimeRange: TimeRange?,
     cachedWireComplicationData: WireComplicationData?,
     dataSource: ComponentName?,
-    colorRamp: ColorRamp?,
-    @RangedValueType valueType: Int,
+    public val colorRamp: ColorRamp?,
+    @RangedValueType public val valueType: Int,
     @ComplicationPersistencePolicy persistencePolicy: Int,
     @ComplicationDisplayPolicy displayPolicy: Int
 ) : ComplicationData(
@@ -1005,19 +1005,6 @@
     persistencePolicy = persistencePolicy,
     displayPolicy = displayPolicy
 ) {
-    /** Optional hint to render the value with the specified [ColorRamp]. */
-    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @get:ComplicationExperimental
-    @ComplicationExperimental
-    val colorRamp: ColorRamp? = colorRamp
-
-    /** The semantic meaning of [value], see [RangedValueType] for more details. */
-    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @get:ComplicationExperimental
-    @ComplicationExperimental
-    @RangedValueType
-    val valueType: Int = valueType
-
     /**
      * Builder for [RangedValueComplicationData].
      *
@@ -1030,7 +1017,6 @@
      * @param max The maximum value. This must be less than [Float.MAX_VALUE].
      * @param contentDescription Localized description for use by screen readers
      */
-    @OptIn(ComplicationExperimental::class)
     public class Builder(
         private val value: Float,
         private val min: Float,
@@ -1043,9 +1029,7 @@
         private var smallImage: SmallImage? = null
         private var title: ComplicationText? = null
         private var text: ComplicationText? = null
-        @OptIn(ComplicationExperimental::class)
         private var colorRamp: ColorRamp? = null
-        @OptIn(ComplicationExperimental::class)
         @RangedValueType
         private var valueType: Int = RangedValueTypes.UNDEFINED
 
@@ -1087,25 +1071,23 @@
         }
 
         /**
-         * Sets an optional hint which suggests the renderer draws the complication using a
+         * Sets an optional hint that the renderer should draw the progress bar using the
          * [ColorRamp].
          */
-        @ComplicationExperimental
         public fun setColorRamp(colorRamp: ColorRamp?): Builder = apply {
             this.colorRamp = colorRamp
         }
 
         /**
-         * Sets the semantic meaning of [value], see [@RangedValueTypes] for detail.s Defaults to
-         * [RangedValueTypes.UNDEFINED] if not set.
+         * Sets the semantic meaning of [value], see [@RangedValueTypes] for details. Defaults to
+         * [RangedValueTypes.UNDEFINED] if not set. Watch faces may use this as a hint to select
+         * rendering style.
          */
-        @ComplicationExperimental
         public fun setValueType(@RangedValueType valueType: Int): Builder = apply {
             this.valueType = valueType
         }
 
         /** Builds the [RangedValueComplicationData]. */
-        @OptIn(ComplicationExperimental::class)
         public override fun build(): RangedValueComplicationData {
             require(
                 monochromaticImage != null || smallImage != null || text != null || title != null
@@ -1144,7 +1126,6 @@
         }.build().also { cachedWireComplicationData = it }
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
         builder.setRangedValue(value)
         builder.setRangedMinValue(min)
@@ -1169,7 +1150,6 @@
         builder.setRangedValueType(valueType)
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (javaClass != other?.javaClass) return false
@@ -1196,7 +1176,6 @@
         return true
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun hashCode(): Int {
         var result = value.hashCode()
         result = 31 * result + valueType
@@ -1217,7 +1196,6 @@
         return result
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun toString(): String {
         val valueString = if (WireComplicationData.shouldRedact()) {
             "REDACTED"
@@ -1267,10 +1245,12 @@
 }
 
 /**
- * Type used for complications which show progress towards a goal, E.g. you've done 2400 out of your
- * daily target of 10000 steps. Unlike [RangedValueComplicationData] [value] is allowed to be larger
- * than [targetValue] (e.g. you've done 12000 steps) and renderers may chose to acknowledge this in
- * a special way. The value may be accompanied by an icon and/or short text and title.
+ * Type used for complications which shows the user's progress towards a goal, E.g. you've done 2400
+ * out of your daily target of 10000 steps. Unlike [RangedValueComplicationData] [value] is allowed
+ * to be larger than [targetValue] (e.g. you've done 12000 steps) and renderers may chose to
+ * acknowledge this in a special way (e.g. by colorizing part of the progress bar in a different
+ * color to indicate progress past the goal). The value may be accompanied by an icon and/or short
+ * text and title.
  *
  * The [value], and [targetValue] fields are required for this type and the progress is expected to
  * always be displayed.
@@ -1285,8 +1265,8 @@
  * specify both a [monochromaticImage] and a [smallImage].
  *
  * If you want to represent a score for something that's not based on the user (e.g. air quality
- * index) then you should use a [RangedValueComplicationData] and pass [RangedValueTypes.SCORE] into
- * [RangedValueComplicationData.Builder.setValueType].
+ * index) then you should instead use a [RangedValueComplicationData] and pass
+ * [RangedValueTypes.SCORE] into [RangedValueComplicationData.Builder.setValueType].
  *
  * @property value The [Float] value of this complication which is >= 0f, this value may be larger
  * than [targetValue]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder
@@ -1318,8 +1298,10 @@
  * placeholder rather than rendering normally, its suggested it should be rendered as a light grey
  * box.
  * @property contentDescription The content description field for accessibility.
+ * @property colorRamp Optional hint to render the progress bar representing [value] with the
+ * specified [ColorRamp].
  */
-@ComplicationExperimental
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class GoalProgressComplicationData
 internal constructor(
     public val value: Float,
@@ -1333,7 +1315,7 @@
     validTimeRange: TimeRange?,
     cachedWireComplicationData: WireComplicationData?,
     dataSource: ComponentName?,
-    colorRamp: ColorRamp?,
+    public val colorRamp: ColorRamp?,
     @ComplicationPersistencePolicy persistencePolicy: Int,
     @ComplicationDisplayPolicy displayPolicy: Int
 ) : ComplicationData(
@@ -1345,12 +1327,6 @@
     persistencePolicy = persistencePolicy,
     displayPolicy = displayPolicy
 ) {
-    /** Optional hint to render the value with the specified [ColorRamp]. */
-    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @get:ComplicationExperimental
-    @ComplicationExperimental
-    val colorRamp: ColorRamp? = colorRamp
-
     /**
      * Builder for [GoalProgressComplicationData].
      *
@@ -1361,7 +1337,6 @@
      * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
      * @param contentDescription Localized description for use by screen readers
      */
-    @OptIn(ComplicationExperimental::class)
     public class Builder(
         private val value: Float,
         private val targetValue: Float,
@@ -1373,7 +1348,6 @@
         private var smallImage: SmallImage? = null
         private var title: ComplicationText? = null
         private var text: ComplicationText? = null
-        @OptIn(ComplicationExperimental::class)
         private var colorRamp: ColorRamp? = null
 
         init {
@@ -1417,13 +1391,11 @@
          * Sets an optional hint which suggests the renderer draws the complication using a
          * [ColorRamp].
          */
-        @ComplicationExperimental
         public fun setColorRamp(colorRamp: ColorRamp?): Builder = apply {
             this.colorRamp = colorRamp
         }
 
         /** Builds the [GoalProgressComplicationData]. */
-        @OptIn(ComplicationExperimental::class)
         public override fun build(): GoalProgressComplicationData {
             require(
                 monochromaticImage != null || smallImage != null || text != null || title != null
@@ -1460,7 +1432,6 @@
         }.build().also { cachedWireComplicationData = it }
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
         builder.setRangedValue(value)
         builder.setTargetValue(targetValue)
@@ -1483,7 +1454,6 @@
         }
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (javaClass != other?.javaClass) return false
@@ -1508,7 +1478,6 @@
         return true
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun hashCode(): Int {
         var result = value.hashCode()
         result = 31 * result + targetValue.hashCode()
@@ -1527,7 +1496,6 @@
         return result
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun toString(): String {
         val valueString = if (WireComplicationData.shouldRedact()) {
             "REDACTED"
@@ -1560,7 +1528,6 @@
     /** @hide */
     public companion object {
         /** The [ComplicationType] corresponding to objects of this type. */
-        @OptIn(ComplicationExperimental::class)
         @JvmField
         public val TYPE: ComplicationType = ComplicationType.GOAL_PROGRESS
 
@@ -1593,11 +1560,13 @@
  * recommended to choose the [smallImage]. It's best practice for a ComplicationDataSource to
  * specify both a [monochromaticImage] and a [smallImage].
  *
- * @property elements The breakdown of the subject into various [Element]s. E.g. the proportion of
- * calories consumed which were carbohydrates, fats etc... If this is equal to [PLACEHOLDER] then
- * the renderer must display this in a visually distinct way to suggest to the user that it's
- * placeholder data.  E.g. each rendered is rendered in light grey. The maximum valid size of this
- * list is [MAX_NUM_ELEMENTS].
+ * @property elements The breakdown of the subject into various [Element]s (e.g. the proportion of
+ * calories consumed which were carbohydrates, fats, etc.). The colors need to be meaningful to the
+ * user (e.g. blue is cold, yellow/red is worm), and should be consistent with the experience
+ * launched by tapping on the complication. If this is equal to [PLACEHOLDER] then the renderer must
+ * display this in a visually distinct way to suggest to the user that it's placeholder data.  E.g.
+ * each element is rendered in light grey. The maximum valid size of this list is
+ * [MAX_NUM_ELEMENTS].
  * @property elementBackgroundColor If elements are draw as segments then this is the background
  * color to use in between them.
  * @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
@@ -1626,7 +1595,7 @@
  * box.
  * @property contentDescription The content description field for accessibility.
  */
-@ComplicationExperimental
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class WeightedElementsComplicationData
 internal constructor(
     public val elements: List<Element>,
@@ -1657,10 +1626,11 @@
      * @property weight The weight of the Element which must be > zero. The size of the element when
      * rendered should be proportional to its weight. Weights are not required to sum to any
      * particular value.
-     * @property color The color of the Element. In conjunction with the other fields this color
-     * needs to be meaningful to the user. Tapping on the complication should launch an experience
-     * where the data is presented in more detail. Care must be taken to ensure the colors used are
-     * consistent.
+     * @property color The color of the Element, which must be used instead of the watch face's
+     * colors. This color needs to be meaningful to the user in conjunction with the other fields
+     * (e.g. blue is cold, red/yellow is warm). Tapping on the complication should launch an
+     * experience where the data is presented in more detail. Care must be taken to ensure the
+     * colors used are consistent with the launched experience.
      */
     class Element(
         @FloatRange(from = 0.0, fromInclusive = false) val weight: Float,
@@ -1705,7 +1675,6 @@
      * [MAX_NUM_ELEMENTS].
      * @param contentDescription Localized description for use by screen readers
      */
-    @OptIn(ComplicationExperimental::class)
     public class Builder(
         private val elements: List<Element>,
         private var contentDescription: ComplicationText
@@ -1766,7 +1735,6 @@
         }
 
         /** Builds the [GoalProgressComplicationData]. */
-        @OptIn(ComplicationExperimental::class)
         public override fun build(): WeightedElementsComplicationData {
             require(
                 monochromaticImage != null || smallImage != null || text != null || title != null
@@ -1802,7 +1770,6 @@
         }.build().also { cachedWireComplicationData = it }
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
         builder.setElementWeights(elements.map { it.weight }.toFloatArray())
         builder.setElementColors(elements.map { it.color }.toIntArray())
@@ -1832,7 +1799,6 @@
         }
     }
 
-    @OptIn(ComplicationExperimental::class)
     override fun toString(): String {
         val elementsString = if (WireComplicationData.shouldRedact()) {
             "REDACTED"
@@ -1888,7 +1854,6 @@
 
     public companion object {
         /** The [ComplicationType] corresponding to objects of this type. */
-        @OptIn(ComplicationExperimental::class)
         @JvmField
         public val TYPE: ComplicationType = ComplicationType.WEIGHTED_ELEMENTS
 
@@ -2378,416 +2343,6 @@
 }
 
 /**
- * A complication that contains a serialized protoLayout.
- *
- * @property contentDescription The content description field for accessibility and is used to
- * describe what data the image represents. If the image is purely stylistic, and does not convey
- * any information to the user, then provide an empty content description. If no content description
- * is provided, a generic content description will be used instead.
- */
-@ComplicationExperimental
-public class ProtoLayoutComplicationData
-internal
-/**
- * @param ambientLayoutWireFormat The [LayoutElementBuilders.Layout] serialized into a
- * [ByteArray] to be displayed when the device is ambient.
- * @param interactiveLayoutWireFormat The [LayoutElementBuilders.Layout] serialized into a
- * [ByteArray] to be displayed when the device is interactive.
- * @param layoutResourcesWireFormat The [ResourceBuilders.Resources] serialized into a [ByteArray]
- * for [interactiveLayoutWireFormat] and [ambientLayoutWireFormat].
- */
-constructor(
-    private val ambientLayoutWireFormat: ByteArray,
-    private val interactiveLayoutWireFormat: ByteArray,
-    private val layoutResourcesWireFormat: ByteArray,
-    val contentDescription: ComplicationText?,
-    tapAction: PendingIntent?,
-    validTimeRange: TimeRange?,
-    cachedWireComplicationData: WireComplicationData?,
-    dataSource: ComponentName?,
-    @ComplicationPersistencePolicy persistencePolicy: Int,
-    @ComplicationDisplayPolicy displayPolicy: Int
-) : ComplicationData(
-        TYPE,
-        tapAction,
-        cachedWireComplicationData,
-        validTimeRange ?: TimeRange.ALWAYS,
-        dataSource = dataSource,
-        persistencePolicy = persistencePolicy,
-        displayPolicy = displayPolicy
-    ) {
-
-    /** The [LayoutElementBuilders.Layout] to be displayed when the device is ambient. */
-    public val ambientLayout: LayoutElementBuilders.Layout by lazy {
-        LayoutElementBuilders.Layout.fromByteArray(ambientLayoutWireFormat)!!
-    }
-
-    /** The [LayoutElementBuilders.Layout] to be displayed when the device is interactive. */
-    public val interactiveLayout: LayoutElementBuilders.Layout by lazy {
-        LayoutElementBuilders.Layout.fromByteArray(interactiveLayoutWireFormat)!!
-    }
-
-    /** The [ResourceBuilders.Resources] for [ambientLayout] and [interactiveLayout]. */
-    public val layoutResources: ResourceBuilders.Resources by lazy {
-        ResourceBuilders.Resources.fromByteArray(layoutResourcesWireFormat)!!
-    }
-
-    /**
-     * Builder for [ProtoLayoutComplicationData].
-     *
-     * You must at a minimum set the [ambientLayout], [interactiveLayout], [layoutResources] and
-     * [contentDescription] fields.
-     *
-     * @param ambientLayout The [LayoutElementBuilders.Layout] serialized into a [ByteArray] to be
-     * displayed when the device is ambient
-     * @param interactiveLayout The [LayoutElementBuilders.Layout] serialized into a [ByteArray] to
-     * be displayed when the device is interactive
-     * @param layoutResources The [ResourceBuilders.Resources] serialized into a [ByteArray] for
-     * [interactiveLayout] and [ambientLayout]
-     * @param contentDescription Localized description for use by screen readers
-     */
-    public class Builder(
-        private val ambientLayout: ByteArray,
-        private val interactiveLayout: ByteArray,
-        private val layoutResources: ByteArray,
-        private val contentDescription: ComplicationText
-    ) : BaseBuilder<Builder, ProtoLayoutComplicationData>() {
-        /**
-         * @param ambientLayout The [LayoutElementBuilders.Layout] to be displayed when the device
-         * is ambient
-         * @param interactiveLayout The [LayoutElementBuilders.Layout] to be displayed when the
-         * device is interactive
-         * @param resources The [ResourceBuilders.Resources] for [interactiveLayout] and
-         * [ambientLayout]
-         * @param contentDescription Localized description for use by screen readers
-         */
-        constructor(
-            ambientLayout: LayoutElementBuilders.Layout,
-            interactiveLayout: LayoutElementBuilders.Layout,
-            resources: ResourceBuilders.Resources,
-            contentDescription: ComplicationText
-        ) : this(
-            ambientLayout.toByteArray(),
-            interactiveLayout.toByteArray(),
-            resources.toByteArray(),
-            contentDescription
-        )
-
-        private var tapAction: PendingIntent? = null
-        private var validTimeRange: TimeRange? = null
-
-        /** Sets optional pending intent to be invoked when the complication is tapped. */
-        public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
-            this.tapAction = tapAction
-        }
-
-        /** Sets optional time range during which the complication has to be shown. */
-        @Suppress("MissingGetterMatchingBuilder") // b/174052810
-        public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
-            this.validTimeRange = validTimeRange
-        }
-
-        /** Builds the [ProtoLayoutComplicationData]. */
-        public override fun build(): ProtoLayoutComplicationData =
-            ProtoLayoutComplicationData(
-                ambientLayout,
-                interactiveLayout,
-                layoutResources,
-                contentDescription,
-                tapAction,
-                validTimeRange,
-                cachedWireComplicationData,
-                dataSource,
-                persistencePolicy,
-                displayPolicy
-            )
-    }
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder()
-            .apply { fillWireComplicationDataBuilder(this) }
-            .build()
-            .also { cachedWireComplicationData = it }
-    }
-
-    override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
-        builder.setInteractiveLayout(interactiveLayoutWireFormat)
-        builder.setAmbientLayout(ambientLayoutWireFormat)
-        builder.setLayoutResources(layoutResourcesWireFormat)
-        builder.setContentDescription(
-            when (contentDescription) {
-                ComplicationText.EMPTY -> null
-                else -> contentDescription?.toWireComplicationText()
-            }
-        )
-        builder.setTapAction(tapAction)
-        setValidTimeRange(validTimeRange, builder)
-        builder.setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as ProtoLayoutComplicationData
-
-        if (!interactiveLayoutWireFormat.contentEquals(other.interactiveLayoutWireFormat))
-            return false
-        if (!ambientLayoutWireFormat.contentEquals(other.ambientLayoutWireFormat)) return false
-        if (!layoutResourcesWireFormat.contentEquals(other.layoutResourcesWireFormat)) return false
-        if (contentDescription != other.contentDescription) return false
-        if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
-        if (tapAction != other.tapAction) return false
-        if (validTimeRange != other.validTimeRange) return false
-        if (dataSource != other.dataSource) return false
-        if (persistencePolicy != other.persistencePolicy) return false
-        if (displayPolicy != other.displayPolicy) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = interactiveLayoutWireFormat.hashCode()
-        result = 31 * result + ambientLayoutWireFormat.hashCode()
-        result = 31 * result + layoutResourcesWireFormat.hashCode()
-        result = 31 * result + (contentDescription?.hashCode() ?: 0)
-        result = 31 * result + tapActionLostDueToSerialization.hashCode()
-        result = 31 * result + (tapAction?.hashCode() ?: 0)
-        result = 31 * result + validTimeRange.hashCode()
-        result = 31 * result + dataSource.hashCode()
-        result = 31 * result + persistencePolicy.hashCode()
-        result = 31 * result + persistencePolicy.hashCode()
-        result = 31 * result + displayPolicy.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return "ProtoLayoutComplicationData(protoLayoutWireFormat=$interactiveLayoutWireFormat, " +
-            "ambientProtoLayoutWireFormat=$ambientLayoutWireFormat, " +
-            "resourcesWireFormat=$layoutResourcesWireFormat, " +
-            "contentDescription=$contentDescription, " +
-            "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
-            "tapAction=$tapAction, validTimeRange=$validTimeRange, dataSource=$dataSource, " +
-            "persistencePolicy=$persistencePolicy, displayPolicy=$displayPolicy)"
-    }
-
-    /** @hide */
-    public companion object {
-        /** The [ComplicationType] corresponding to objects of this type. */
-        @JvmField
-        public val TYPE: ComplicationType = ComplicationType.PROTO_LAYOUT
-    }
-}
-
-/**
- * A complication that's a list of other complications, typically rendered as a table. E.g. the
- * weather forecast for the next three days could consist of three [ShortTextComplicationData]s
- * displayed in a row of columns.
- *
- * @property complicationList The list of sub [ComplicationData]s to display. This has a maximum
- * size of [ListComplicationData.MAX_ITEMS]. Note complicationList may not include a
- * ListComplicationData.
- * @property contentDescription The content description field for accessibility and is used to
- * describe what data the image represents. If the image is purely stylistic, and does not convey
- * any information to the user, then provide an empty content description. If no content description
- * is provided, a generic content description will be used instead.
- * @property styleHint The [StyleHint] which influences layout.
- */
-@ComplicationExperimental
-public class ListComplicationData
-internal constructor(
-    public val complicationList: List<ComplicationData>,
-    public val contentDescription: ComplicationText?,
-    tapAction: PendingIntent?,
-    validTimeRange: TimeRange?,
-    cachedWireComplicationData: WireComplicationData?,
-    dataSource: ComponentName?,
-    public val styleHint: StyleHint,
-    @ComplicationPersistencePolicy persistencePolicy: Int,
-    @ComplicationDisplayPolicy displayPolicy: Int
-) : ComplicationData(
-        TYPE,
-        tapAction = tapAction,
-        cachedWireComplicationData = cachedWireComplicationData,
-        validTimeRange = validTimeRange ?: TimeRange.ALWAYS,
-        dataSource = dataSource,
-        persistencePolicy = persistencePolicy,
-        displayPolicy = displayPolicy
-) {
-
-    init {
-        require(complicationList.size <= MAX_ITEMS) {
-            "complicationList has a maximum of $MAX_ITEMS entries, but found " +
-                complicationList.size
-        }
-
-        for (entry in complicationList) {
-            require(entry !is ListComplicationData) {
-                "You may not include a ListComplicationData inside a ListComplicationData"
-            }
-        }
-    }
-
-    /** A hint for generating a layout for [ListComplicationData]. */
-    @ComplicationExperimental
-    enum class StyleHint(private val wireType: Int) {
-        /** Hints the list should be displayed as a single column where the entries are rows. */
-        ColumnOfRows(0),
-
-        /** Hints the list should be displayed as a single row where the entries are columns. */
-        RowOfColumns(1);
-
-        override fun toString(): String {
-            return "ListComplicationLayoutStyleHint(wireType=$wireType)"
-        }
-
-        internal companion object {
-            fun fromWireFormat(wireType: Int): StyleHint =
-                when (wireType) {
-                    ColumnOfRows.ordinal -> ColumnOfRows
-                    RowOfColumns.ordinal -> RowOfColumns
-                    else ->
-                        throw java.lang.IllegalArgumentException(
-                            "Unrecognized ListComplicationLayoutStyleHint wireType $wireType"
-                        )
-                }
-        }
-    }
-
-    /**
-     * Builder for [ListComplicationData].
-     *
-     * You must at a minimum set the [complicationList], [styleHint] and [contentDescription]
-     * fields.
-     *
-     * @param complicationList The list [ComplicationData] to be displayed, typically as a table.
-     * Note complicationList may not include a ListComplicationData.
-     * @param styleHint The [StyleHint] which influences layout.
-     * @param contentDescription Localized description for use by screen readers
-     */
-    public class Builder(
-        private val complicationList: List<ComplicationData>,
-        private val styleHint: StyleHint,
-        private val contentDescription: ComplicationText
-    ) : BaseBuilder<Builder, ListComplicationData>() {
-        private var tapAction: PendingIntent? = null
-        private var validTimeRange: TimeRange? = null
-
-        /** Sets optional pending intent to be invoked when the complication is tapped. */
-        @SuppressWarnings("MissingGetterMatchingBuilder") // See http://b/174052810
-        public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
-            this.tapAction = tapAction
-        }
-
-        /** Sets optional time range during which the complication has to be shown. */
-        @SuppressWarnings("MissingGetterMatchingBuilder") // See http://b/174052810
-        public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
-            this.validTimeRange = validTimeRange
-        }
-
-        /** Builds the [ListComplicationData]. */
-        public override fun build(): ListComplicationData =
-            ListComplicationData(
-                complicationList,
-                contentDescription,
-                tapAction,
-                validTimeRange,
-                cachedWireComplicationData,
-                dataSource,
-                styleHint,
-                persistencePolicy,
-                displayPolicy
-            )
-    }
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    override fun asWireComplicationData(): WireComplicationData {
-        cachedWireComplicationData?.let {
-            return it
-        }
-        return createWireComplicationDataBuilder()
-            .apply { fillWireComplicationDataBuilder(this) }
-            .build()
-            .also { cachedWireComplicationData = it }
-    }
-
-    override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
-        builder.setListEntryCollection(complicationList.map {
-            it.asWireComplicationData()
-        })
-        builder.setListStyleHint(styleHint.ordinal)
-        builder.setContentDescription(
-            when (contentDescription) {
-                ComplicationText.EMPTY -> null
-                else -> contentDescription?.toWireComplicationText()
-            }
-        )
-        builder.setTapAction(tapAction)
-        setValidTimeRange(validTimeRange, builder)
-        builder.setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
-    }
-
-    override fun hasPlaceholderFields() = false
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as ListComplicationData
-
-        if (complicationList != other.complicationList) return false
-        if (contentDescription != other.contentDescription) return false
-        if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
-        if (tapAction != other.tapAction) return false
-        if (validTimeRange != other.validTimeRange) return false
-        if (dataSource != other.dataSource) return false
-        if (styleHint != other.styleHint) return false
-        if (persistencePolicy != other.persistencePolicy) return false
-        if (displayPolicy != other.displayPolicy) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = complicationList.hashCode()
-        result = 31 * result + (contentDescription?.hashCode() ?: 0)
-        result = 31 * result + tapActionLostDueToSerialization.hashCode()
-        result = 31 * result + (tapAction?.hashCode() ?: 0)
-        result = 31 * result + validTimeRange.hashCode()
-        result = 31 * result + dataSource.hashCode()
-        result = 31 * result + styleHint.hashCode()
-        result = 31 * result + persistencePolicy.hashCode()
-        result = 31 * result + displayPolicy.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return "ListComplicationData(complicationList=$complicationList, " +
-            "contentDescription=$contentDescription, " +
-            "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
-            "tapAction=$tapAction, validTimeRange=$validTimeRange, dataSource=$dataSource, " +
-            "styleHint=$styleHint, persistencePolicy=$persistencePolicy, " +
-            "displayPolicy=$displayPolicy)"
-    }
-
-    /** @hide */
-    public companion object {
-        /** The [ComplicationType] corresponding to objects of this type. */
-        @JvmField
-        public val TYPE: ComplicationType = ComplicationType.LIST
-
-        /** The maximum number of items in [ListComplicationData.complicationList]. */
-        public const val MAX_ITEMS = 5
-    }
-}
-
-/**
  * Type sent by the system when the watch face does not have permission to receive complication
  * data.
  *
@@ -2948,7 +2503,6 @@
     }
 }
 
-@OptIn(ComplicationExperimental::class)
 @Suppress("NewApi")
 internal fun WireComplicationData.toPlaceholderComplicationData(): ComplicationData? {
     try {
@@ -3049,38 +2603,6 @@
                     displayPolicyCopy
                 )
 
-            // TODO(b/230102159): We need to build support for placeholder ProtoLayoutComplicationData.
-            ProtoLayoutComplicationData.TYPE.toWireComplicationType() ->
-                ProtoLayoutComplicationData.Builder(
-                    ambientLayout!!,
-                    interactiveLayout!!,
-                    layoutResources!!,
-                    contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
-                )
-                    .apply {
-                        setTapAction(tapAction)
-                        setValidTimeRange(parseTimeRange())
-                        setDataSource(dataSourceCopy)
-                        setPersistencePolicy(persistencePolicyCopy)
-                        setDisplayPolicy(displayPolicyCopy)
-                    }
-                    .build()
-
-            ListComplicationData.TYPE.toWireComplicationType() ->
-                ListComplicationData.Builder(
-                    listEntries!!.map { it.toApiComplicationData() },
-                    ListComplicationData.StyleHint.fromWireFormat(listStyleHint),
-                    contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
-                )
-                    .apply {
-                        setTapAction(tapAction)
-                        setValidTimeRange(parseTimeRange())
-                        setDataSource(dataSourceCopy)
-                        setPersistencePolicy(persistencePolicyCopy)
-                        setDisplayPolicy(displayPolicyCopy)
-                    }
-                    .build()
-
             GoalProgressComplicationData.TYPE.toWireComplicationType() ->
                 GoalProgressComplicationData.Builder(
                     value = rangedValue,
@@ -3145,7 +2667,6 @@
 /**
  * @hide
  */
-@OptIn(ComplicationExperimental::class)
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @Suppress("NewApi")
 public fun WireComplicationData.toApiComplicationData(): ComplicationData {
@@ -3261,39 +2782,6 @@
                     setDisplayPolicy(displayPolicyCopy)
                 }.build()
 
-            ProtoLayoutComplicationData.TYPE.toWireComplicationType() ->
-                ProtoLayoutComplicationData.Builder(
-                    ambientLayout!!,
-                    interactiveLayout!!,
-                    layoutResources!!,
-                    contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
-                )
-                    .apply {
-                        setTapAction(tapAction)
-                        setValidTimeRange(parseTimeRange())
-                        setCachedWireComplicationData(wireComplicationData)
-                        setDataSource(dataSourceCopy)
-                        setPersistencePolicy(persistencePolicyCopy)
-                        setDisplayPolicy(displayPolicyCopy)
-                    }
-                    .build()
-
-            ListComplicationData.TYPE.toWireComplicationType() ->
-                ListComplicationData.Builder(
-                    listEntries!!.map { it.toApiComplicationData() },
-                    ListComplicationData.StyleHint.fromWireFormat(listStyleHint),
-                    contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
-                )
-                    .apply {
-                        setTapAction(tapAction)
-                        setValidTimeRange(parseTimeRange())
-                        setCachedWireComplicationData(wireComplicationData)
-                        setDataSource(dataSourceCopy)
-                        setPersistencePolicy(persistencePolicyCopy)
-                        setDisplayPolicy(displayPolicyCopy)
-                    }
-                    .build()
-
             NoPermissionComplicationData.TYPE.toWireComplicationType() ->
                 NoPermissionComplicationData.Builder().apply {
                     setMonochromaticImage(parseIcon())
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
index 2d02345..49b6a23 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
@@ -16,6 +16,8 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.os.Build
+import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 
 /**
@@ -36,15 +38,10 @@
     SMALL_IMAGE(WireComplicationData.TYPE_SMALL_IMAGE),
     PHOTO_IMAGE(WireComplicationData.TYPE_LARGE_IMAGE),
     NO_PERMISSION(WireComplicationData.TYPE_NO_PERMISSION),
-
-    @ComplicationExperimental
-    PROTO_LAYOUT(WireComplicationData.EXP_TYPE_PROTO_LAYOUT),
-    @ComplicationExperimental
-    GOAL_PROGRESS(WireComplicationData.EXP_TYPE_GOAL_PROGRESS),
-    @ComplicationExperimental
-    WEIGHTED_ELEMENTS(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS),
-    @ComplicationExperimental
-    LIST(WireComplicationData.EXP_TYPE_LIST);
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    GOAL_PROGRESS(WireComplicationData.TYPE_GOAL_PROGRESS),
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    WEIGHTED_ELEMENTS(WireComplicationData.TYPE_WEIGHTED_ELEMENTS);
 
     /**
      * Converts this value to the integer value used for serialization.
@@ -69,6 +66,7 @@
         @OptIn(ComplicationExperimental::class)
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @JvmStatic
+        @Suppress("NewApi")
         public fun fromWireType(wireType: Int): ComplicationType =
             when (wireType) {
                 NO_DATA.wireType -> NO_DATA
@@ -81,10 +79,8 @@
                 SMALL_IMAGE.wireType -> SMALL_IMAGE
                 PHOTO_IMAGE.wireType -> PHOTO_IMAGE
                 NO_PERMISSION.wireType -> NO_PERMISSION
-                PROTO_LAYOUT.wireType -> PROTO_LAYOUT
                 GOAL_PROGRESS.wireType -> GOAL_PROGRESS
                 WEIGHTED_ELEMENTS.wireType -> WEIGHTED_ELEMENTS
-                LIST.wireType -> LIST
                 else -> EMPTY
             }
 
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 340b6ee..a369ee0 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -398,7 +398,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun rangedValueComplicationData() {
         val data = RangedValueComplicationData.Builder(
@@ -467,7 +466,6 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.P)
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun rangedValueComplicationData_withImages() {
         val data = RangedValueComplicationData.Builder(
@@ -551,7 +549,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun goalProgressComplicationData_with_ColorRamp() {
         val data = GoalProgressComplicationData.Builder(
@@ -564,7 +561,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
                     .setRangedValue(1200f)
                     .setTargetValue(10000f)
                     .setShortTitle(WireComplicationText.plainText("steps"))
@@ -622,7 +619,6 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.P)
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun goalProgressComplicationData_with_ColorRamp_and_Images() {
         val data = GoalProgressComplicationData.Builder(
@@ -637,7 +633,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
                     .setRangedValue(1200f)
                     .setTargetValue(10000f)
                     .setIcon(monochromaticImageIcon)
@@ -706,7 +702,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun rangedValueComplicationData_with_ColorRamp() {
         val data = RangedValueComplicationData.Builder(
@@ -779,7 +774,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun weightedElementsComplicationData() {
         val data = WeightedElementsComplicationData.Builder(
@@ -796,7 +790,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+                WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                     .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                     .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                     .setElementBackgroundColor(Color.GRAY)
@@ -866,7 +860,6 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.P)
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun weightedElementsComplicationData_withImages() {
         val data = WeightedElementsComplicationData.Builder(
@@ -884,7 +877,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+                WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                     .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                     .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                     .setElementBackgroundColor(Color.TRANSPARENT)
@@ -1239,139 +1232,6 @@
     }
 
     @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun listComplicationData() {
-        val data = ListComplicationData.Builder(
-            listOf(
-                ShortTextComplicationData.Builder("text".complicationText, ComplicationText.EMPTY)
-                    .build(),
-                RangedValueComplicationData.Builder(
-                    value = 95f,
-                    min = 0f,
-                    max = 100f,
-                    ComplicationText.EMPTY
-                )
-                    .setText("battery".complicationText)
-                    .build()
-            ),
-            ListComplicationData.StyleHint.RowOfColumns,
-            ComplicationText.EMPTY
-        )
-            .setDataSource(dataSourceA).build()
-
-        ParcelableSubject.assertThat(data.asWireComplicationData())
-            .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
-                    .setListEntryCollection(
-                        listOf(
-                            WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
-                                .setShortText(WireComplicationText.plainText("text"))
-                                .setPersistencePolicy(
-                                    ComplicationPersistencePolicies.CACHING_ALLOWED
-                                )
-                                .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                                .build(),
-                            WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
-                                .setRangedValue(95f)
-                                .setRangedValueType(RangedValueTypes.UNDEFINED)
-                                .setRangedMinValue(0f)
-                                .setRangedMaxValue(100f)
-                                .setShortText(WireComplicationText.plainText("battery"))
-                                .setPersistencePolicy(
-                                    ComplicationPersistencePolicies.CACHING_ALLOWED
-                                )
-                                .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                                .build()
-                        )
-                    )
-                    .setListStyleHint(ListComplicationData.StyleHint.RowOfColumns.ordinal)
-                    .setDataSource(dataSourceA)
-                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                    .build()
-            )
-        testRoundTripConversions(data)
-        val deserialized = serializeAndDeserialize(data) as ListComplicationData
-        assertThat(deserialized.complicationList).containsExactly(
-            ShortTextComplicationData.Builder("text".complicationText, ComplicationText.EMPTY)
-                .build(),
-            RangedValueComplicationData.Builder(
-                value = 95f,
-                min = 0f,
-                max = 100f,
-                ComplicationText.EMPTY
-            )
-                .setText("battery".complicationText)
-                .build()
-        )
-        assertThat(deserialized.styleHint).isEqualTo(ListComplicationData.StyleHint.RowOfColumns)
-
-        val data2 = ListComplicationData.Builder(
-            listOf(
-                ShortTextComplicationData.Builder("text".complicationText, ComplicationText.EMPTY)
-                    .build(),
-                RangedValueComplicationData.Builder(
-                    value = 95f,
-                    min = 0f,
-                    max = 100f,
-                    ComplicationText.EMPTY
-                )
-                    .setText("battery".complicationText)
-                    .build()
-            ),
-            ListComplicationData.StyleHint.RowOfColumns,
-            ComplicationText.EMPTY
-        ).setDataSource(dataSourceA).build()
-
-        val data3 = ListComplicationData.Builder(
-            listOf(
-                ShortTextComplicationData.Builder("text2".complicationText, ComplicationText.EMPTY)
-                    .build(),
-                RangedValueComplicationData.Builder(
-                    value = 95f,
-                    min = 0f,
-                    max = 100f,
-                    ComplicationText.EMPTY
-                )
-                    .setText("battery".complicationText)
-                    .build()
-            ),
-            ListComplicationData.StyleHint.RowOfColumns,
-            ComplicationText.EMPTY
-        ).setDataSource(dataSourceB).build()
-
-        assertThat(data).isEqualTo(data2)
-        assertThat(data).isNotEqualTo(data3)
-        assertThat(data.hashCode()).isEqualTo(data2.hashCode())
-        assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
-        assertThat(data.toString()).isEqualTo(
-            "ListComplicationData(complicationList=[ShortTextComplicationData(" +
-                "text=ComplicationText{mSurroundingText=text, mTimeDependentText=null}, " +
-                "title=null, monochromaticImage=null, smallImage=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=, mTimeDependentText=null}," +
-                " tapActionLostDueToSerialization=false, tapAction=null, " +
-                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
-                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=null, " +
-                "persistencePolicy=0, displayPolicy=0), RangedValueComplicationData(value=95.0, " +
-                "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
-                "title=null, text=ComplicationText{mSurroundingText=battery, " +
-                "mTimeDependentText=null}, contentDescription=ComplicationText{mSurroundingText=," +
-                " mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
-                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=null, " +
-                "colorRamp=null, persistencePolicy=0, displayPolicy=0)], " +
-                "contentDescription=ComplicationText{mSurroundingText=, mTimeDependentText=null}," +
-                " tapActionLostDueToSerialization=false, tapAction=null," +
-                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z," +
-                " endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
-                "ComponentInfo{com.pkg_a/com.a}, " +
-                "styleHint=ListComplicationLayoutStyleHint(wireType=1), persistencePolicy=0, " +
-                "displayPolicy=0)"
-        )
-    }
-
-    @Test
     public fun noDataComplicationData_shortText() {
         val data = NoDataComplicationData(
             ShortTextComplicationData.Builder(
@@ -1513,7 +1373,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_rangedValue() {
         val data = NoDataComplicationData(
@@ -1598,7 +1457,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_goalProgress() {
         val data = NoDataComplicationData(
@@ -1616,7 +1474,7 @@
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
                             .setRangedValue(GoalProgressComplicationData.PLACEHOLDER)
                             .setTargetValue(10000f)
                             .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
@@ -1684,7 +1542,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_weightedElements() {
         val data = NoDataComplicationData(
@@ -1705,7 +1562,7 @@
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                             .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                             .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                             .setElementBackgroundColor(Color.GRAY)
@@ -1777,7 +1634,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_rangedValue_with_ColorRange() {
         val data = NoDataComplicationData(
@@ -2147,7 +2003,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun rangedValueComplicationData() {
         assertRoundtrip(
@@ -2164,7 +2019,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun rangedValueComplicationData_drawSegmented() {
         assertRoundtrip(
@@ -2181,11 +2035,10 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun goalProgressComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+            WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
                 .setRangedValue(1200f)
                 .setTargetValue(10000f)
                 .setShortTitle(WireComplicationText.plainText("steps"))
@@ -2199,11 +2052,10 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun weightedElementsComplicationData() {
         assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+            WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                 .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                 .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                 .setElementBackgroundColor(Color.DKGRAY)
@@ -2272,52 +2124,6 @@
     }
 
     @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun protoLayoutComplicationData() {
-        val ambientLayout = ByteArray(1)
-        val interactiveLayout = ByteArray(2)
-        val resources = ByteArray(3)
-
-        assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_PROTO_LAYOUT)
-                .setAmbientLayout(ambientLayout)
-                .setInteractiveLayout(interactiveLayout)
-                .setLayoutResources(resources)
-                .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                .build(),
-            ComplicationType.PROTO_LAYOUT
-        )
-    }
-
-    @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun listComplicationData() {
-        val wireShortText = WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(WireComplicationText.plainText("text"))
-            .build()
-
-        val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
-            .setRangedValue(95f)
-            .setRangedMinValue(0f)
-            .setRangedMaxValue(100f)
-            .setShortText(WireComplicationText.plainText("battery"))
-            .build()
-
-        assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
-                .setListEntryCollection(listOf(wireShortText, wireRangedValue))
-                .setListStyleHint(
-                    ListComplicationData.StyleHint.RowOfColumns.ordinal
-                )
-                .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                .build(),
-            ComplicationType.LIST
-        )
-    }
-
-    @Test
     public fun noDataComplicationData_shortText() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
@@ -2365,7 +2171,6 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_rangedValue() {
         val icon = Icon.createWithContentUri("someuri")
@@ -2393,14 +2198,13 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_goalProgress() {
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+                    WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
                         .setRangedValue(1200f)
                         .setTargetValue(10000f)
                         .setShortTitle(WireComplicationText.plainText("steps"))
@@ -2421,13 +2225,12 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_weightedElementsComplicationData() {
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
                 .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+                    WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                         .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                         .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                         .setElementBackgroundColor(Color.DKGRAY)
@@ -2491,64 +2294,6 @@
         )
     }
 
-    @Test
-    public fun noDataComplicationData_protoLayout() {
-        val ambientLayout = ByteArray(1)
-        val interactiveLayout = ByteArray(2)
-        val resources = ByteArray(3)
-        assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_PROTO_LAYOUT)
-                        .setAmbientLayout(ambientLayout)
-                        .setInteractiveLayout(interactiveLayout)
-                        .setLayoutResources(resources)
-                        .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                        .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                        .build()
-                )
-                .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                .build(),
-            ComplicationType.NO_DATA
-        )
-    }
-
-    @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun noDataComplicationData_list() {
-        val wireShortText = WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(WireComplicationText.plainText("text"))
-            .build()
-
-        val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
-            .setRangedValue(95f)
-            .setRangedMinValue(0f)
-            .setRangedMaxValue(100f)
-            .setShortTitle(WireComplicationText.plainText("battery"))
-            .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-            .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-            .build()
-
-        assertRoundtrip(
-            WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                .setPlaceholder(
-                    WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
-                        .setListEntryCollection(listOf(wireShortText, wireRangedValue))
-                        .setListStyleHint(
-                            ListComplicationData.StyleHint.RowOfColumns.ordinal
-                        )
-                        .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                        .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                        .build(),
-                )
-                .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                .build(),
-            ComplicationType.NO_DATA
-        )
-    }
-
     private fun assertRoundtrip(wireData: WireComplicationData, type: ComplicationType) {
         val data = wireData.toApiComplicationData()
         assertThat(data.type).isEqualTo(type)
@@ -2596,10 +2341,10 @@
         ).isEqualTo(mPendingIntent)
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun goalProgressComplicationData() {
         assertThat(
+            @Suppress("NewApi")
             GoalProgressComplicationData.Builder(
                 value = 1200f, targetValue = 10000f,
                 contentDescription = "content description".complicationText
@@ -2611,10 +2356,10 @@
         ).isEqualTo(mPendingIntent)
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun weightedElementsComplicationData() {
         assertThat(
+            @Suppress("NewApi")
             WeightedElementsComplicationData.Builder(
                 listOf(
                     WeightedElementsComplicationData.Element(0.5f, Color.RED),
@@ -2676,44 +2421,6 @@
             ).asWireComplicationData().placeholder?.tapAction
         ).isEqualTo(mPendingIntent)
     }
-
-    @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun protoLayoutComplicationData() {
-        val ambientLayout = ByteArray(1)
-        val interactiveLayout = ByteArray(2)
-        val resources = ByteArray(3)
-
-        assertThat(
-            ProtoLayoutComplicationData.Builder(
-                ambientLayout,
-                interactiveLayout,
-                resources,
-                ComplicationText.EMPTY
-            )
-                .setTapAction(mPendingIntent)
-                .build()
-                .tapAction
-        ).isEqualTo(mPendingIntent)
-    }
-
-    @OptIn(ComplicationExperimental::class)
-    @Test
-    public fun listComplicationData() {
-        val icon = Icon.createWithContentUri("someuri")
-        val image = SmallImage.Builder(icon, SmallImageType.PHOTO).build()
-        assertThat(
-            ListComplicationData.Builder(
-                listOf(
-                    SmallImageComplicationData.Builder(image, ComplicationText.EMPTY).build()
-                ),
-                ListComplicationData.StyleHint.RowOfColumns,
-                ComplicationText.EMPTY
-            )
-                .setTapAction(mPendingIntent).build()
-                .tapAction
-        ).isEqualTo(mPendingIntent)
-    }
 }
 
 @RunWith(SharedRobolectricTestRunner::class)
@@ -2756,10 +2463,10 @@
         ).isEqualTo(mPendingIntent)
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun goalProgressComplicationData() {
         assertThat(
+            @Suppress("NewApi")
             GoalProgressComplicationData.Builder(
                 value = 1200f, targetValue = 10000f,
                 contentDescription = "content description".complicationText
@@ -2771,10 +2478,10 @@
         ).isEqualTo(mPendingIntent)
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun weightedElementsComplicationData() {
         assertThat(
+            @Suppress("NewApi")
             WeightedElementsComplicationData.Builder(
                 listOf(
                     WeightedElementsComplicationData.Element(0.5f, Color.RED),
@@ -2839,46 +2546,6 @@
             ).asWireComplicationData().toApiComplicationData().tapAction
         ).isEqualTo(mPendingIntent)
     }
-
-    @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun protoLayoutComplicationData() {
-        val ambientLayout = ByteArray(1)
-        val interactiveLayout = ByteArray(2)
-        val resources = ByteArray(3)
-
-        assertThat(
-            ProtoLayoutComplicationData.Builder(
-                ambientLayout,
-                interactiveLayout,
-                resources,
-                ComplicationText.EMPTY
-            )
-                .setTapAction(mPendingIntent)
-                .build()
-                .asWireComplicationData().toApiComplicationData().tapAction
-        ).isEqualTo(mPendingIntent)
-    }
-
-    @OptIn(ComplicationExperimental::class)
-    @Test
-    public fun listComplicationData() {
-        val icon = Icon.createWithContentUri("someuri")
-        val image = SmallImage.Builder(icon, SmallImageType.PHOTO).build()
-        assertThat(
-            ListComplicationData.Builder(
-                listOf(
-                    SmallImageComplicationData.Builder(image, ComplicationText.EMPTY).build()
-                ),
-                ListComplicationData.StyleHint.RowOfColumns,
-                ComplicationText.EMPTY
-            )
-                .setTapAction(mPendingIntent).build()
-                .asWireComplicationData()
-                .toApiComplicationData()
-                .tapAction
-        ).isEqualTo(mPendingIntent)
-    }
 }
 
 @RunWith(SharedRobolectricTestRunner::class)
@@ -2923,7 +2590,6 @@
             )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun rangedValueComplicationData() {
         val data = RangedValueComplicationData.Builder(
@@ -2949,7 +2615,6 @@
             )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun goalProgressComplicationData() {
         val data = GoalProgressComplicationData.Builder(
@@ -2962,7 +2627,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
                     .setRangedValue(1200f)
                     .setTargetValue(10000f)
                     .setShortTitle(WireComplicationText.plainText("steps"))
@@ -2977,7 +2642,6 @@
             )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun weightedElementsComplicationData() {
         val data = WeightedElementsComplicationData.Builder(
@@ -2993,7 +2657,7 @@
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+                WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                     .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                     .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                     .setElementBackgroundColor(Color.TRANSPARENT)
@@ -3065,92 +2729,6 @@
     }
 
     @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun protoLayout() {
-        val ambientLayout = ByteArray(1)
-        val interactiveLayout = ByteArray(2)
-        val resources = ByteArray(3)
-
-        val data = ProtoLayoutComplicationData.Builder(
-            ambientLayout,
-            interactiveLayout,
-            resources,
-            ComplicationText.EMPTY
-        )
-            .setValidTimeRange(TimeRange.between(testStartInstant, testEndDateInstant))
-            .build()
-
-        ParcelableSubject.assertThat(data.asWireComplicationData())
-            .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_PROTO_LAYOUT)
-                    .setAmbientLayout(ambientLayout)
-                    .setInteractiveLayout(interactiveLayout)
-                    .setLayoutResources(resources)
-                    .setStartDateTimeMillis(testStartInstant.toEpochMilli())
-                    .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
-                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                    .build()
-            )
-    }
-
-    @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun list() {
-        val data = ListComplicationData.Builder(
-            listOf(
-                ShortTextComplicationData.Builder(
-                    "text".complicationText,
-                    ComplicationText.EMPTY
-                )
-                    .build(),
-                RangedValueComplicationData.Builder(
-                    value = 95f,
-                    min = 0f,
-                    max = 100f,
-                    ComplicationText.EMPTY
-                )
-                    .setText("battery".complicationText)
-                    .build()
-            ),
-            ListComplicationData.StyleHint.RowOfColumns,
-            ComplicationText.EMPTY
-        )
-            .setValidTimeRange(TimeRange.between(testStartInstant, testEndDateInstant))
-            .build()
-
-        val wireShortText = WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(WireComplicationText.plainText("text"))
-            .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-            .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-            .build()
-
-        val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
-            .setRangedValue(95f)
-            .setRangedValueType(RangedValueTypes.UNDEFINED)
-            .setRangedMinValue(0f)
-            .setRangedMaxValue(100f)
-            .setShortText(WireComplicationText.plainText("battery"))
-            .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-            .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-            .build()
-
-        ParcelableSubject.assertThat(data.asWireComplicationData())
-            .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
-                    .setListEntryCollection(listOf(wireShortText, wireRangedValue))
-                    .setListStyleHint(
-                        ListComplicationData.StyleHint.RowOfColumns.ordinal
-                    )
-                    .setStartDateTimeMillis(testStartInstant.toEpochMilli())
-                    .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
-                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                    .build()
-            )
-    }
-
-    @Test
     public fun noDataComplicationData_shortText() {
         val data = NoDataComplicationData(
             ShortTextComplicationData.Builder("text".complicationText, ComplicationText.EMPTY)
@@ -3194,7 +2772,6 @@
             )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_rangedValue() {
         val data = NoDataComplicationData(
@@ -3227,7 +2804,6 @@
             )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_goalProgress() {
         val data = NoDataComplicationData(
@@ -3243,7 +2819,7 @@
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
                             .setRangedValue(1200f)
                             .setTargetValue(10000f)
                             .setShortTitle(WireComplicationText.plainText("steps"))
@@ -3262,7 +2838,6 @@
             )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
     public fun noDataComplicationData_weightedElements() {
         val data = NoDataComplicationData(
@@ -3281,7 +2856,7 @@
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
                     .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
                             .setElementWeights(floatArrayOf(0.5f, 1f, 2f))
                             .setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
                             .setElementBackgroundColor(Color.TRANSPARENT)
@@ -3367,99 +2942,6 @@
                     .build()
             )
     }
-
-    @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun noDataComplicationData_protoLayout() {
-        val ambientLayout = ByteArray(1)
-        val interactiveLayout = ByteArray(2)
-        val resources = ByteArray(3)
-
-        val data = NoDataComplicationData(
-            ProtoLayoutComplicationData.Builder(
-                ambientLayout,
-                interactiveLayout,
-                resources,
-                ComplicationText.EMPTY
-            ).build()
-        )
-        ParcelableSubject.assertThat(data.asWireComplicationData())
-            .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_PROTO_LAYOUT)
-                            .setAmbientLayout(ambientLayout)
-                            .setInteractiveLayout(interactiveLayout)
-                            .setLayoutResources(resources)
-                            .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                            .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                            .build()
-                    )
-                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                    .build()
-            )
-    }
-
-    @Test
-    @OptIn(ComplicationExperimental::class)
-    public fun noDataComplicationData_list() {
-        val data = NoDataComplicationData(
-            ListComplicationData.Builder(
-                listOf(
-                    ShortTextComplicationData.Builder(
-                        "text".complicationText,
-                        ComplicationText.EMPTY
-                    )
-                        .build(),
-                    RangedValueComplicationData.Builder(
-                        value = 95f,
-                        min = 0f,
-                        max = 100f,
-                        ComplicationText.EMPTY
-                    )
-                        .setText("battery".complicationText)
-                        .build()
-                ),
-                ListComplicationData.StyleHint.RowOfColumns,
-                ComplicationText.EMPTY
-            ).build()
-        )
-
-        val wireShortText = WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
-            .setShortText(WireComplicationText.plainText("text"))
-            .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-            .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-            .build()
-
-        val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
-            .setRangedValue(95f)
-            .setRangedValueType(RangedValueTypes.UNDEFINED)
-            .setRangedMinValue(0f)
-            .setRangedMaxValue(100f)
-            .setShortText(WireComplicationText.plainText("battery"))
-            .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-            .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-            .build()
-
-        ParcelableSubject.assertThat(data.asWireComplicationData())
-            .hasSameSerializationAs(
-                WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholder(
-                        WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
-                            .setListEntryCollection(listOf(wireShortText, wireRangedValue))
-                            .setListStyleHint(
-                                ListComplicationData.StyleHint.RowOfColumns.ordinal
-                            )
-                            .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                            .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                            .build()
-                    )
-                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
-                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
-                    .build()
-            )
-    }
 }
 
 @RunWith(SharedRobolectricTestRunner::class)
@@ -3549,8 +3031,8 @@
         )
     }
 
-    @OptIn(ComplicationExperimental::class)
     @Test
+    @Suppress("NewApi")
     fun goalProgress() {
         val data = GoalProgressComplicationData.Builder(
             value = 1200f, targetValue = 10000f,
@@ -3571,7 +3053,7 @@
                 "persistencePolicy=0, displayPolicy=0)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
-            "ComplicationData{mType=-13, mFields=REDACTED}"
+            "ComplicationData{mType=13, mFields=REDACTED}"
         )
     }
 
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
index dbedd54..c70e7a6 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
@@ -29,6 +29,7 @@
 import org.junit.runner.RunWith
 
 @RunWith(SharedRobolectricTestRunner::class)
+@Suppress("NewApi")
 class PlaceholderTest {
     val text = "text".complicationText
     val title = "title".complicationText
diff --git a/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java b/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
index 96524a6..8ee1c50 100644
--- a/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
+++ b/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
@@ -16,6 +16,12 @@
 
 package androidx.wear.watchface.complications.rendering;
 
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import static java.lang.Math.toRadians;
+
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Canvas;
@@ -24,12 +30,14 @@
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Paint.Style;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.SweepGradient;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.graphics.drawable.Icon.OnDrawableLoadedListener;
@@ -46,6 +54,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.wear.watchface.complications.data.ImageKt;
 import androidx.wear.watchface.complications.data.RangedValueComplicationData;
+import androidx.wear.watchface.complications.data.RangedValueTypes;
 import androidx.wear.watchface.complications.rendering.utils.IconLayoutHelper;
 import androidx.wear.watchface.complications.rendering.utils.LargeImageLayoutHelper;
 import androidx.wear.watchface.complications.rendering.utils.LayoutHelper;
@@ -84,6 +93,9 @@
     @VisibleForTesting
     static final int STROKE_GAP_IN_DEGREES = 4;
 
+    /** The gap between in progress stroke for scores. */
+    static final int STROKE_GAP_IN_DEGREES_FOR_SCORE = 15;
+
     /**
      * Starting angle for ranged value, i.e. in progress part will start from this angle. As it's
      * drawn clockwise, -90 corresponds to 12 o'clock on a watch.
@@ -110,6 +122,18 @@
     @VisibleForTesting
     static final Paint PLACEHOLDER_PAINT = createPlaceHolderPaint();
 
+    /** Defines the placeholder shape for WeightedElementsComplicationData. */
+    private static final float[] PLACEHOLDER_WEIGHTS = {3.0f, 2.0f, 1.0f};
+
+    /** Defines the gap between weighted elements in degrees. */
+    private static final float WEIGHTED_ANGLE_GAP = 15.0f;
+
+    /** Used for goal progress to denote over achievement */
+    private static final float OVER_ACHIEVEMENT_ARC_LENGTH = 36.0f;
+
+    /** The fraction of the progress bar reserved for progress beyond the target. */
+    private static final float OVER_ACHIEVEMENT_FRACTION = 1.1f;
+
     private static Paint createPlaceHolderPaint() {
         Paint paint = new Paint();
         paint.setColor(Color.LTGRAY);
@@ -167,6 +191,8 @@
     @VisibleForTesting
     boolean mIsPlaceholderRangedValue;
     @VisibleForTesting
+    boolean mIsPlaceholderWeightedElements;
+    @VisibleForTesting
     boolean mIsPlaceholderTitle;
     @VisibleForTesting
     boolean mIsPlaceholderText;
@@ -283,6 +309,7 @@
         mIsPlaceholderSmallImage = false;
         mIsPlaceholderLargeImage = false;
         mIsPlaceholderRangedValue = false;
+        mIsPlaceholderWeightedElements = false;
         mIsPlaceholderTitle = false;
         mIsPlaceholderText = false;
         mIsPlaceholder = false;
@@ -297,8 +324,9 @@
                 mIsPlaceholderLargeImage =
                         data.hasLargeImage() && ImageKt.isPlaceholder(data.getLargeImage());
                 mIsPlaceholderRangedValue = data.hasRangedValue()
-                        && data.getRangedValue()
-                        == RangedValueComplicationData.PLACEHOLDER;
+                        && data.getRangedValue() == RangedValueComplicationData.PLACEHOLDER;
+                mIsPlaceholderWeightedElements = data.getElementWeights() != null
+                        && data.getElementWeights().length == 0;
                 if (data.getType() == ComplicationData.TYPE_LONG_TEXT) {
                     mIsPlaceholderTitle =
                             data.hasLongTitle() && data.getLongTitle().isPlaceholder();
@@ -450,6 +478,7 @@
         drawSmallImage(canvas, currentPaintSet, mIsPlaceholderSmallImage);
         drawLargeImage(canvas, currentPaintSet, mIsPlaceholderLargeImage);
         drawRangedValue(canvas, currentPaintSet, mIsPlaceholderRangedValue);
+        drawWeightedElements(canvas, currentPaintSet, mIsPlaceholderWeightedElements);
         drawMainText(canvas, currentPaintSet, mIsPlaceholderText);
         drawSubText(canvas, currentPaintSet, mIsPlaceholderTitle);
         // Draw highlight if highlighted
@@ -602,6 +631,11 @@
             canvas.drawRect(mRangedValueBoundsF, mDebugPaint);
         }
 
+        if (mComplicationData.getType() == ComplicationData.TYPE_GOAL_PROGRESS) {
+            drawGoalProgress(canvas, paintSet, isPlaceholder);
+            return;
+        }
+
         float rangedMinValue = mComplicationData.getRangedMinValue();
         float rangedMaxValue = mComplicationData.getRangedMaxValue();
         float rangedValue = mComplicationData.getRangedValue();
@@ -616,9 +650,17 @@
                 Math.min(rangedMaxValue, Math.max(rangedMinValue, rangedValue)) - rangedMinValue;
         float interval = rangedMaxValue - rangedMinValue;
         float progress = interval > 0 ? value / interval : 0;
+        int valueType = mComplicationData.getRangedValueType();
 
-        // do not need to draw gap in the cases of full circle
-        float gap = (progress > 0.0f && progress < 1.0f) ? STROKE_GAP_IN_DEGREES : 0.0f;
+        float gap;
+        if (valueType == RangedValueTypes.SCORE) {
+            gap = STROKE_GAP_IN_DEGREES_FOR_SCORE;
+        } else if (progress <= 0 || progress >= 1.0f) {
+            // We do not need to draw a gap when there's either 0% or 100% progress.
+            gap = 0.0f;
+        } else {
+            gap = STROKE_GAP_IN_DEGREES;
+        }
         float inProgressAngle = Math.max(0, 360.0f * progress - gap);
         float remainderAngle = Math.max(0, 360.0f * (1.0f - progress) - gap);
 
@@ -629,26 +671,203 @@
             PLACEHOLDER_PROGRESS_PAINT.setStrokeWidth(paintSet.mInProgressPaint.getStrokeWidth());
         }
 
-        // Draw the progress arc.
+        float startAngle = RANGED_VALUE_START_ANGLE + gap / 2.0f;
+        switch (valueType) {
+            case RangedValueTypes.SCORE: {
+                float sweepAngle = 360.0f - gap / 2;
+                drawProgressBarArc(canvas, isPlaceholder, paintSet, startAngle, sweepAngle);
+
+                // Draw the progress indicator.
+                float strokeWidth = paintSet.mInProgressPaint.getStrokeWidth();
+                float radiusX = mRangedValueBoundsF.width() * 0.5f;
+                float radiusY = mRangedValueBoundsF.height() * 0.5f;
+                float x = mRangedValueBoundsF.centerX()
+                        + radiusX * (float) cos(toRadians(startAngle + inProgressAngle));
+                float y = mRangedValueBoundsF.centerY()
+                        + radiusY * (float) sin(toRadians(startAngle + inProgressAngle));
+                canvas.drawCircle(x, y, strokeWidth,
+                        isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
+                break;
+            }
+
+            default:
+            case RangedValueTypes.UNDEFINED:
+                // Draw the arc represent by the value.
+                drawProgressBarArc(canvas, isPlaceholder, paintSet, startAngle, inProgressAngle);
+
+                // Draw an arc representing the remainder.
+                if (!isPlaceholder) {
+                    canvas.drawArc(
+                            mRangedValueBoundsF,
+                            startAngle + inProgressAngle + gap,
+                            remainderAngle,
+                            /* useCenter = */ false,
+                            paintSet.mRemainingPaint);
+                }
+                break;
+        }
+
+        mRangedValueBoundsF.inset(-insetAmount, -insetAmount);
+    }
+
+    private void drawGoalProgress(Canvas canvas, PaintSet paintSet, boolean isPlaceholder) {
+        float rangedMaxValue = mComplicationData.getTargetValue() * OVER_ACHIEVEMENT_FRACTION;
+        float rangedValue = mComplicationData.getRangedValue();
+
+        if (rangedValue > rangedMaxValue) {
+            rangedValue = rangedMaxValue;
+        }
+
+        if (isPlaceholder) {
+            rangedMaxValue = 100.0f;
+            rangedValue = 75.0f;
+        }
+
+        float value =  Math.min(rangedMaxValue, Math.max(0f, rangedValue));
+        float interval = rangedMaxValue;
+        float progress = interval > 0 ? value / interval : 0;
+        float gap = STROKE_GAP_IN_DEGREES_FOR_SCORE;
+        float inProgressAngle = Math.max(0, 360.0f * progress - gap);
+
+        int insetAmount = (int) Math.ceil(paintSet.mInProgressPaint.getStrokeWidth());
+        mRangedValueBoundsF.inset(insetAmount, insetAmount);
+
+        if (isPlaceholder) {
+            PLACEHOLDER_PROGRESS_PAINT.setStrokeWidth(paintSet.mInProgressPaint.getStrokeWidth());
+        }
+
+        float startAngle = RANGED_VALUE_START_ANGLE + gap / 2.0f;
+        float sweepAngle = 360.0f - gap / 2;
+
+        // Draw the fixed length progress arc.
+        drawProgressBarArc(canvas, isPlaceholder, paintSet, startAngle,
+                sweepAngle - OVER_ACHIEVEMENT_ARC_LENGTH);
+
+        // Draw the fixed length over-achievement achievement arc, resenting progress past the
+        // target.
+        int prevColor = paintSet.mInProgressPaint.getColor();
+        paintSet.mInProgressPaint.setColor(Color.RED);
         canvas.drawArc(
                 mRangedValueBoundsF,
-                RANGED_VALUE_START_ANGLE + gap / 2,
-                inProgressAngle,
-                false,
+                startAngle + sweepAngle - OVER_ACHIEVEMENT_ARC_LENGTH,
+                OVER_ACHIEVEMENT_ARC_LENGTH,
+                /* useCenter = */ false,
+                paintSet.mInProgressPaint);
+
+        paintSet.mInProgressPaint.setColor(prevColor);
+
+        // Draw the progress indicator circle.
+        float strokeWidth = paintSet.mInProgressPaint.getStrokeWidth();
+        float radiusX = mRangedValueBoundsF.width() * 0.5f;
+        float radiusY = mRangedValueBoundsF.height() * 0.5f;
+        float x = mRangedValueBoundsF.centerX()
+                + radiusX * (float) cos(toRadians(startAngle + inProgressAngle));
+        float y = mRangedValueBoundsF.centerY()
+                + radiusY * (float) sin(toRadians(startAngle + inProgressAngle));
+        canvas.drawCircle(x, y, strokeWidth,
                 isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
 
-        // Draw the remain arc.
-        if (!isPlaceholder) {
+        mRangedValueBoundsF.inset(-insetAmount, -insetAmount);
+    }
+
+    private void drawProgressBarArc(Canvas canvas, boolean isPlaceholder, PaintSet paintSet,
+            float startAngle, float sweepAngle) {
+        int[] colorRamp = mComplicationData.getColorRamp();
+        if (colorRamp != null) {
+            if (!checkNotNull(mComplicationData.isColorRampInterpolated())) {
+                drawNonInterpolatedColorRampArc(
+                        canvas, isPlaceholder, paintSet, startAngle, sweepAngle, colorRamp);
+                return;
+            }
+
+            // Set up the SweepGradient shader, rotated so the start is at the top (12 o'clock).
+            SweepGradient gradient =
+                    new SweepGradient(mRangedValueBoundsF.centerX(), mRangedValueBoundsF.centerY(),
+                            colorRamp, /* positions= */ null);
+            Matrix matrix = new Matrix();
+            matrix.postRotate(startAngle,
+                    mRangedValueBoundsF.centerX(), mRangedValueBoundsF.centerY());
+            gradient.setLocalMatrix(matrix);
+            paintSet.mInProgressPaint.setShader(gradient);
+        }
+        canvas.drawArc(
+                mRangedValueBoundsF,
+                startAngle,
+                sweepAngle,
+                false,
+                isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
+        paintSet.mInProgressPaint.setShader(null);
+    }
+
+    private void drawNonInterpolatedColorRampArc(
+            Canvas canvas, boolean isPlaceholder, PaintSet paintSet,
+            float startAngle, float sweepAngle, int[] colorRamp) {
+        // We need to draw the arc in segments of equal color.
+        float segmentSweepAngle = sweepAngle / (float) colorRamp.length;
+        int prevColor = paintSet.mInProgressPaint.getColor();
+        paintSet.mInProgressPaint.setColor(Color.RED);
+        for (int j : colorRamp) {
+            paintSet.mInProgressPaint.setColor(j);
             canvas.drawArc(
                     mRangedValueBoundsF,
-                    RANGED_VALUE_START_ANGLE
-                            + gap / 2.0f
-                            + inProgressAngle
-                            + gap,
-                    remainderAngle,
+                    startAngle,
+                    segmentSweepAngle,
                     false,
-                    paintSet.mRemainingPaint);
+                    isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
+            startAngle += segmentSweepAngle;
         }
+        paintSet.mInProgressPaint.setColor(prevColor);
+    }
+
+    private void drawWeightedElements(Canvas canvas, PaintSet paintSet, boolean isPlaceholder) {
+        if (mRangedValueBoundsF.isEmpty()
+                || mComplicationData.getType() != ComplicationData.TYPE_WEIGHTED_ELEMENTS) {
+            return;
+        }
+        if (DEBUG_MODE) {
+            canvas.drawRect(mRangedValueBoundsF, mDebugPaint);
+        }
+
+        int insetAmount = (int) Math.ceil(paintSet.mInProgressPaint.getStrokeWidth());
+        mRangedValueBoundsF.inset(insetAmount, insetAmount);
+
+        // Fill with the background color to show between elements.
+        if (!isPlaceholder) {
+            paintSet.mInProgressPaint.setColor(mComplicationData.getElementBackgroundColor());
+            canvas.drawArc(
+                    mRangedValueBoundsF,
+                    /* startAngle = */ 0f,
+                    /* sweepAngle = */ 360.0f,
+                    /* useCenter = */ false,
+                    paintSet.mInProgressPaint);
+        }
+
+        float[] weights = isPlaceholder ? PLACEHOLDER_WEIGHTS :
+                mComplicationData.getElementWeights();
+        int[] colors = mComplicationData.getElementColors();
+        float sum = 0;
+        for (float weight : weights) {
+            sum += weight;
+        }
+
+        // Only add gaps between elements if we have more than one value.
+        float gapAngle = (weights.length > 1) ? WEIGHTED_ANGLE_GAP * (float) weights.length : 0f;
+        float scale = (360.0f - gapAngle) / sum;
+
+        // Draw each element.
+        float angle = RANGED_VALUE_START_ANGLE;
+        for (int i = 0; i < weights.length; i++) {
+            float sweepLength = weights[i] * scale;
+            paintSet.mInProgressPaint.setColor(colors[i]);
+            canvas.drawArc(
+                    mRangedValueBoundsF,
+                    angle,
+                    sweepLength,
+                    /* useCenter = */ false,
+                    isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
+            angle += sweepLength + WEIGHTED_ANGLE_GAP;
+        }
+
         mRangedValueBoundsF.inset(-insetAmount, -insetAmount);
     }
 
@@ -790,7 +1009,9 @@
             case ComplicationData.TYPE_LONG_TEXT:
                 currentLayoutHelper = new LongTextLayoutHelper();
                 break;
+            case ComplicationData.TYPE_GOAL_PROGRESS:
             case ComplicationData.TYPE_RANGED_VALUE:
+            case ComplicationData.TYPE_WEIGHTED_ELEMENTS:
                 if (mRangedValueProgressHidden) {
                     if (mComplicationData.getShortText() == null) {
                         currentLayoutHelper = new IconLayoutHelper();
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
index a019e7b..e9b11db 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
@@ -103,7 +103,7 @@
     @SuppressWarnings("deprecation") // b/251211092
     @Before
     public void setUp() {
-        MockitoAnnotations.initMocks(this);
+        MockitoAnnotations.openMocks(this);
         mComplicationDrawable = new ComplicationDrawable();
         mComplicationDrawable.setCallback(mMockDrawableCallback);
 
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
index 72795cd..f5f9868 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
@@ -110,7 +110,7 @@
     @SuppressWarnings("deprecation") // b/251211092
     @Before
     public void setUp() {
-        MockitoAnnotations.initMocks(this);
+        MockitoAnnotations.openMocks(this);
         mComplicationBounds = new Rect(0, 0, BOUNDS_WIDTH, BOUNDS_HEIGHT);
 
         mComplicationRenderer = createRendererWithBounds(mComplicationBounds);
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/RoundedDrawableTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/RoundedDrawableTest.java
index ac2c452..3ffc761 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/RoundedDrawableTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/RoundedDrawableTest.java
@@ -55,7 +55,7 @@
     @SuppressWarnings("deprecation") // b/251211092
     @Before
     public void setUp() {
-        MockitoAnnotations.initMocks(this);
+        MockitoAnnotations.openMocks(this);
         mRoundedDrawable = new RoundedDrawable();
         mBitmapDrawable =
                 new BitmapDrawable(
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
index 941d1a0..af75b55 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
@@ -75,7 +75,12 @@
     /** Reserved field */
     @ParcelField(101)
     @Nullable
-    ComponentName mAuxiliaryComponentName;
+    String mAuxiliaryComponentClassName;
+
+    /** Reserved field */
+    @ParcelField(102)
+    @Nullable
+    String mAuxiliaryComponentPackageName;
 
     /** Used by VersionedParcelable. */
     WallpaperInteractiveWatchFaceInstanceParams() {
@@ -93,7 +98,10 @@
         mWatchUiState = watchUiState;
         mUserStyle = userStyle;
         mIdAndComplicationDataWireFormats = idAndComplicationDataWireFormats;
-        mAuxiliaryComponentName = auxiliaryComponentName;
+        if (auxiliaryComponentName != null) {
+            mAuxiliaryComponentClassName = auxiliaryComponentName.getClassName();
+            mAuxiliaryComponentPackageName = auxiliaryComponentName.getPackageName();
+        }
     }
 
     @NonNull
diff --git a/wear/watchface/watchface-samples-minimal-complications/build.gradle b/wear/watchface/watchface-samples-minimal-complications/build.gradle
index ec59cdc..1bbe66d 100644
--- a/wear/watchface/watchface-samples-minimal-complications/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-complications/build.gradle
@@ -44,6 +44,7 @@
 
 android {
     defaultConfig {
+        applicationId "androidx.wear.watchface.samples.minimal.complications"
         minSdkVersion 26
     }
 
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/AndroidManifest.xml b/wear/watchface/watchface-samples-minimal-complications/src/main/AndroidManifest.xml
index f0b904f..4318358 100644
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface-samples-minimal-complications/src/main/AndroidManifest.xml
@@ -1,67 +1,65 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
-  <uses-feature android:name="android.hardware.type.watch" />
-
-  <uses-permission android:name="android.permission.WAKE_LOCK" />
-  <uses-permission
-      android:name="com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA" />
-
-  <application
-      android:allowBackup="true"
-      android:icon="@mipmap/ic_launcher"
-      android:label="@string/app_name"
-      android:supportsRtl="true"
-      android:theme="@android:style/Theme.DeviceDefault"
-      android:fullBackupContent="false">
-
-    <activity
-        android:name=".ConfigActivity"
-        android:exported="true"
-        android:label="@string/configuration_title">
-      <intent-filter>
-        <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
-
-        <category
-            android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
-        <category android:name="android.intent.category.DEFAULT" />
-      </intent-filter>
-    </activity>
-
-    <service
-        android:name=".WatchFaceService"
-        android:directBootAware="true"
-        android:exported="true"
+    <application
+        android:allowBackup="true"
+        android:fullBackupContent="false"
+        android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
-        android:permission="android.permission.BIND_WALLPAPER">
+        android:supportsRtl="true"
+        android:theme="@android:style/Theme.DeviceDefault">
 
-      <intent-filter>
-        <action android:name="android.service.wallpaper.WallpaperService" />
-        <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
-      </intent-filter>
+        <activity
+            android:name=".ConfigActivity"
+            android:exported="true"
+            android:label="@string/configuration_title">
+            <intent-filter>
+                <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.preview"
-          android:resource="@drawable/preview" />
+                <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
 
-      <meta-data
-          android:name="android.service.wallpaper"
-          android:resource="@xml/watch_face" />
+        <service
+            android:name=".WatchFaceService"
+            android:directBootAware="true"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:permission="android.permission.BIND_WALLPAPER">
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
-          android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
+            <intent-filter>
+                <action android:name="android.service.wallpaper.WallpaperService" />
+                <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+            </intent-filter>
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
-          android:value="true" />
+            <meta-data
+                android:name="com.google.android.wearable.watchface.preview"
+                android:resource="@drawable/preview" />
 
-      <meta-data
-          android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
-          android:resource="@xml/xml_watchface" />
+            <meta-data
+                android:name="android.service.wallpaper"
+                android:resource="@xml/watch_face" />
 
-    </service>
+            <meta-data
+                android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+                android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
 
-  </application>
+            <meta-data
+                android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
+                android:value="true" />
+
+            <meta-data
+                android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
+                android:resource="@xml/xml_watchface" />
+
+        </service>
+
+    </application>
+
+    <uses-feature android:name="android.hardware.type.watch" />
+    <uses-permission android:name="com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA" />
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
 
 </manifest>
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/BaseFutureCallback.java b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/BaseFutureCallback.java
deleted file mode 100644
index 8b254a9..0000000
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/BaseFutureCallback.java
+++ /dev/null
@@ -1,64 +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.wear.watchface.samples.minimal.complications;
-
-import android.content.Context;
-import android.util.Log;
-import android.widget.Toast;
-
-/** A base class for a {@link FutureCallback} that logs the outcome. */
-abstract class BaseFutureCallback<T> implements FutureCallback<T> {
-
-    private final Context mContext;
-    private final String mTag;
-    private final String mName;
-
-    BaseFutureCallback(Context context, String tag, String name) {
-        mContext = context;
-        mTag = tag;
-        mName = name;
-    }
-
-    @Override
-    public void onPending() {
-        Log.d(mTag, mName + ".onPending()");
-    }
-
-    @Override
-    public void onSuccess(T value) {
-        Log.d(mTag, mName + ".onSuccess(" + value + ")");
-    }
-
-    @Override
-    public void onFailure(Throwable throwable) {
-        Log.d(mTag, mName + ".onFailure(" + throwable.getMessage() + ")", throwable);
-        Toast.makeText(mContext, "Failure", Toast.LENGTH_LONG).show();
-    }
-
-    @Override
-    public void onCancelled() {
-        Log.d(mTag, mName + ".onCancelled()");
-        Toast.makeText(mContext, "Cancelled", Toast.LENGTH_LONG).show();
-    }
-
-    @Override
-    public void onInterrupted() {
-        Thread.currentThread().interrupt();
-        Log.d(mTag, mName + ".onInterrupted()");
-        Toast.makeText(mContext, "Interrupted", Toast.LENGTH_LONG).show();
-    }
-}
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
index 580fea5..f46584f 100644
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
+++ b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
@@ -18,8 +18,6 @@
 
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.util.Log;
 import android.view.View;
 import android.widget.ImageView;
@@ -27,6 +25,7 @@
 
 import androidx.activity.ComponentActivity;
 import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
 import androidx.lifecycle.FlowLiveDataConversions;
 import androidx.wear.watchface.complications.ComplicationDataSourceInfo;
 import androidx.wear.watchface.complications.data.ComplicationData;
@@ -35,22 +34,13 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutionException;
 
 /** Configuration activity for the watch face. */
 public class ConfigActivity extends ComponentActivity {
 
     private static final String TAG = "ConfigActivity";
 
-    private Executor mMainExecutor = new Executor() {
-        private final Handler mHandler = new Handler(Looper.getMainLooper());
-
-        @Override
-        public void execute(Runnable runnable) {
-            mHandler.post(runnable);
-        }
-    };
-
     private TextView mComplicationProviderName;
     private ImageView mComplicationPreview;
     private ComplicationDrawable mComplicationPreviewDrawable;
@@ -58,23 +48,11 @@
     @Nullable
     private ListenableEditorSession mEditorSession;
 
-    public ConfigActivity() {
-        addCallback(
-                ListenableEditorSession.listenableCreateOnWatchEditorSession(this),
-                new BaseFutureCallback<ListenableEditorSession>(
-                        this, TAG, "listenableCreateOnWatchEditingSession") {
-                    @Override
-                    public void onSuccess(ListenableEditorSession editorSession) {
-                        super.onSuccess(editorSession);
-                        setEditorSession(editorSession);
-                    }
-                });
-    }
-
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.config_activity_layout);
+        listenForEditorSession();
 
         mComplicationProviderName = findViewById(R.id.complication_provider_name);
         mComplicationPreview = findViewById(R.id.complication_preview);
@@ -88,8 +66,32 @@
         }
     }
 
-    private void setEditorSession(ListenableEditorSession editorSession) {
-        ConfigActivity.this.mEditorSession = editorSession;
+    @Override
+    protected void onDestroy() {
+        finish();
+        super.onDestroy();
+    }
+
+    private void listenForEditorSession() {
+        ListenableFuture<ListenableEditorSession> editorSessionFuture =
+                ListenableEditorSession.listenableCreateOnWatchEditorSession(this);
+        editorSessionFuture.addListener(() -> {
+            ListenableEditorSession editorSession;
+            try {
+                editorSession = editorSessionFuture.get();
+            } catch (ExecutionException | InterruptedException e) {
+                e.printStackTrace();
+                return;
+            }
+            if (editorSession == null) {
+                return;
+            }
+            mEditorSession = editorSession;
+            observeComplications();
+        }, ContextCompat.getMainExecutor(this));
+    }
+
+    private void observeComplications() {
         FlowLiveDataConversions.asLiveData(mEditorSession.getComplicationsDataSourceInfo()).observe(
                 this,
                 complicationDataSourceInfoMap -> {
@@ -138,16 +140,7 @@
         mEditorSession
                 .listenableOpenComplicationDataSourceChooser(
                         WatchFaceService.getComplicationId(getResources())
-                ).addListener(() -> { /* Empty on purpose. */ }, mMainExecutor);
-    }
-
-    @Override
-    protected void onDestroy() {
-        finish();
-        super.onDestroy();
-    }
-
-    private <T> void addCallback(ListenableFuture<T> future, FutureCallback<T> callback) {
-        FutureCallback.addCallback(future, callback, mMainExecutor);
+                ).addListener(() -> { /* Empty on purpose. */ },
+                        ContextCompat.getMainExecutor(this));
     }
 }
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/FutureCallback.java b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/FutureCallback.java
deleted file mode 100644
index 1a629dd..0000000
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/FutureCallback.java
+++ /dev/null
@@ -1,63 +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.wear.watchface.samples.minimal.complications;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-
-/** A callback for a future that explicitly handles different types of outcomes. */
-interface FutureCallback<T> {
-
-    static <T> void addCallback(
-            ListenableFuture<T> future, FutureCallback<T> callback, Executor executor) {
-        if (!future.isDone()) {
-            callback.onPending();
-        }
-        future.addListener(
-                () -> {
-                    if (future.isCancelled()) {
-                        callback.onCancelled();
-                    } else {
-                        try {
-                            callback.onSuccess(future.get());
-                        } catch (InterruptedException e) {
-                            callback.onInterrupted();
-                        } catch (ExecutionException e) {
-                            callback.onFailure(e.getCause());
-                        }
-                    }
-                },
-                executor);
-    }
-
-    /** Called immediately if a callback is added to a future that is not yet done. */
-    void onPending();
-
-    /** Called if the future returns a value. */
-    void onSuccess(T value);
-
-    /** Called if the future throws an exception. */
-    void onFailure(Throwable throwable);
-
-    /** Called if the future is interrupted. */
-    void onInterrupted();
-
-    /** Called if the future is cancelled. */
-    void onCancelled();
-}
diff --git a/wear/watchface/watchface-samples-minimal-instances/build.gradle b/wear/watchface/watchface-samples-minimal-instances/build.gradle
index 2931105..4249bd3 100644
--- a/wear/watchface/watchface-samples-minimal-instances/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-instances/build.gradle
@@ -43,6 +43,7 @@
 
 android {
     defaultConfig {
+        applicationId "androidx.wear.watchface.samples.minimal.instances"
         minSdkVersion 26
     }
 
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/AndroidManifest.xml b/wear/watchface/watchface-samples-minimal-instances/src/main/AndroidManifest.xml
index 2b1f8b1..52e37d3 100644
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface-samples-minimal-instances/src/main/AndroidManifest.xml
@@ -1,68 +1,67 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
-  <uses-feature android:name="android.hardware.type.watch" />
-
-  <uses-permission android:name="android.permission.WAKE_LOCK" />
-
-  <application
-      android:allowBackup="true"
-      android:icon="@mipmap/ic_launcher"
-      android:label="@string/app_name"
-      android:supportsRtl="true"
-      android:theme="@android:style/Theme.DeviceDefault"
-      android:fullBackupContent="false">
-
-    <activity
-        android:name="androidx.wear.watchface.samples.minimal.instances.ConfigActivity"
-        android:exported="true"
-        android:label="@string/configuration_title">
-      <intent-filter>
-        <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
-
-        <category
-            android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
-        <category android:name="android.intent.category.DEFAULT" />
-      </intent-filter>
-    </activity>
-
-    <service
-        android:name="androidx.wear.watchface.samples.minimal.instances.WatchFaceService"
-        android:directBootAware="true"
-        android:exported="true"
+    <application
+        android:allowBackup="true"
+        android:fullBackupContent="false"
+        android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
-        android:permission="android.permission.BIND_WALLPAPER">
+        android:supportsRtl="true"
+        android:theme="@android:style/Theme.DeviceDefault">
 
-      <intent-filter>
-        <action android:name="android.service.wallpaper.WallpaperService" />
-        <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
-      </intent-filter>
+        <activity
+            android:name=".ConfigActivity"
+            android:exported="true"
+            android:label="@string/configuration_title">
+            <intent-filter>
+                <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.preview"
-          android:resource="@drawable/preview" />
+                <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
 
-      <meta-data
-          android:name="android.service.wallpaper"
-          android:resource="@xml/watch_face" />
+        <service
+            android:name=".WatchFaceService"
+            android:directBootAware="true"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:permission="android.permission.BIND_WALLPAPER">
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
-          android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
+            <intent-filter>
+                <action android:name="android.service.wallpaper.WallpaperService" />
+                <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+            </intent-filter>
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
-          android:value="true" />
+            <meta-data
+                android:name="com.google.android.wearable.watchface.preview"
+                android:resource="@drawable/preview" />
 
-      <meta-data
-          android:name="androidx.wear.watchface.MULTIPLE_INSTANCES_ALLOWED"
-          android:value="true" />
+            <meta-data
+                android:name="android.service.wallpaper"
+                android:resource="@xml/watch_face" />
 
-      <meta-data
-          android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
-          android:resource="@xml/xml_watchface" />
-    </service>
+            <meta-data
+                android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+                android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
 
-  </application>
+            <meta-data
+                android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
+                android:value="true" />
+
+            <meta-data
+                android:name="androidx.wear.watchface.MULTIPLE_INSTANCES_ALLOWED"
+                android:value="true" />
+
+            <meta-data
+                android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
+                android:resource="@xml/xml_watchface" />
+        </service>
+
+    </application>
+
+    <uses-feature android:name="android.hardware.type.watch" />
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
 
 </manifest>
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/BaseFutureCallback.java b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/BaseFutureCallback.java
deleted file mode 100644
index 3f7d131..0000000
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/BaseFutureCallback.java
+++ /dev/null
@@ -1,64 +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.wear.watchface.samples.minimal.instances;
-
-import android.content.Context;
-import android.util.Log;
-import android.widget.Toast;
-
-/** A base class for a {@link FutureCallback} that logs the outcome. */
-abstract class BaseFutureCallback<T> implements FutureCallback<T> {
-
-    private final Context mContext;
-    private final String mTag;
-    private final String mName;
-
-    BaseFutureCallback(Context context, String tag, String name) {
-        mContext = context;
-        mTag = tag;
-        mName = name;
-    }
-
-    @Override
-    public void onPending() {
-        Log.d(mTag, mName + ".onPending()");
-    }
-
-    @Override
-    public void onSuccess(T value) {
-        Log.d(mTag, mName + ".onSuccess(" + value + ")");
-    }
-
-    @Override
-    public void onFailure(Throwable throwable) {
-        Log.d(mTag, mName + ".onFailure(" + throwable.getMessage() + ")", throwable);
-        Toast.makeText(mContext, "Failure", Toast.LENGTH_LONG).show();
-    }
-
-    @Override
-    public void onCancelled() {
-        Log.d(mTag, mName + ".onCancelled()");
-        Toast.makeText(mContext, "Cancelled", Toast.LENGTH_LONG).show();
-    }
-
-    @Override
-    public void onInterrupted() {
-        Thread.currentThread().interrupt();
-        Log.d(mTag, mName + ".onInterrupted()");
-        Toast.makeText(mContext, "Interrupted", Toast.LENGTH_LONG).show();
-    }
-}
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/ConfigActivity.java b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/ConfigActivity.java
index e53dbbd..fc4c23a 100644
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/ConfigActivity.java
+++ b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/ConfigActivity.java
@@ -27,6 +27,7 @@
 
 import androidx.activity.ComponentActivity;
 import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
 import androidx.wear.watchface.editor.ListenableEditorSession;
 import androidx.wear.watchface.style.MutableUserStyle;
 import androidx.wear.watchface.style.UserStyleSetting;
@@ -35,6 +36,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.Objects;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 
 /** Configuration activity for the watch face. */
@@ -42,15 +44,6 @@
 
     private static final String TAG = "ConfigActivity";
 
-    private final Executor mMainExecutor = new Executor() {
-        private final Handler mHandler = new Handler(Looper.getMainLooper());
-
-        @Override
-        public void execute(Runnable runnable) {
-            mHandler.post(runnable);
-        }
-    };
-
     private CurvedTextView mInstanceId;
     private TextView mStyleValue;
     private final UserStyleSetting.Id mTimeStyleId = new UserStyleSetting.Id("TimeStyle");
@@ -58,25 +51,11 @@
     @Nullable
     private ListenableEditorSession mEditorSession;
 
-    public ConfigActivity() {
-        addCallback(
-                ListenableEditorSession.listenableCreateOnWatchEditorSession(this),
-                new BaseFutureCallback<ListenableEditorSession>(
-                        this, TAG, "listenableCreateOnWatchEditingSession") {
-                    @Override
-                    public void onSuccess(ListenableEditorSession editorSession) {
-                        super.onSuccess(editorSession);
-                        mEditorSession = editorSession;
-                        updateInstanceId();
-                        updateStyleValue();
-                    }
-                });
-    }
-
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.config_activity_layout);
+        listenForEditorSession();
 
         mInstanceId = findViewById(R.id.instance_id);
         mStyleValue = findViewById(R.id.style_value);
@@ -96,6 +75,26 @@
         super.onDestroy();
     }
 
+    private void listenForEditorSession() {
+        ListenableFuture<ListenableEditorSession> editorSessionFuture =
+                ListenableEditorSession.listenableCreateOnWatchEditorSession(this);
+        editorSessionFuture.addListener(() -> {
+            ListenableEditorSession editorSession;
+            try {
+                editorSession = editorSessionFuture.get();
+            } catch (ExecutionException | InterruptedException e) {
+                e.printStackTrace();
+                return;
+            }
+            if (editorSession == null) {
+                return;
+            }
+            mEditorSession = editorSession;
+            updateInstanceId();
+            updateStyleValue();
+        }, ContextCompat.getMainExecutor(this));
+    }
+
     private void changeStyle() {
         Log.d(TAG, "changeStyle");
         if (mEditorSession == null) {
@@ -136,8 +135,4 @@
                 (ListOption) mEditorSession.getUserStyle().getValue().get(mTimeStyleId);
         mStyleValue.setText(option.getDisplayName());
     }
-
-    private <T> void addCallback(ListenableFuture<T> future, FutureCallback<T> callback) {
-        FutureCallback.addCallback(future, callback, mMainExecutor);
-    }
 }
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/FutureCallback.java b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/FutureCallback.java
deleted file mode 100644
index 4c12b45..0000000
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/FutureCallback.java
+++ /dev/null
@@ -1,63 +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.wear.watchface.samples.minimal.instances;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-
-/** A callback for a future that explicitly handles different types of outcomes. */
-interface FutureCallback<T> {
-
-    static <T> void addCallback(
-            ListenableFuture<T> future, FutureCallback<T> callback, Executor executor) {
-        if (!future.isDone()) {
-            callback.onPending();
-        }
-        future.addListener(
-                () -> {
-                    if (future.isCancelled()) {
-                        callback.onCancelled();
-                    } else {
-                        try {
-                            callback.onSuccess(future.get());
-                        } catch (InterruptedException e) {
-                            callback.onInterrupted();
-                        } catch (ExecutionException e) {
-                            callback.onFailure(e.getCause());
-                        }
-                    }
-                },
-                executor);
-    }
-
-    /** Called immediately if a callback is added to a future that is not yet done. */
-    void onPending();
-
-    /** Called if the future returns a value. */
-    void onSuccess(T value);
-
-    /** Called if the future throws an exception. */
-    void onFailure(Throwable throwable);
-
-    /** Called if the future is interrupted. */
-    void onInterrupted();
-
-    /** Called if the future is cancelled. */
-    void onCancelled();
-}
diff --git a/wear/watchface/watchface-samples-minimal-style/build.gradle b/wear/watchface/watchface-samples-minimal-style/build.gradle
index f2d59f2..df0cb91 100644
--- a/wear/watchface/watchface-samples-minimal-style/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-style/build.gradle
@@ -41,6 +41,7 @@
 
 android {
     defaultConfig {
+        applicationId "androidx.wear.watchface.samples.minimal.style"
         minSdkVersion 26
     }
 
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/AndroidManifest.xml b/wear/watchface/watchface-samples-minimal-style/src/main/AndroidManifest.xml
index 404d473..79fc1fc 100644
--- a/wear/watchface/watchface-samples-minimal-style/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface-samples-minimal-style/src/main/AndroidManifest.xml
@@ -1,64 +1,63 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
-  <uses-feature android:name="android.hardware.type.watch" />
-
-  <uses-permission android:name="android.permission.WAKE_LOCK" />
-
-  <application
-      android:allowBackup="true"
-      android:icon="@mipmap/ic_launcher"
-      android:label="@string/app_name"
-      android:supportsRtl="true"
-      android:theme="@android:style/Theme.DeviceDefault"
-      android:fullBackupContent="false">
-
-    <activity
-        android:name=".ConfigActivity"
-        android:exported="true"
-        android:label="@string/configuration_title">
-      <intent-filter>
-        <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
-
-        <category
-            android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
-        <category android:name="android.intent.category.DEFAULT" />
-      </intent-filter>
-    </activity>
-
-    <service
-        android:name=".WatchFaceService"
-        android:directBootAware="true"
-        android:exported="true"
+    <application
+        android:allowBackup="true"
+        android:fullBackupContent="false"
+        android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
-        android:permission="android.permission.BIND_WALLPAPER">
+        android:supportsRtl="true"
+        android:theme="@android:style/Theme.DeviceDefault">
 
-      <intent-filter>
-        <action android:name="android.service.wallpaper.WallpaperService" />
-        <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
-      </intent-filter>
+        <activity
+            android:name=".ConfigActivity"
+            android:exported="true"
+            android:label="@string/configuration_title">
+            <intent-filter>
+                <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.preview"
-          android:resource="@drawable/preview" />
+                <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
 
-      <meta-data
-          android:name="android.service.wallpaper"
-          android:resource="@xml/watch_face" />
+        <service
+            android:name=".WatchFaceService"
+            android:directBootAware="true"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:permission="android.permission.BIND_WALLPAPER">
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
-          android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
+            <intent-filter>
+                <action android:name="android.service.wallpaper.WallpaperService" />
+                <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+            </intent-filter>
 
-      <meta-data
-          android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
-          android:value="true" />
+            <meta-data
+                android:name="com.google.android.wearable.watchface.preview"
+                android:resource="@drawable/preview" />
 
-      <meta-data
-          android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
-          android:resource="@xml/xml_watchface" />
-    </service>
+            <meta-data
+                android:name="android.service.wallpaper"
+                android:resource="@xml/watch_face" />
 
-  </application>
+            <meta-data
+                android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+                android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
+
+            <meta-data
+                android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
+                android:value="true" />
+
+            <meta-data
+                android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
+                android:resource="@xml/xml_watchface" />
+        </service>
+
+    </application>
+
+    <uses-feature android:name="android.hardware.type.watch" />
+
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
 
 </manifest>
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/BaseFutureCallback.java b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/BaseFutureCallback.java
deleted file mode 100644
index f1499b7..0000000
--- a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/BaseFutureCallback.java
+++ /dev/null
@@ -1,64 +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.wear.watchface.samples.minimal.style;
-
-import android.content.Context;
-import android.util.Log;
-import android.widget.Toast;
-
-/** A base class for a {@link FutureCallback} that logs the outcome. */
-abstract class BaseFutureCallback<T> implements FutureCallback<T> {
-
-    private final Context mContext;
-    private final String mTag;
-    private final String mName;
-
-    BaseFutureCallback(Context context, String tag, String name) {
-        mContext = context;
-        mTag = tag;
-        mName = name;
-    }
-
-    @Override
-    public void onPending() {
-        Log.d(mTag, mName + ".onPending()");
-    }
-
-    @Override
-    public void onSuccess(T value) {
-        Log.d(mTag, mName + ".onSuccess(" + value + ")");
-    }
-
-    @Override
-    public void onFailure(Throwable throwable) {
-        Log.d(mTag, mName + ".onFailure(" + throwable.getMessage() + ")", throwable);
-        Toast.makeText(mContext, "Failure", Toast.LENGTH_LONG).show();
-    }
-
-    @Override
-    public void onCancelled() {
-        Log.d(mTag, mName + ".onCancelled()");
-        Toast.makeText(mContext, "Cancelled", Toast.LENGTH_LONG).show();
-    }
-
-    @Override
-    public void onInterrupted() {
-        Thread.currentThread().interrupt();
-        Log.d(mTag, mName + ".onInterrupted()");
-        Toast.makeText(mContext, "Interrupted", Toast.LENGTH_LONG).show();
-    }
-}
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
index 288ba72..ec29ff6 100644
--- a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
+++ b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
@@ -20,59 +20,36 @@
 import static androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption;
 
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.util.Log;
 import android.widget.TextView;
 
 import androidx.activity.ComponentActivity;
 import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
 import androidx.wear.watchface.editor.ListenableEditorSession;
 import androidx.wear.watchface.style.MutableUserStyle;
 import androidx.wear.watchface.style.UserStyleSetting;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutionException;
 
 /** Configuration activity for the watch face. */
 public class ConfigActivity extends ComponentActivity {
 
     private static final String TAG = "ConfigActivity";
 
-    private final Executor mMainExecutor = new Executor() {
-        private final Handler mHandler = new Handler(Looper.getMainLooper());
-
-        @Override
-        public void execute(Runnable runnable) {
-            mHandler.post(runnable);
-        }
-    };
-
     private TextView mStyleValue;
     private final UserStyleSetting.Id mTimeStyleId = new UserStyleSetting.Id("TimeStyle");
 
     @Nullable
     private ListenableEditorSession mEditorSession;
 
-    public ConfigActivity() {
-        addCallback(
-                ListenableEditorSession.listenableCreateOnWatchEditorSession(this),
-                new BaseFutureCallback<ListenableEditorSession>(
-                        this, TAG, "listenableCreateOnWatchEditingSession") {
-                    @Override
-                    public void onSuccess(ListenableEditorSession editorSession) {
-                        super.onSuccess(editorSession);
-                        mEditorSession = editorSession;
-                        updateStyleValue();
-                    }
-                });
-    }
-
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.config_activity_layout);
+        listenForEditorSession();
 
         mStyleValue = findViewById(R.id.style_value);
 
@@ -91,6 +68,25 @@
         super.onDestroy();
     }
 
+    private void listenForEditorSession() {
+        ListenableFuture<ListenableEditorSession> editorSessionFuture =
+                ListenableEditorSession.listenableCreateOnWatchEditorSession(this);
+        editorSessionFuture.addListener(() -> {
+            ListenableEditorSession editorSession;
+            try {
+                editorSession = editorSessionFuture.get();
+            } catch (ExecutionException | InterruptedException e) {
+                e.printStackTrace();
+                return;
+            }
+            if (editorSession == null) {
+                return;
+            }
+            mEditorSession = editorSession;
+            updateStyleValue();
+        }, ContextCompat.getMainExecutor(this));
+    }
+
     private void changeStyle() {
         Log.d(TAG, "changeStyle");
         if (mEditorSession == null) {
@@ -124,8 +120,4 @@
                 (ListOption) mEditorSession.getUserStyle().getValue().get(mTimeStyleId);
         mStyleValue.setText(option.getDisplayName());
     }
-
-    private <T> void addCallback(ListenableFuture<T> future, FutureCallback<T> callback) {
-        FutureCallback.addCallback(future, callback, mMainExecutor);
-    }
 }
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/FutureCallback.java b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/FutureCallback.java
deleted file mode 100644
index 12f62de..0000000
--- a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/FutureCallback.java
+++ /dev/null
@@ -1,63 +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.wear.watchface.samples.minimal.style;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-
-/** A callback for a future that explicitly handles different types of outcomes. */
-interface FutureCallback<T> {
-
-    static <T> void addCallback(
-            ListenableFuture<T> future, FutureCallback<T> callback, Executor executor) {
-        if (!future.isDone()) {
-            callback.onPending();
-        }
-        future.addListener(
-                () -> {
-                    if (future.isCancelled()) {
-                        callback.onCancelled();
-                    } else {
-                        try {
-                            callback.onSuccess(future.get());
-                        } catch (InterruptedException e) {
-                            callback.onInterrupted();
-                        } catch (ExecutionException e) {
-                            callback.onFailure(e.getCause());
-                        }
-                    }
-                },
-                executor);
-    }
-
-    /** Called immediately if a callback is added to a future that is not yet done. */
-    void onPending();
-
-    /** Called if the future returns a value. */
-    void onSuccess(T value);
-
-    /** Called if the future throws an exception. */
-    void onFailure(Throwable throwable);
-
-    /** Called if the future is interrupted. */
-    void onInterrupted();
-
-    /** Called if the future is cancelled. */
-    void onCancelled();
-}
diff --git a/wear/watchface/watchface/api/current.txt b/wear/watchface/watchface/api/current.txt
index d19bba9..1319949 100644
--- a/wear/watchface/watchface/api/current.txt
+++ b/wear/watchface/watchface/api/current.txt
@@ -41,7 +41,7 @@
     method @UiThread public Integer? getNameResourceId();
     method public androidx.wear.watchface.CanvasComplication getRenderer();
     method @UiThread public Integer? getScreenReaderNameResourceId();
-    method @UiThread public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
+    method public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
     method public androidx.wear.watchface.ComplicationTapFilter getTapFilter();
     method public boolean isActiveAt(java.time.Instant instant);
     method @UiThread public boolean isEnabled();
@@ -63,7 +63,7 @@
     property @UiThread public final Integer? nameResourceId;
     property public final androidx.wear.watchface.CanvasComplication renderer;
     property @UiThread public final Integer? screenReaderNameResourceId;
-    property @UiThread public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
+    property public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
     property public final androidx.wear.watchface.ComplicationTapFilter tapFilter;
     field public static final androidx.wear.watchface.ComplicationSlot.Companion Companion;
   }
diff --git a/wear/watchface/watchface/api/public_plus_experimental_current.txt b/wear/watchface/watchface/api/public_plus_experimental_current.txt
index 7763240..8965e97 100644
--- a/wear/watchface/watchface/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface/api/public_plus_experimental_current.txt
@@ -55,7 +55,7 @@
     method @UiThread public Integer? getNameResourceId();
     method public androidx.wear.watchface.CanvasComplication getRenderer();
     method @UiThread public Integer? getScreenReaderNameResourceId();
-    method @UiThread public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
+    method public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
     method public androidx.wear.watchface.ComplicationTapFilter getTapFilter();
     method public boolean isActiveAt(java.time.Instant instant);
     method @UiThread public boolean isEnabled();
@@ -77,7 +77,7 @@
     property @UiThread public final Integer? nameResourceId;
     property public final androidx.wear.watchface.CanvasComplication renderer;
     property @UiThread public final Integer? screenReaderNameResourceId;
-    property @UiThread public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
+    property public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
     property public final androidx.wear.watchface.ComplicationTapFilter tapFilter;
     field public static final androidx.wear.watchface.ComplicationSlot.Companion Companion;
   }
diff --git a/wear/watchface/watchface/api/restricted_current.txt b/wear/watchface/watchface/api/restricted_current.txt
index d19bba9..1319949 100644
--- a/wear/watchface/watchface/api/restricted_current.txt
+++ b/wear/watchface/watchface/api/restricted_current.txt
@@ -41,7 +41,7 @@
     method @UiThread public Integer? getNameResourceId();
     method public androidx.wear.watchface.CanvasComplication getRenderer();
     method @UiThread public Integer? getScreenReaderNameResourceId();
-    method @UiThread public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
+    method public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
     method public androidx.wear.watchface.ComplicationTapFilter getTapFilter();
     method public boolean isActiveAt(java.time.Instant instant);
     method @UiThread public boolean isEnabled();
@@ -63,7 +63,7 @@
     property @UiThread public final Integer? nameResourceId;
     property public final androidx.wear.watchface.CanvasComplication renderer;
     property @UiThread public final Integer? screenReaderNameResourceId;
-    property @UiThread public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
+    property public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
     property public final androidx.wear.watchface.ComplicationTapFilter tapFilter;
     field public static final androidx.wear.watchface.ComplicationSlot.Companion Companion;
   }
diff --git a/wear/watchface/watchface/samples/minimal/build.gradle b/wear/watchface/watchface/samples/minimal/build.gradle
index 8896dd1..f8f155d 100644
--- a/wear/watchface/watchface/samples/minimal/build.gradle
+++ b/wear/watchface/watchface/samples/minimal/build.gradle
@@ -37,6 +37,7 @@
 
 android {
     defaultConfig {
+        applicationId "androidx.wear.watchface.samples.minimal"
         minSdkVersion 26
     }
 
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index e22627f..d2186c6 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -28,10 +28,7 @@
 import android.graphics.drawable.Icon
 import android.view.SurfaceHolder
 import androidx.annotation.RequiresApi
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.annotation.VisibleForTesting
 import androidx.wear.watchface.CanvasComplicationFactory
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.ComplicationSlot
@@ -44,6 +41,10 @@
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationDeniedActivity
 import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationRationalActivity
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -64,55 +65,12 @@
 import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import androidx.wear.watchface.style.WatchFaceLayer
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 import java.time.ZonedDateTime
 import kotlin.math.cos
 import kotlin.math.sin
-
-private const val CENTER_CIRCLE_DIAMETER_FRACTION = 0.03738f
-private const val OUTER_CIRCLE_STROKE_THICKNESS_FRACTION = 0.00467f
-private const val NUMBER_STYLE_OUTER_CIRCLE_RADIUS_FRACTION = 0.00584f
-
-private const val GAP_BETWEEN_OUTER_CIRCLE_AND_BORDER_FRACTION = 0.03738f
-private const val GAP_BETWEEN_HAND_AND_CENTER_FRACTION =
-    0.01869f + CENTER_CIRCLE_DIAMETER_FRACTION / 2.0f
-
-private const val HOUR_HAND_LENGTH_FRACTION = 0.21028f
-private const val HOUR_HAND_THICKNESS_FRACTION = 0.02336f
-private const val MINUTE_HAND_LENGTH_FRACTION = 0.3783f
-private const val MINUTE_HAND_THICKNESS_FRACTION = 0.0163f
-private const val SECOND_HAND_LENGTH_FRACTION = 0.37383f
-private const val SECOND_HAND_THICKNESS_FRACTION = 0.00934f
-
-const val NUMBER_RADIUS_FRACTION = 0.45f
-
-const val COLOR_STYLE_SETTING = "color_style_setting"
-const val RED_STYLE = "red_style"
-const val GREEN_STYLE = "green_style"
-const val BLUE_STYLE = "blue_style"
-const val DRAW_HOUR_PIPS_STYLE_SETTING = "draw_hour_pips_style_setting"
-const val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
-const val COMPLICATIONS_STYLE_SETTING = "complications_style_setting"
-const val HOURS_DRAW_FREQ_STYLE_SETTING = "hours_draw_freq_style_setting"
-const val NO_COMPLICATIONS = "NO_COMPLICATIONS"
-const val LEFT_COMPLICATION = "LEFT_COMPLICATION"
-const val RIGHT_COMPLICATION = "RIGHT_COMPLICATION"
-const val LEFT_AND_RIGHT_COMPLICATIONS = "LEFT_AND_RIGHT_COMPLICATIONS"
-
-/** How long each frame is displayed at expected frame rate.  */
-private const val FRAME_PERIOD_MS: Long = 16L
-
-const val EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID = 101
-const val EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID = 102
-
-const val HOURS_DRAW_FREQ_MIN = 1L
-const val HOURS_DRAW_FREQ_MAX = 4L
-const val HOURS_DRAW_FREQ_DEFAULT = 3L
-
-const val CONFIGURABLE_DATA_SOURCE_PKG = "androidx.wear.watchface.complications.datasource.samples"
-const val CONFIGURABLE_DATA_SOURCE = "$CONFIGURABLE_DATA_SOURCE_PKG.ConfigurableDataSourceService"
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
 /** A simple example canvas based analog watch face. NB this is open for testing. */
 open class ExampleCanvasAnalogWatchFaceService : WatchFaceService() {
@@ -270,13 +228,15 @@
         )
     )
 
-    internal val exampleFlavor by lazy {
+    private val exampleFlavor by lazy {
         UserStyleFlavor(
             "exampleFlavor",
-            UserStyle(mapOf(
-                colorStyleSetting to colorStyleSetting.getOptionForId(Option.Id(BLUE_STYLE)),
-                watchHandLengthStyleSetting to DoubleRangeOption(1.0)
-            )),
+            UserStyle(
+                mapOf(
+                    colorStyleSetting to colorStyleSetting.getOptionForId(Option.Id(BLUE_STYLE)),
+                    watchHandLengthStyleSetting to DoubleRangeOption(1.0)
+                )
+            ),
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
                     DefaultComplicationDataSourcePolicy(
@@ -290,7 +250,8 @@
                         SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
                         ComplicationType.SHORT_TEXT
                     )
-            ))
+            )
+        )
     }
 
     public override fun createUserStyleFlavors(
@@ -314,6 +275,8 @@
             canvasComplicationFactory,
             listOf(
                 ComplicationType.RANGED_VALUE,
+                ComplicationType.GOAL_PROGRESS,
+                ComplicationType.WEIGHTED_ELEMENTS,
                 ComplicationType.LONG_TEXT,
                 ComplicationType.SHORT_TEXT,
                 ComplicationType.MONOCHROMATIC_IMAGE,
@@ -335,6 +298,8 @@
             canvasComplicationFactory,
             listOf(
                 ComplicationType.RANGED_VALUE,
+                ComplicationType.GOAL_PROGRESS,
+                ComplicationType.WEIGHTED_ELEMENTS,
                 ComplicationType.LONG_TEXT,
                 ComplicationType.SHORT_TEXT,
                 ComplicationType.MONOCHROMATIC_IMAGE,
@@ -380,361 +345,420 @@
         .setComplicationRationaleDialogIntent(
             Intent(this, ComplicationRationalActivity::class.java)
         )
-}
 
-@OptIn(WatchFaceExperimental::class)
-@Suppress("Deprecation")
-@RequiresApi(27)
-class ExampleAnalogWatchCanvasRenderer(
-    surfaceHolder: SurfaceHolder,
-    private val context: Context,
-    private var watchFaceColorStyle: WatchFaceColorStyle,
-    currentUserStyleRepository: CurrentUserStyleRepository,
-    watchState: WatchState,
-    private val colorStyleSetting: ListUserStyleSetting,
-    private val drawPipsStyleSetting: BooleanUserStyleSetting,
-    private val watchHandLengthStyleSettingDouble: DoubleRangeUserStyleSetting,
-    private val hoursDrawFreqStyleSetting: LongRangeUserStyleSetting,
-    private val complicationSlotsManager: ComplicationSlotsManager
-) : Renderer.CanvasRenderer(
-    surfaceHolder,
-    currentUserStyleRepository,
-    watchState,
-    CanvasType.HARDWARE,
-    FRAME_PERIOD_MS,
-    clearWithBackgroundTintBeforeRenderingHighlightLayer = true
-) {
-    private val clockHandPaint = Paint().apply {
-        isAntiAlias = true
-        strokeWidth = context.resources.getDimensionPixelSize(
-            R.dimen.clock_hand_stroke_width
-        ).toFloat()
-    }
+    @OptIn(WatchFaceExperimental::class)
+    @Suppress("Deprecation")
+    @RequiresApi(27)
+    private class ExampleAnalogWatchCanvasRenderer(
+        surfaceHolder: SurfaceHolder,
+        private val context: Context,
+        private var watchFaceColorStyle: WatchFaceColorStyle,
+        currentUserStyleRepository: CurrentUserStyleRepository,
+        watchState: WatchState,
+        private val colorStyleSetting: ListUserStyleSetting,
+        private val drawPipsStyleSetting: BooleanUserStyleSetting,
+        private val watchHandLengthStyleSettingDouble: DoubleRangeUserStyleSetting,
+        private val hoursDrawFreqStyleSetting: LongRangeUserStyleSetting,
+        private val complicationSlotsManager: ComplicationSlotsManager
+    ) : Renderer.CanvasRenderer(
+        surfaceHolder,
+        currentUserStyleRepository,
+        watchState,
+        CanvasType.HARDWARE,
+        FRAME_PERIOD_MS,
+        clearWithBackgroundTintBeforeRenderingHighlightLayer = true
+    ) {
+        private val clockHandPaint = Paint().apply {
+            isAntiAlias = true
+            strokeWidth = context.resources.getDimensionPixelSize(
+                R.dimen.clock_hand_stroke_width
+            ).toFloat()
+        }
 
-    private val outerElementPaint = Paint().apply {
-        isAntiAlias = true
-    }
+        private val outerElementPaint = Paint().apply {
+            isAntiAlias = true
+        }
 
-    private val textPaint = Paint().apply {
-        isAntiAlias = true
-        textSize = context.resources.getDimensionPixelSize(R.dimen.hour_mark_size).toFloat()
-    }
+        private val textPaint = Paint().apply {
+            isAntiAlias = true
+            textSize = context.resources.getDimensionPixelSize(R.dimen.hour_mark_size).toFloat()
+        }
 
-    private lateinit var hourHandFill: Path
-    private lateinit var hourHandBorder: Path
-    private lateinit var minuteHandFill: Path
-    private lateinit var minuteHandBorder: Path
-    private lateinit var secondHand: Path
+        private lateinit var hourHandFill: Path
+        private lateinit var hourHandBorder: Path
+        private lateinit var minuteHandFill: Path
+        private lateinit var minuteHandBorder: Path
+        private lateinit var secondHand: Path
 
-    private var drawHourPips = true
-    private var watchHandScale = 1.0f
-    private var hoursDrawFreq = HOURS_DRAW_FREQ_DEFAULT.toInt()
+        private var drawHourPips = true
+        private var watchHandScale = 1.0f
+        private var hoursDrawFreq = HOURS_DRAW_FREQ_DEFAULT.toInt()
 
-    init {
-        CoroutineScope(Dispatchers.Main.immediate).launch {
-            currentUserStyleRepository.userStyle.collect { userStyle ->
-                watchFaceColorStyle = WatchFaceColorStyle.create(
-                    context,
-                    userStyle[colorStyleSetting]!!.toString()
-                )
+        init {
+            CoroutineScope(Dispatchers.Main.immediate).launch {
+                currentUserStyleRepository.userStyle.collect { userStyle ->
+                    watchFaceColorStyle = WatchFaceColorStyle.create(
+                        context,
+                        userStyle[colorStyleSetting]!!.toString()
+                    )
 
-                // Apply the userStyle to the complicationSlots. ComplicationDrawables for each
-                // of the styles are defined in XML so we need to replace the complication's
-                // drawables.
-                for ((_, complication) in complicationSlotsManager.complicationSlots) {
-                    (complication.renderer as CanvasComplicationDrawable).drawable =
-                        watchFaceColorStyle.getDrawable(context)!!
-                }
+                    // Apply the userStyle to the complicationSlots. ComplicationDrawables for each
+                    // of the styles are defined in XML so we need to replace the complication's
+                    // drawables.
+                    for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                        (complication.renderer as CanvasComplicationDrawable).drawable =
+                            watchFaceColorStyle.getDrawable(context)!!
+                    }
 
-                drawHourPips = (userStyle[drawPipsStyleSetting]!! as BooleanOption).value
-                watchHandScale =
-                    (userStyle[watchHandLengthStyleSettingDouble]!! as DoubleRangeOption)
-                        .value.toFloat()
-                hoursDrawFreq = (userStyle[hoursDrawFreqStyleSetting]!! as LongRangeOption)
+                    drawHourPips = (userStyle[drawPipsStyleSetting]!! as BooleanOption).value
+                    watchHandScale =
+                        (userStyle[watchHandLengthStyleSettingDouble]!! as DoubleRangeOption)
+                            .value.toFloat()
+                    hoursDrawFreq = (userStyle[hoursDrawFreqStyleSetting]!! as LongRangeOption)
                         .value.toInt()
 
-                watchfaceColors = WatchFaceColors(
-                    Color.valueOf(watchFaceColorStyle.activeStyle.primaryColor),
-                    Color.valueOf(watchFaceColorStyle.activeStyle.secondaryColor),
-                    Color.valueOf(Color.DKGRAY)
+                    watchfaceColors = WatchFaceColors(
+                        Color.valueOf(watchFaceColorStyle.activeStyle.primaryColor),
+                        Color.valueOf(watchFaceColorStyle.activeStyle.secondaryColor),
+                        Color.valueOf(Color.DKGRAY)
+                    )
+                }
+            }
+        }
+
+        override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+            val style = if (renderParameters.drawMode == DrawMode.AMBIENT) {
+                watchFaceColorStyle.ambientStyle
+            } else {
+                watchFaceColorStyle.activeStyle
+            }
+
+            canvas.drawColor(style.backgroundColor)
+
+            // We don't need to check renderParameters.watchFaceWatchFaceLayers because
+            // CanvasComplicationDrawable does that for us.
+            for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                if (complication.enabled) {
+                    complication.render(canvas, zonedDateTime, renderParameters)
+                }
+            }
+
+            if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)
+            ) {
+                drawClockHands(canvas, bounds, zonedDateTime, style)
+            }
+
+            if (renderParameters.drawMode != DrawMode.AMBIENT &&
+                renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE) && drawHourPips
+            ) {
+                drawNumberStyleOuterElement(canvas, bounds, style)
+            }
+        }
+
+        override fun renderHighlightLayer(
+            canvas: Canvas,
+            bounds: Rect,
+            zonedDateTime: ZonedDateTime
+        ) {
+            for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                if (complication.enabled) {
+                    complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
+                }
+            }
+        }
+
+        private fun drawClockHands(
+            canvas: Canvas,
+            bounds: Rect,
+            zonedDateTime: ZonedDateTime,
+            style: ColorStyle
+        ) {
+            recalculateClockHands(bounds)
+            val hours = (zonedDateTime.hour % 12).toFloat()
+            val minutes = zonedDateTime.minute.toFloat()
+            val seconds = zonedDateTime.second.toFloat() +
+                (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
+
+            val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
+            val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
+
+            canvas.save()
+
+            recalculateClockHands(bounds)
+
+            if (renderParameters.drawMode == DrawMode.AMBIENT) {
+                clockHandPaint.style = Paint.Style.STROKE
+                clockHandPaint.color = style.primaryColor
+                canvas.scale(
+                    watchHandScale,
+                    watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+                canvas.rotate(hourRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.drawPath(hourHandBorder, clockHandPaint)
+                canvas.rotate(-hourRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.scale(
+                    1.0f / watchHandScale,
+                    1.0f / watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+
+                clockHandPaint.color = style.secondaryColor
+                canvas.scale(
+                    watchHandScale,
+                    watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+                canvas.rotate(minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.drawPath(minuteHandBorder, clockHandPaint)
+                canvas.rotate(-minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.scale(
+                    1.0f / watchHandScale,
+                    1.0f / watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+            } else {
+                clockHandPaint.style = Paint.Style.FILL
+                clockHandPaint.color = style.primaryColor
+                canvas.scale(
+                    watchHandScale,
+                    watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+                canvas.rotate(hourRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.drawPath(hourHandFill, clockHandPaint)
+                canvas.rotate(-hourRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.scale(
+                    1.0f / watchHandScale,
+                    1.0f / watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+
+                clockHandPaint.color = style.secondaryColor
+                canvas.scale(
+                    watchHandScale,
+                    watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+                canvas.rotate(minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.drawPath(minuteHandFill, clockHandPaint)
+                canvas.rotate(-minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.scale(
+                    1.0f / watchHandScale,
+                    1.0f / watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+
+                val secondsRot = seconds / 60.0f * 360.0f
+
+                clockHandPaint.color = style.secondaryColor
+                canvas.scale(
+                    watchHandScale,
+                    watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
+                )
+                canvas.rotate(secondsRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.drawPath(secondHand, clockHandPaint)
+                canvas.rotate(-secondsRot, bounds.exactCenterX(), bounds.exactCenterY())
+                canvas.scale(
+                    1.0f / watchHandScale,
+                    1.0f / watchHandScale,
+                    bounds.exactCenterX(),
+                    bounds.exactCenterY()
                 )
             }
-        }
-    }
 
-    override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
-        val style = if (renderParameters.drawMode == DrawMode.AMBIENT) {
-            watchFaceColorStyle.ambientStyle
-        } else {
-            watchFaceColorStyle.activeStyle
+            canvas.restore()
         }
 
-        canvas.drawColor(style.backgroundColor)
+        private fun recalculateClockHands(bounds: Rect) {
+            val rx = 1.5f
+            val ry = 1.5f
 
-        // We don't need to check renderParameters.watchFaceWatchFaceLayers because
-        // CanvasComplicationDrawable does that for us.
-        for ((_, complication) in complicationSlotsManager.complicationSlots) {
-            if (complication.enabled) {
-                complication.render(canvas, zonedDateTime, renderParameters)
-            }
-        }
-
-        if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)
-        ) {
-            drawClockHands(canvas, bounds, zonedDateTime, style)
-        }
-
-        if (renderParameters.drawMode != DrawMode.AMBIENT &&
-            renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE) && drawHourPips
-        ) {
-            drawNumberStyleOuterElement(canvas, bounds, style)
-        }
-    }
-
-    override fun renderHighlightLayer(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
-        for ((_, complication) in complicationSlotsManager.complicationSlots) {
-            if (complication.enabled) {
-                complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
-            }
-        }
-    }
-
-    private fun drawClockHands(
-        canvas: Canvas,
-        bounds: Rect,
-        zonedDateTime: ZonedDateTime,
-        style: ColorStyle
-    ) {
-        recalculateClockHands(bounds)
-        val hours = (zonedDateTime.hour % 12).toFloat()
-        val minutes = zonedDateTime.minute.toFloat()
-        val seconds = zonedDateTime.second.toFloat() +
-            (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
-
-        val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
-        val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
-
-        canvas.save()
-
-        recalculateClockHands(bounds)
-
-        if (renderParameters.drawMode == DrawMode.AMBIENT) {
-            clockHandPaint.style = Paint.Style.STROKE
-            clockHandPaint.color = style.primaryColor
-            canvas.scale(
-                watchHandScale,
-                watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-            canvas.rotate(hourRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.drawPath(hourHandBorder, clockHandPaint)
-            canvas.rotate(-hourRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.scale(
-                1.0f / watchHandScale,
-                1.0f / watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-
-            clockHandPaint.color = style.secondaryColor
-            canvas.scale(
-                watchHandScale,
-                watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-            canvas.rotate(minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.drawPath(minuteHandBorder, clockHandPaint)
-            canvas.rotate(-minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.scale(
-                1.0f / watchHandScale,
-                1.0f / watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-        } else {
-            clockHandPaint.style = Paint.Style.FILL
-            clockHandPaint.color = style.primaryColor
-            canvas.scale(
-                watchHandScale,
-                watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-            canvas.rotate(hourRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.drawPath(hourHandFill, clockHandPaint)
-            canvas.rotate(-hourRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.scale(
-                1.0f / watchHandScale,
-                1.0f / watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-
-            clockHandPaint.color = style.secondaryColor
-            canvas.scale(
-                watchHandScale,
-                watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-            canvas.rotate(minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.drawPath(minuteHandFill, clockHandPaint)
-            canvas.rotate(-minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.scale(
-                1.0f / watchHandScale,
-                1.0f / watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-
-            val secondsRot = seconds / 60.0f * 360.0f
-
-            clockHandPaint.color = style.secondaryColor
-            canvas.scale(
-                watchHandScale,
-                watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-            canvas.rotate(secondsRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.drawPath(secondHand, clockHandPaint)
-            canvas.rotate(-secondsRot, bounds.exactCenterX(), bounds.exactCenterY())
-            canvas.scale(
-                1.0f / watchHandScale,
-                1.0f / watchHandScale,
-                bounds.exactCenterX(),
-                bounds.exactCenterY()
-            )
-        }
-
-        canvas.restore()
-    }
-
-    private fun recalculateClockHands(bounds: Rect) {
-        val rx = 1.5f
-        val ry = 1.5f
-
-        hourHandBorder =
-            createClockHand(bounds, HOUR_HAND_LENGTH_FRACTION, HOUR_HAND_THICKNESS_FRACTION, rx, ry)
-
-        minuteHandBorder =
-            createClockHand(
-                bounds, MINUTE_HAND_LENGTH_FRACTION, MINUTE_HAND_THICKNESS_FRACTION, rx, ry
-            )
-
-        hourHandFill =
-            createClockHand(
-                bounds,
-                HOUR_HAND_LENGTH_FRACTION,
-                HOUR_HAND_THICKNESS_FRACTION,
-                rx,
-                ry
-            )
-
-        minuteHandFill =
-            createClockHand(
-                bounds,
-                MINUTE_HAND_LENGTH_FRACTION,
-                MINUTE_HAND_THICKNESS_FRACTION,
-                rx,
-                ry
-            )
-
-        secondHand =
-            createClockHand(
-                bounds,
-                SECOND_HAND_LENGTH_FRACTION,
-                SECOND_HAND_THICKNESS_FRACTION,
-                0.0f,
-                0.0f
-            )
-    }
-
-    /**
-     * Returns a round rect clock hand if {@code rx} and {@code ry} equals to 0, otherwise return a
-     * rect clock hand.
-     *
-     * @param bounds The bounds use to determine the coordinate of the clock hand.
-     * @param length Clock hand's length, in fraction of {@code bounds.width()}.
-     * @param thickness Clock hand's thickness, in fraction of {@code bounds.width()}.
-     * @param rx The x-radius of the rounded corners on the round-rectangle.
-     * @param ry The y-radius of the rounded corners on the round-rectangle.
-     */
-    private fun createClockHand(
-        bounds: Rect,
-        length: Float,
-        thickness: Float,
-        rx: Float,
-        ry: Float
-    ): Path {
-        val width = bounds.width()
-        val cx = bounds.exactCenterX()
-        val cy = bounds.exactCenterY()
-        val left = cx - thickness / 2 * width
-        val top = cy - (GAP_BETWEEN_HAND_AND_CENTER_FRACTION + length) * width
-        val right = cx + thickness / 2 * width
-        val bottom = cy - GAP_BETWEEN_HAND_AND_CENTER_FRACTION * width
-        val path = Path()
-        if (rx != 0.0f || ry != 0.0f) {
-            path.addRoundRect(left, top, right, bottom, rx, ry, Path.Direction.CW)
-        } else {
-            path.addRect(left, top, right, bottom, Path.Direction.CW)
-        }
-        return path
-    }
-
-    private fun drawNumberStyleOuterElement(canvas: Canvas, bounds: Rect, style: ColorStyle) {
-        val textBounds = Rect()
-        textPaint.color = style.outerElementColor
-        for (i in 12 downTo 1 step hoursDrawFreq) {
-            val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
-            val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
-            val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
-            val mark = i.toString()
-            textPaint.getTextBounds(mark, 0, mark.length, textBounds)
-            canvas.drawText(
-                mark,
-                bounds.exactCenterX() + dx - textBounds.width() / 2.0f,
-                bounds.exactCenterY() + dy + textBounds.height() / 2.0f,
-                textPaint
-            )
-        }
-
-        // Draws the circle for the remain hour indicators.
-        outerElementPaint.strokeWidth = OUTER_CIRCLE_STROKE_THICKNESS_FRACTION * bounds.width()
-        outerElementPaint.color = style.outerElementColor
-        canvas.save()
-        for (i in 0 until 12) {
-            if (i % hoursDrawFreq != 0) {
-                drawTopMiddleCircle(
-                    canvas,
+            hourHandBorder =
+                createClockHand(
                     bounds,
-                    NUMBER_STYLE_OUTER_CIRCLE_RADIUS_FRACTION
+                    HOUR_HAND_LENGTH_FRACTION,
+                    HOUR_HAND_THICKNESS_FRACTION,
+                    rx,
+                    ry
+                )
+
+            minuteHandBorder =
+                createClockHand(
+                    bounds, MINUTE_HAND_LENGTH_FRACTION, MINUTE_HAND_THICKNESS_FRACTION, rx, ry
+                )
+
+            hourHandFill =
+                createClockHand(
+                    bounds,
+                    HOUR_HAND_LENGTH_FRACTION,
+                    HOUR_HAND_THICKNESS_FRACTION,
+                    rx,
+                    ry
+                )
+
+            minuteHandFill =
+                createClockHand(
+                    bounds,
+                    MINUTE_HAND_LENGTH_FRACTION,
+                    MINUTE_HAND_THICKNESS_FRACTION,
+                    rx,
+                    ry
+                )
+
+            secondHand =
+                createClockHand(
+                    bounds,
+                    SECOND_HAND_LENGTH_FRACTION,
+                    SECOND_HAND_THICKNESS_FRACTION,
+                    0.0f,
+                    0.0f
+                )
+        }
+
+        /**
+         * Returns a round rect clock hand if {@code rx} and {@code ry} equals to 0, otherwise return a
+         * rect clock hand.
+         *
+         * @param bounds The bounds use to determine the coordinate of the clock hand.
+         * @param length Clock hand's length, in fraction of {@code bounds.width()}.
+         * @param thickness Clock hand's thickness, in fraction of {@code bounds.width()}.
+         * @param rx The x-radius of the rounded corners on the round-rectangle.
+         * @param ry The y-radius of the rounded corners on the round-rectangle.
+         */
+        private fun createClockHand(
+            bounds: Rect,
+            length: Float,
+            thickness: Float,
+            rx: Float,
+            ry: Float
+        ): Path {
+            val width = bounds.width()
+            val cx = bounds.exactCenterX()
+            val cy = bounds.exactCenterY()
+            val left = cx - thickness / 2 * width
+            val top = cy - (GAP_BETWEEN_HAND_AND_CENTER_FRACTION + length) * width
+            val right = cx + thickness / 2 * width
+            val bottom = cy - GAP_BETWEEN_HAND_AND_CENTER_FRACTION * width
+            val path = Path()
+            if (rx != 0.0f || ry != 0.0f) {
+                path.addRoundRect(left, top, right, bottom, rx, ry, Path.Direction.CW)
+            } else {
+                path.addRect(left, top, right, bottom, Path.Direction.CW)
+            }
+            return path
+        }
+
+        private fun drawNumberStyleOuterElement(canvas: Canvas, bounds: Rect, style: ColorStyle) {
+            val textBounds = Rect()
+            textPaint.color = style.outerElementColor
+            for (i in 12 downTo 1 step hoursDrawFreq) {
+                val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
+                val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
+                val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
+                val mark = i.toString()
+                textPaint.getTextBounds(mark, 0, mark.length, textBounds)
+                canvas.drawText(
+                    mark,
+                    bounds.exactCenterX() + dx - textBounds.width() / 2.0f,
+                    bounds.exactCenterY() + dy + textBounds.height() / 2.0f,
+                    textPaint
                 )
             }
-            canvas.rotate(360.0f / 12.0f, bounds.exactCenterX(), bounds.exactCenterY())
+
+            // Draws the circle for the remain hour indicators.
+            outerElementPaint.strokeWidth = OUTER_CIRCLE_STROKE_THICKNESS_FRACTION * bounds.width()
+            outerElementPaint.color = style.outerElementColor
+            canvas.save()
+            for (i in 0 until 12) {
+                if (i % hoursDrawFreq != 0) {
+                    drawTopMiddleCircle(
+                        canvas,
+                        bounds,
+                        NUMBER_STYLE_OUTER_CIRCLE_RADIUS_FRACTION
+                    )
+                }
+                canvas.rotate(360.0f / 12.0f, bounds.exactCenterX(), bounds.exactCenterY())
+            }
+            canvas.restore()
         }
-        canvas.restore()
+
+        /** Draws the outer circle on the top middle of the given bounds. */
+        private fun drawTopMiddleCircle(
+            canvas: Canvas,
+            bounds: Rect,
+            radiusFraction: Float
+        ) {
+            outerElementPaint.style = Paint.Style.FILL_AND_STROKE
+
+            val cx = 0.5f * bounds.width().toFloat()
+            val cy =
+                bounds.width() * (GAP_BETWEEN_OUTER_CIRCLE_AND_BORDER_FRACTION + radiusFraction)
+
+            canvas.drawCircle(
+                cx,
+                cy,
+                radiusFraction * bounds.width(),
+                outerElementPaint
+            )
+        }
     }
 
-    /** Draws the outer circle on the top middle of the given bounds. */
-    private fun drawTopMiddleCircle(
-        canvas: Canvas,
-        bounds: Rect,
-        radiusFraction: Float
-    ) {
-        outerElementPaint.style = Paint.Style.FILL_AND_STROKE
+    @VisibleForTesting
+    companion object {
+        private const val CENTER_CIRCLE_DIAMETER_FRACTION = 0.03738f
+        private const val OUTER_CIRCLE_STROKE_THICKNESS_FRACTION = 0.00467f
+        private const val NUMBER_STYLE_OUTER_CIRCLE_RADIUS_FRACTION = 0.00584f
 
-        val cx = 0.5f * bounds.width().toFloat()
-        val cy = bounds.width() * (GAP_BETWEEN_OUTER_CIRCLE_AND_BORDER_FRACTION + radiusFraction)
+        private const val GAP_BETWEEN_OUTER_CIRCLE_AND_BORDER_FRACTION = 0.03738f
+        private const val GAP_BETWEEN_HAND_AND_CENTER_FRACTION =
+            0.01869f + CENTER_CIRCLE_DIAMETER_FRACTION / 2.0f
 
-        canvas.drawCircle(
-            cx,
-            cy,
-            radiusFraction * bounds.width(),
-            outerElementPaint
-        )
+        private const val HOUR_HAND_LENGTH_FRACTION = 0.21028f
+        private const val HOUR_HAND_THICKNESS_FRACTION = 0.02336f
+        private const val MINUTE_HAND_LENGTH_FRACTION = 0.3783f
+        private const val MINUTE_HAND_THICKNESS_FRACTION = 0.0163f
+        private const val SECOND_HAND_LENGTH_FRACTION = 0.37383f
+        private const val SECOND_HAND_THICKNESS_FRACTION = 0.00934f
+
+        private const val NUMBER_RADIUS_FRACTION = 0.45f
+
+        const val COLOR_STYLE_SETTING = "color_style_setting"
+        private const val RED_STYLE = "red_style"
+        const val GREEN_STYLE = "green_style"
+        const val BLUE_STYLE = "blue_style"
+        const val DRAW_HOUR_PIPS_STYLE_SETTING = "draw_hour_pips_style_setting"
+        const val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
+        const val COMPLICATIONS_STYLE_SETTING = "complications_style_setting"
+        private const val HOURS_DRAW_FREQ_STYLE_SETTING = "hours_draw_freq_style_setting"
+        const val NO_COMPLICATIONS = "NO_COMPLICATIONS"
+        const val LEFT_COMPLICATION = "LEFT_COMPLICATION"
+        private const val RIGHT_COMPLICATION = "RIGHT_COMPLICATION"
+        private const val LEFT_AND_RIGHT_COMPLICATIONS = "LEFT_AND_RIGHT_COMPLICATIONS"
+
+        /** How long each frame is displayed at expected frame rate.  */
+        private const val FRAME_PERIOD_MS: Long = 16L
+
+        const val EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID = 101
+        const val EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID = 102
+
+        private const val HOURS_DRAW_FREQ_MIN = 1L
+        private const val HOURS_DRAW_FREQ_MAX = 4L
+        private const val HOURS_DRAW_FREQ_DEFAULT = 3L
+
+        const val CONFIGURABLE_DATA_SOURCE_PKG =
+            "androidx.wear.watchface.complications.datasource.samples"
+        const val CONFIGURABLE_DATA_SOURCE =
+            "$CONFIGURABLE_DATA_SOURCE_PKG.ConfigurableDataSourceService"
     }
 }
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index da22db5..3fd4f52 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -38,10 +38,6 @@
 import android.view.animation.PathInterpolator
 import androidx.annotation.ColorInt
 import androidx.annotation.RequiresApi
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.CanvasComplicationFactory
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.ComplicationSlot
@@ -54,6 +50,10 @@
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationDeniedActivity
 import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationRationalActivity
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -62,407 +62,13 @@
 import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import androidx.wear.watchface.style.WatchFaceLayer
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.android.asCoroutineDispatcher
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
 import java.time.ZonedDateTime
 import kotlin.math.max
 import kotlin.math.min
-
-// Render at approximately 60fps in interactive mode.
-private const val INTERACTIVE_UPDATE_RATE_MS = 16L
-
-internal class Vec2f(val x: Float, val y: Float)
-
-// Constants for the size of complication.
-internal val CIRCLE_COMPLICATION_DIAMETER_FRACTION = Vec2f(0.252f, 0.252f)
-internal val ROUND_RECT_COMPLICATION_SIZE_FRACTION = Vec2f(0.645f, 0.168f)
-
-// Constants for the upper complication location.
-internal val UPPER_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.21f)
-internal val UPPER_ROUND_RECT_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.21f)
-
-// Constants for the lower complication location.
-internal val LOWER_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.79f)
-internal val LOWER_ROUND_RECT_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.79f)
-
-// Constants for the left complication location.
-internal val LEFT_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.177f, 0.5f)
-
-// Constants for the right complication location.
-internal val RIGHT_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.823f, 0.5f)
-
-// Constants for the clock digits' position, based on the height and width of given bounds.
-internal val MARGIN_FRACTION_WITHOUT_COMPLICATION = Vec2f(0.2f, 0.2f)
-internal val MARGIN_FRACTION_WITH_COMPLICATION = Vec2f(0.4f, 0.4f)
-
-enum class ComplicationID {
-    UPPER,
-    RIGHT,
-    LOWER,
-    LEFT,
-    BACKGROUND
-}
-
-internal val VERTICAL_COMPLICATION_IDS = arrayOf(
-    ComplicationID.UPPER.ordinal,
-    ComplicationID.LOWER.ordinal
-)
-internal val HORIZONTAL_COMPLICATION_IDS = arrayOf(
-    ComplicationID.LEFT.ordinal,
-    ComplicationID.RIGHT.ordinal
-)
-internal val FOREGROUND_COMPLICATION_IDS = arrayOf(
-    ComplicationID.UPPER.ordinal, ComplicationID.RIGHT.ordinal,
-    ComplicationID.LOWER.ordinal, ComplicationID.LEFT.ordinal
-)
-
-// The name of the font used for drawing the text in the digit watch face.
-private const val DIGITAL_TYPE_FACE = "sans-serif-condensed-light"
-
-// The width of the large digit bitmaps, as a fraction of their height.
-private const val DIGIT_WIDTH_FRACTION = 0.65f
-
-// The height of the small digits (used for minutes and seconds), given as a fraction of the  height
-// of the large digits.
-private const val SMALL_DIGIT_SIZE_FRACTION = 0.45f
-
-// The width of the small digit bitmaps, as a fraction of their height.
-private const val SMALL_DIGIT_WIDTH_FRACTION = 0.7f
-
-// The padding at the top and bottom of the digit bitmaps, given as a fraction of the height.
-// Needed as some characters may ascend or descend slightly (e.g. "8").
-private const val DIGIT_PADDING_FRACTION = 0.05f
-
-// The gap between the hours and the minutes/seconds, given as a fraction of the width of the large
-// digits.
-private const val GAP_WIDTH_FRACTION = 0.1f
-
-// A string containing all digits, used to measure their height.
-private const val ALL_DIGITS = "0123456789"
-
-internal val DIGITS = ALL_DIGITS.toCharArray().map { it.toString() }
-
-// Changing digits are animated. This enum is used to label the start and end animation parameters.
-internal enum class DigitMode {
-    OUTGOING,
-    INCOMING
-}
-
-// The start and end times of the animations, expressed as a fraction of a second.
-// (So 0.5 means that the animation of that digit will begin half-way through the second).
-// Note that because we only cache one digit of each type, the current and next times must
-// not overlap.
-internal val DIGIT_ANIMATION_START_TIME_FRACTION = mapOf(
-    DigitMode.OUTGOING to 0.5f,
-    DigitMode.INCOMING to 0.667f
-)
-internal val DIGIT_ANIMATION_END_TIME = mapOf(
-    DigitMode.OUTGOING to 0.667f,
-    DigitMode.INCOMING to 1f
-)
-internal const val POSITION_ANIMATION_START_TIME = 0.0833f
-private const val POSITION_ANIMATION_END_TIME = 0.5833f
-
-// Parameters governing the animation of the current and next digits. NB Scale is a size multiplier.
-// The first index is the values for the outgoing digit, and the second index for the incoming
-// digit. If seconds are changing from 1 -> 2 for example, the 1 will scale from 1f to 0.65f, and
-// rotate from 0f to 82f. The 2 will scale from 0.65f to 1f, and rotate from -97f to 0f.
-private val DIGIT_SCALE_START = mapOf(DigitMode.OUTGOING to 1f, DigitMode.INCOMING to 0.65f)
-private val DIGIT_SCALE_END = mapOf(DigitMode.OUTGOING to 0.65f, DigitMode.INCOMING to 1f)
-private val DIGIT_ROTATE_START_DEGREES = mapOf(
-    DigitMode.OUTGOING to 0f,
-    DigitMode.INCOMING to -97f
-)
-private val DIGIT_ROTATE_END_DEGREES = mapOf(DigitMode.OUTGOING to 82f, DigitMode.INCOMING to 0f)
-private val DIGIT_OPACITY_START = mapOf(DigitMode.OUTGOING to 1f, DigitMode.INCOMING to 0.07f)
-private val DIGIT_OPACITY_END = mapOf(DigitMode.OUTGOING to 0f, DigitMode.INCOMING to 1f)
-
-// The offset used to stagger the animation when multiple digits are animating at the same time.
-private const val TIME_OFFSET_SECONDS_PER_DIGIT_TYPE = -5 / 60f
-
-// The duration of the ambient mode change animation.
-private const val AMBIENT_TRANSITION_MS = 333L
-
-private val DIGIT_SCALE_INTERPOLATOR = mapOf(
-    DigitMode.OUTGOING to PathInterpolator(0.4f, 0f, 0.67f, 1f),
-    DigitMode.INCOMING to PathInterpolator(0.33f, 0f, 0.2f, 1f)
-)
-private val DIGIT_ROTATION_INTERPOLATOR = mapOf(
-    DigitMode.OUTGOING to PathInterpolator(0.57f, 0f, 0.73f, 0.49f),
-    DigitMode.INCOMING to PathInterpolator(0.15f, 0.49f, 0.37f, 1f)
-)
-private val DIGIT_OPACITY_INTERPOLATOR = mapOf(
-    DigitMode.OUTGOING to PathInterpolator(0.4f, 0f, 1f, 1f),
-    DigitMode.INCOMING to PathInterpolator(0f, 0f, 0.2f, 1f)
-)
-internal val CENTERING_ADJUSTMENT_INTERPOLATOR =
-    PathInterpolator(0.4f, 0f, 0.2f, 1f)
-
-@ColorInt
-internal fun colorRgb(red: Float, green: Float, blue: Float) =
-    0xff000000.toInt() or
-        ((red * 255.0f + 0.5f).toInt() shl 16) or
-        ((green * 255.0f + 0.5f).toInt() shl 8) or
-        (blue * 255.0f + 0.5f).toInt()
-
-internal fun redFraction(@ColorInt color: Int) = Color.red(color).toFloat() / 255.0f
-
-internal fun greenFraction(@ColorInt color: Int) = Color.green(color).toFloat() / 255.0f
-
-internal fun blueFraction(@ColorInt color: Int) = Color.blue(color).toFloat() / 255.0f
-
-/**
- * Returns an RGB color that has the same effect as drawing `color` with `alphaFraction` over a
- * `backgroundColor` background.
- *
- * @param color the foreground color
- * @param alphaFraction the fraction of the alpha value, range from 0 to 1
- * @param backgroundColor the background color
- */
-internal fun getRGBColor(
-    @ColorInt color: Int,
-    alphaFraction: Float,
-    @ColorInt backgroundColor: Int
-): Int {
-    return colorRgb(
-        lerp(redFraction(backgroundColor), redFraction(color), alphaFraction),
-        lerp(greenFraction(backgroundColor), greenFraction(color), alphaFraction),
-        lerp(blueFraction(backgroundColor), blueFraction(color), alphaFraction)
-    )
-}
-
-internal enum class DigitType {
-    HOUR_TENS,
-    HOUR_UNITS,
-    MINUTE_TENS,
-    MINUTE_UNITS,
-    SECOND_TENS,
-    SECOND_UNITS
-}
-
-/** A class to provide string representations of each of the digits of a given time. */
-internal class DigitStrings {
-    private var hourTens = ""
-    private var hourUnits = ""
-    private var minuteTens = ""
-    private var minuteUnits = ""
-    private var secondTens = ""
-    private var secondUnits = ""
-
-    /** Sets the time represented by this instance. */
-    fun set(zonedDateTime: ZonedDateTime, is24Hour: Boolean) {
-        if (is24Hour) {
-            val hourValue = zonedDateTime.hour
-            hourTens = getTensDigitString(hourValue, true)
-            hourUnits = getUnitsDigitString(hourValue)
-        } else {
-            var hourValue = zonedDateTime.hour % 12
-            // We should show 12 for noon and midnight.
-            if (hourValue == 0) {
-                hourValue = 12
-            }
-            hourTens = getTensDigitString(hourValue, false)
-            hourUnits = getUnitsDigitString(hourValue)
-        }
-
-        val minuteValue = zonedDateTime.minute
-        minuteTens = getTensDigitString(minuteValue, true)
-        minuteUnits = getUnitsDigitString(minuteValue)
-        val secondsValue = zonedDateTime.second
-        secondTens = getTensDigitString(secondsValue, true)
-        secondUnits = getUnitsDigitString(secondsValue)
-    }
-
-    /** Returns a string representing the specified digit of the time represented by this object. */
-    fun get(digitType: DigitType): String {
-        return when (digitType) {
-            DigitType.HOUR_TENS -> hourTens
-            DigitType.HOUR_UNITS -> hourUnits
-            DigitType.MINUTE_TENS -> minuteTens
-            DigitType.MINUTE_UNITS -> minuteUnits
-            DigitType.SECOND_TENS -> secondTens
-            DigitType.SECOND_UNITS -> secondUnits
-        }
-    }
-
-    /**
-     * Returns the number of hour digits in this object. If the representation is 24-hour, this will
-     * always return 2. If 12-hour, this will return 1 or 2.
-     */
-    fun getNumberOfHoursDigits(): Int {
-        return if (hourTens == "") 1 else 2
-    }
-
-    /**
-     * Returns a {@link String} representing the tens digit of the provided non-negative {@code
-     * value}. If {@code padWithZeroes} is true, returns zero if {@code value} < 10. If {@code
-     * padWithZeroes} is false, returns an empty string if {@code value} < 10.
-     */
-    private fun getTensDigitString(value: Int, padWithZeroes: Boolean): String {
-        if (value < 10 && !padWithZeroes) {
-            return ""
-        }
-        // We don't use toString() because during draw calls we don't want to avoid allocating objects.
-        return DIGITS[(value / 10) % 10]
-    }
-
-    /**
-     * Returns a {@link String} representing the units digit of the provided non-negative {@code
-     * value}.
-     */
-    private fun getUnitsDigitString(value: Int): String {
-        // We don't use toString() because during draw calls we don't want to avoid allocating objects.
-        return DIGITS[value % 10]
-    }
-}
-
-/** Returns a linear interpolation between a and b using the scalar s.  */
-private fun lerp(a: Float, b: Float, s: Float) = a + s * (b - a)
-
-/**
- * Returns the interpolation scalar (s) that satisfies the equation: `value = lerp(a, b, s)`
- *
- * If `a == b`, then this function will return 0.
- */
-private fun lerpInv(a: Float, b: Float, value: Float) = if (a != b) (value - a) / (b - a) else 0.0f
-
-internal fun getInterpolatedValue(
-    startValue: Float,
-    endValue: Float,
-    startTime: Float,
-    endTime: Float,
-    currentTime: Float,
-    interpolator: TimeInterpolator
-): Float {
-    val progress = when {
-        currentTime < startTime -> 0f
-        currentTime > endTime -> 1f
-        else -> interpolator.getInterpolation(lerpInv(startTime, endTime, currentTime))
-    }
-    return lerp(startValue, endValue, progress)
-}
-
-internal data class DigitDrawProperties(
-    var shouldDraw: Boolean = false,
-    var scale: Float = 0f,
-    var rotation: Float = 0f,
-    var opacity: Float = 0f
-)
-
-/**
- * Sets the [DigitDrawProperties] that should be used for drawing, given the specified
- * parameters.
- *
- * @param secondProgress the sub-second part of the current time, where 0 means the current second
- * has just begun, and 1 means the current second has just ended
- * @param offsetSeconds a value added to the start and end time of the animations
- * @param digitMode whether the digit is OUTGOING or INCOMING
- * @param output the [DigitDrawProperties] that will be set
- */
-internal fun getDigitDrawProperties(
-    secondProgress: Float,
-    offsetSeconds: Float,
-    digitMode: DigitMode,
-    output: DigitDrawProperties
-) {
-    val startTime = DIGIT_ANIMATION_START_TIME_FRACTION[digitMode]!! + offsetSeconds
-    val endTime = DIGIT_ANIMATION_END_TIME[digitMode]!! + offsetSeconds
-    output.shouldDraw = if (digitMode == DigitMode.OUTGOING) {
-        secondProgress < endTime
-    } else {
-        secondProgress >= startTime
-    }
-    output.scale = getInterpolatedValue(
-        DIGIT_SCALE_START[digitMode]!!,
-        DIGIT_SCALE_END[digitMode]!!,
-        startTime,
-        endTime,
-        secondProgress,
-        DIGIT_SCALE_INTERPOLATOR[digitMode]!!
-    )
-    output.rotation = getInterpolatedValue(
-        DIGIT_ROTATE_START_DEGREES[digitMode]!!,
-        DIGIT_ROTATE_END_DEGREES[digitMode]!!,
-        startTime,
-        endTime,
-        secondProgress,
-        DIGIT_ROTATION_INTERPOLATOR[digitMode]!!
-    )
-    output.opacity = getInterpolatedValue(
-        DIGIT_OPACITY_START[digitMode]!!,
-        DIGIT_OPACITY_END[digitMode]!!,
-        startTime,
-        endTime,
-        secondProgress,
-        DIGIT_OPACITY_INTERPOLATOR[digitMode]!!
-    )
-}
-
-internal fun getTimeOffsetSeconds(digitType: DigitType): Float {
-    return when (digitType) {
-        DigitType.HOUR_TENS -> 5f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
-        DigitType.HOUR_UNITS -> 4f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
-        DigitType.MINUTE_TENS -> 3f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
-        DigitType.MINUTE_UNITS -> 2f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
-        DigitType.SECOND_TENS -> 1f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
-        DigitType.SECOND_UNITS -> 0f
-    }
-}
-
-private class DrawProperties(
-    var backgroundAlpha: Float = 1f,
-    var timeScale: Float = 1f,
-    var secondsScale: Float = 1f
-) {
-    companion object {
-        val TIME_SCALE = object : FloatProperty<DrawProperties>("timeScale") {
-            override fun setValue(obj: DrawProperties, value: Float) {
-                obj.timeScale = value
-            }
-
-            override fun get(obj: DrawProperties): Float {
-                return obj.timeScale
-            }
-        }
-
-        val SECONDS_SCALE = object : FloatProperty<DrawProperties>("secondsScale") {
-            override fun setValue(obj: DrawProperties, value: Float) {
-                obj.secondsScale = value
-            }
-
-            override fun get(obj: DrawProperties): Float {
-                return obj.secondsScale
-            }
-        }
-    }
-}
-
-/** Applies a multiplier to a color, e.g. to darken if it's < 1.0 */
-internal fun multiplyColor(colorInt: Int, multiplier: Float): Int {
-    val adjustedMultiplier = multiplier / 255.0f
-    return colorRgb(
-        Color.red(colorInt).toFloat() * adjustedMultiplier,
-        Color.green(colorInt).toFloat() * adjustedMultiplier,
-        Color.blue(colorInt).toFloat() * adjustedMultiplier,
-    )
-}
-
-internal fun createBoundsRect(
-    centerFraction: PointF,
-    size: Vec2f
-): RectF {
-    val halfWidth = size.x / 2.0f
-    val halfHeight = size.y / 2.0f
-    return RectF(
-        (centerFraction.x - halfWidth),
-        (centerFraction.y - halfHeight),
-        (centerFraction.x + halfWidth),
-        (centerFraction.y + halfHeight)
-    )
-}
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.launch
 
 /** A simple example canvas based digital watch face. */
 class ExampleCanvasDigitalWatchFaceService : WatchFaceService() {
@@ -518,6 +124,8 @@
         canvasComplicationFactory,
         listOf(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
             ComplicationType.SMALL_IMAGE
@@ -540,6 +148,8 @@
         canvasComplicationFactory,
         listOf(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
             ComplicationType.SMALL_IMAGE
@@ -560,10 +170,13 @@
     private val upperAndLowerComplicationTypes = listOf(
         ComplicationType.LONG_TEXT,
         ComplicationType.RANGED_VALUE,
+        ComplicationType.GOAL_PROGRESS,
+        ComplicationType.WEIGHTED_ELEMENTS,
         ComplicationType.SHORT_TEXT,
         ComplicationType.MONOCHROMATIC_IMAGE,
         ComplicationType.SMALL_IMAGE
     )
+
     // The upper and lower complicationSlots change shape depending on the complication's type.
     private val upperComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder(
         ComplicationID.UPPER.ordinal,
@@ -685,626 +298,1040 @@
                 Intent(this, ComplicationRationalActivity::class.java)
             )
     }
-}
 
-@OptIn(WatchFaceExperimental::class)
-@Suppress("Deprecation")
-@RequiresApi(27)
-class ExampleDigitalWatchCanvasRenderer(
-    surfaceHolder: SurfaceHolder,
-    private val context: Context,
-    private var watchFaceColorStyle: WatchFaceColorStyle,
-    currentUserStyleRepository: CurrentUserStyleRepository,
-    watchState: WatchState,
-    private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
-    private val complicationSlotsManager: ComplicationSlotsManager
-) : Renderer.CanvasRenderer(
-    surfaceHolder,
-    currentUserStyleRepository,
-    watchState,
-    CanvasType.HARDWARE,
-    INTERACTIVE_UPDATE_RATE_MS,
-    clearWithBackgroundTintBeforeRenderingHighlightLayer = true
-) {
-    internal var oldBounds = Rect(0, 0, 0, 0)
+    @OptIn(WatchFaceExperimental::class)
+    @Suppress("Deprecation")
+    @RequiresApi(27)
+    private class ExampleDigitalWatchCanvasRenderer(
+        surfaceHolder: SurfaceHolder,
+        private val context: Context,
+        private var watchFaceColorStyle: WatchFaceColorStyle,
+        currentUserStyleRepository: CurrentUserStyleRepository,
+        watchState: WatchState,
+        private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
+        private val complicationSlotsManager: ComplicationSlotsManager
+    ) : Renderer.CanvasRenderer(
+        surfaceHolder,
+        currentUserStyleRepository,
+        watchState,
+        CanvasType.HARDWARE,
+        INTERACTIVE_UPDATE_RATE_MS,
+        clearWithBackgroundTintBeforeRenderingHighlightLayer = true
+    ) {
+        internal var oldBounds = Rect(0, 0, 0, 0)
 
-    private fun getBaseDigitPaint() = Paint().apply {
-        typeface = Typeface.create(DIGITAL_TYPE_FACE, Typeface.NORMAL)
-        isAntiAlias = true
-    }
+        private fun getBaseDigitPaint() = Paint().apply {
+            typeface = Typeface.create(DIGITAL_TYPE_FACE, Typeface.NORMAL)
+            isAntiAlias = true
+        }
 
-    private val digitTextHoursPaint = getBaseDigitPaint()
-    private val digitTextMinutesPaint = getBaseDigitPaint()
-    private val digitTextSecondsPaint = getBaseDigitPaint()
+        private val digitTextHoursPaint = getBaseDigitPaint()
+        private val digitTextMinutesPaint = getBaseDigitPaint()
+        private val digitTextSecondsPaint = getBaseDigitPaint()
 
-    // Used for drawing the cached digits to the watchface.
-    private val digitBitmapPaint = Paint().apply {
-        isFilterBitmap = true
-    }
+        // Used for drawing the cached digits to the watchface.
+        private val digitBitmapPaint = Paint().apply {
+            isFilterBitmap = true
+        }
 
-    // Used for computing text sizes, not directly used for rendering.
-    private var digitTextPaint = getBaseDigitPaint()
+        // Used for computing text sizes, not directly used for rendering.
+        private var digitTextPaint = getBaseDigitPaint()
 
-    private val clockBounds = Rect()
-    private val digitBounds = Rect()
-    private var digitTextSize = 0f
-    private var smallDigitTextSize = 0f
-    private var digitHeight = 0
-    private var digitWidth = 0
-    private var smallDigitHeight = 0
-    private var smallDigitWidth = 0
-    private var digitVerticalPadding = 0
-    private var gapWidth = 0f
-    private val currentDigitStrings = DigitStrings()
-    private val nextDigitStrings = DigitStrings()
-    private val digitDrawProperties = DigitDrawProperties()
-    private var drawProperties = DrawProperties()
-    private var prevDrawMode = DrawMode.INTERACTIVE
+        private val clockBounds = Rect()
+        private val digitBounds = Rect()
+        private var digitTextSize = 0f
+        private var smallDigitTextSize = 0f
+        private var digitHeight = 0
+        private var digitWidth = 0
+        private var smallDigitHeight = 0
+        private var smallDigitWidth = 0
+        private var digitVerticalPadding = 0
+        private var gapWidth = 0f
+        private val currentDigitStrings = DigitStrings()
+        private val nextDigitStrings = DigitStrings()
+        private val digitDrawProperties = DigitDrawProperties()
+        private var drawProperties = DrawProperties()
+        private var prevDrawMode = DrawMode.INTERACTIVE
 
-    // Animation played when exiting ambient mode.
-    private val ambientExitAnimator = AnimatorSet().apply {
-        val linearOutSlow = AnimationUtils.loadInterpolator(
-            context,
-            android.R.interpolator.linear_out_slow_in
-        )
-        playTogether(
-            ObjectAnimator.ofFloat(
-                drawProperties,
-                DrawProperties.TIME_SCALE,
-                1.0f
-            ).apply {
-                duration = AMBIENT_TRANSITION_MS
-                interpolator = linearOutSlow
-                setAutoCancel(true)
-            },
-            ObjectAnimator.ofFloat(
-                drawProperties,
-                DrawProperties.SECONDS_SCALE,
-                1.0f
-            ).apply {
-                duration = AMBIENT_TRANSITION_MS
-                interpolator = linearOutSlow
-                setAutoCancel(true)
-            }
-        )
-    }
+        // Animation played when exiting ambient mode.
+        private val ambientExitAnimator = AnimatorSet().apply {
+            val linearOutSlow = AnimationUtils.loadInterpolator(
+                context,
+                android.R.interpolator.linear_out_slow_in
+            )
+            playTogether(
+                ObjectAnimator.ofFloat(
+                    drawProperties,
+                    DrawProperties.TIME_SCALE,
+                    1.0f
+                ).apply {
+                    duration = AMBIENT_TRANSITION_MS
+                    interpolator = linearOutSlow
+                    setAutoCancel(true)
+                },
+                ObjectAnimator.ofFloat(
+                    drawProperties,
+                    DrawProperties.SECONDS_SCALE,
+                    1.0f
+                ).apply {
+                    duration = AMBIENT_TRANSITION_MS
+                    interpolator = linearOutSlow
+                    setAutoCancel(true)
+                }
+            )
+        }
 
-    // Animation played when entering ambient mode.
-    private val ambientEnterAnimator = AnimatorSet().apply {
-        val fastOutLinearIn = AnimationUtils.loadInterpolator(
-            context,
-            android.R.interpolator.fast_out_linear_in
-        )
-        playTogether(
-            ObjectAnimator.ofFloat(
-                drawProperties,
-                DrawProperties.TIME_SCALE,
-                1.0f
-            ).apply {
-                duration = AMBIENT_TRANSITION_MS
-                interpolator = fastOutLinearIn
-                setAutoCancel(true)
-            },
-            ObjectAnimator.ofFloat(
-                drawProperties,
-                DrawProperties.SECONDS_SCALE,
-                0.0f
-            ).apply {
-                duration = AMBIENT_TRANSITION_MS
-                interpolator = fastOutLinearIn
-                setAutoCancel(true)
-            }
-        )
-    }
+        // Animation played when entering ambient mode.
+        private val ambientEnterAnimator = AnimatorSet().apply {
+            val fastOutLinearIn = AnimationUtils.loadInterpolator(
+                context,
+                android.R.interpolator.fast_out_linear_in
+            )
+            playTogether(
+                ObjectAnimator.ofFloat(
+                    drawProperties,
+                    DrawProperties.TIME_SCALE,
+                    1.0f
+                ).apply {
+                    duration = AMBIENT_TRANSITION_MS
+                    interpolator = fastOutLinearIn
+                    setAutoCancel(true)
+                },
+                ObjectAnimator.ofFloat(
+                    drawProperties,
+                    DrawProperties.SECONDS_SCALE,
+                    0.0f
+                ).apply {
+                    duration = AMBIENT_TRANSITION_MS
+                    interpolator = fastOutLinearIn
+                    setAutoCancel(true)
+                }
+            )
+        }
 
-    // A mapping from digit type to cached bitmap. One bitmap is cached per digit type, and the
-    // digit shown in the cached image is stored in [currentCachedDigits].
-    private val digitBitmapCache = SparseArray<Bitmap>()
+        // A mapping from digit type to cached bitmap. One bitmap is cached per digit type, and the
+        // digit shown in the cached image is stored in [currentCachedDigits].
+        private val digitBitmapCache = SparseArray<Bitmap>()
 
-    // A mapping from digit type to the digit that the cached bitmap
-    // (stored in [digitBitmapCache]) displays.
-    private val currentCachedDigits = SparseArray<String>()
+        // A mapping from digit type to the digit that the cached bitmap
+        // (stored in [digitBitmapCache]) displays.
+        private val currentCachedDigits = SparseArray<String>()
 
-    private val coroutineScope = CoroutineScope(Dispatchers.Main.immediate)
+        private val coroutineScope = CoroutineScope(Dispatchers.Main.immediate)
 
-    init {
-        // Listen for style changes.
-        coroutineScope.launch {
-            currentUserStyleRepository.userStyle.collect { userStyle ->
-                watchFaceColorStyle =
-                    WatchFaceColorStyle.create(
-                        context,
-                        userStyle[colorStyleSetting]!!.toString()
+        init {
+            // Listen for style changes.
+            coroutineScope.launch {
+                currentUserStyleRepository.userStyle.collect { userStyle ->
+                    watchFaceColorStyle =
+                        WatchFaceColorStyle.create(
+                            context,
+                            userStyle[colorStyleSetting]!!.toString()
+                        )
+
+                    watchfaceColors = WatchFaceColors(
+                        Color.valueOf(watchFaceColorStyle.activeStyle.primaryColor),
+                        Color.valueOf(watchFaceColorStyle.activeStyle.secondaryColor),
+                        Color.valueOf(Color.DKGRAY)
                     )
 
-                watchfaceColors = WatchFaceColors(
-                    Color.valueOf(watchFaceColorStyle.activeStyle.primaryColor),
-                    Color.valueOf(watchFaceColorStyle.activeStyle.secondaryColor),
-                    Color.valueOf(Color.DKGRAY)
-                )
+                    // Apply the userStyle to the complicationSlots. ComplicationDrawables for each
+                    // of the styles are defined in XML so we need to replace the complication's
+                    // drawables.
+                    for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                        (complication.renderer as CanvasComplicationDrawable).drawable =
+                            watchFaceColorStyle.getDrawable(context)!!
+                    }
 
-                // Apply the userStyle to the complicationSlots. ComplicationDrawables for each
-                // of the styles are defined in XML so we need to replace the complication's
-                // drawables.
-                for ((_, complication) in complicationSlotsManager.complicationSlots) {
-                    (complication.renderer as CanvasComplicationDrawable).drawable =
-                        watchFaceColorStyle.getDrawable(context)!!
+                    clearDigitBitmapCache()
                 }
+            }
 
+            // Listen for ambient state changes.
+            coroutineScope.launch {
+                watchState.isAmbient.collect {
+                    if (it!!) {
+                        ambientEnterAnimator.start()
+                    } else {
+                        ambientExitAnimator.start()
+                    }
+
+                    // Trigger recomputation of bounds.
+                    oldBounds.set(0, 0, 0, 0)
+                    val antiAlias = !(it && watchState.hasLowBitAmbient)
+                    digitTextHoursPaint.isAntiAlias = antiAlias
+                    digitTextMinutesPaint.isAntiAlias = antiAlias
+                    digitTextSecondsPaint.isAntiAlias = antiAlias
+                }
+            }
+        }
+
+        override fun shouldAnimate(): Boolean {
+            // Make sure we keep animating while ambientEnterAnimator is running.
+            return ambientEnterAnimator.isRunning || super.shouldAnimate()
+        }
+
+        private fun applyColorStyleAndDrawMode(drawMode: DrawMode) {
+            digitTextHoursPaint.color = when (drawMode) {
+                DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.primaryColor
+                DrawMode.LOW_BATTERY_INTERACTIVE ->
+                    multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.6f)
+
+                DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.8f)
+                DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.primaryColor
+            }
+
+            digitTextMinutesPaint.color = when (drawMode) {
+                DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.primaryColor
+                DrawMode.LOW_BATTERY_INTERACTIVE ->
+                    multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.6f)
+
+                DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.8f)
+                DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.primaryColor
+            }
+
+            digitTextSecondsPaint.color = when (drawMode) {
+                DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.secondaryColor
+                DrawMode.LOW_BATTERY_INTERACTIVE ->
+                    multiplyColor(watchFaceColorStyle.activeStyle.secondaryColor, 0.6f)
+
+                DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.secondaryColor, 0.8f)
+                DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.secondaryColor
+            }
+
+            if (prevDrawMode != drawMode) {
+                prevDrawMode = drawMode
                 clearDigitBitmapCache()
             }
         }
 
-        // Listen for ambient state changes.
-        coroutineScope.launch {
-            watchState.isAmbient.collect {
-                if (it!!) {
-                    ambientEnterAnimator.start()
-                } else {
-                    ambientExitAnimator.start()
-                }
+        override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+            recalculateBoundsIfChanged(bounds, zonedDateTime)
 
-                // Trigger recomputation of bounds.
-                oldBounds.set(0, 0, 0, 0)
-                val antiAlias = !(it && watchState.hasLowBitAmbient)
-                digitTextHoursPaint.isAntiAlias = antiAlias
-                digitTextMinutesPaint.isAntiAlias = antiAlias
-                digitTextSecondsPaint.isAntiAlias = antiAlias
+            applyColorStyleAndDrawMode(renderParameters.drawMode)
+
+            if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
+                drawBackground(canvas)
             }
-        }
-    }
 
-    override fun shouldAnimate(): Boolean {
-        // Make sure we keep animating while ambientEnterAnimator is running.
-        return ambientEnterAnimator.isRunning || super.shouldAnimate()
-    }
+            drawComplications(canvas, zonedDateTime)
 
-    private fun applyColorStyleAndDrawMode(drawMode: DrawMode) {
-        digitTextHoursPaint.color = when (drawMode) {
-            DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.primaryColor
-            DrawMode.LOW_BATTERY_INTERACTIVE ->
-                multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.6f)
-            DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.8f)
-            DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.primaryColor
-        }
+            if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
+                val is24Hour: Boolean = DateFormat.is24HourFormat(context)
 
-        digitTextMinutesPaint.color = when (drawMode) {
-            DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.primaryColor
-            DrawMode.LOW_BATTERY_INTERACTIVE ->
-                multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.6f)
-            DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.8f)
-            DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.primaryColor
-        }
+                currentDigitStrings.set(zonedDateTime, is24Hour)
+                nextDigitStrings.set(zonedDateTime.plusSeconds(1), is24Hour)
 
-        digitTextSecondsPaint.color = when (drawMode) {
-            DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.secondaryColor
-            DrawMode.LOW_BATTERY_INTERACTIVE ->
-                multiplyColor(watchFaceColorStyle.activeStyle.secondaryColor, 0.6f)
-            DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.secondaryColor, 0.8f)
-            DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.secondaryColor
-        }
+                val secondProgress = (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
 
-        if (prevDrawMode != drawMode) {
-            prevDrawMode = drawMode
-            clearDigitBitmapCache()
-        }
-    }
-
-    override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
-        recalculateBoundsIfChanged(bounds, zonedDateTime)
-
-        applyColorStyleAndDrawMode(renderParameters.drawMode)
-
-        if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
-            drawBackground(canvas)
-        }
-
-        drawComplications(canvas, zonedDateTime)
-
-        if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
-            val is24Hour: Boolean = DateFormat.is24HourFormat(context)
-
-            currentDigitStrings.set(zonedDateTime, is24Hour)
-            nextDigitStrings.set(zonedDateTime.plusSeconds(1), is24Hour)
-
-            val secondProgress = (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
-
-            val animationStartFraction = DIGIT_ANIMATION_START_TIME_FRACTION[DigitMode.OUTGOING]!!
-            this.interactiveDrawModeUpdateDelayMillis =
-                if (secondProgress < animationStartFraction &&
-                    !ambientEnterAnimator.isRunning
-                ) {
-                    // The seconds only animate part of the time so we can sleep until the seconds next need to
-                    // animate, which improves battery life.
-                    max(
-                        INTERACTIVE_UPDATE_RATE_MS,
-                        ((animationStartFraction - secondProgress) * 1000f).toLong()
-                    )
-                } else {
-                    INTERACTIVE_UPDATE_RATE_MS
-                }
-
-            // Move the left position to the left if there are fewer than two hour digits, to
-            // ensure it is centered. If the clock is in transition from one to two hour digits or
-            // vice versa, interpolate to animate the clock's position.
-            // Move the left position to the left if there are fewer than two hour digits, to ensure
-            // it is centered. If the clock is in transition from one to two hour digits or
-            // vice versa, interpolate to animate the clock's position.
-            val centeringAdjustment = (
-                getInterpolatedValue(
-                    (2 - currentDigitStrings.getNumberOfHoursDigits()).toFloat(),
-                    (2 - nextDigitStrings.getNumberOfHoursDigits()).toFloat(),
-                    POSITION_ANIMATION_START_TIME,
-                    POSITION_ANIMATION_END_TIME,
-                    secondProgress,
-                    CENTERING_ADJUSTMENT_INTERPOLATOR
-                ) *
-                    digitWidth
-                )
-
-            // This total width assumes two hours digits.
-            val totalWidth = 2f * digitWidth + gapWidth + 2f * smallDigitWidth
-            val left = clockBounds.exactCenterX() - 0.5f * (totalWidth + centeringAdjustment)
-            val top = clockBounds.exactCenterY() - 0.5f * digitHeight
-
-            val wholeTimeSaveCount = canvas.save()
-            try {
-                canvas.scale(
-                    drawProperties.timeScale, drawProperties.timeScale,
-                    clockBounds.exactCenterX(), clockBounds.exactCenterY()
-                )
-
-                // Draw hours.
-                drawDigit(
-                    canvas,
-                    left,
-                    top,
-                    currentDigitStrings,
-                    nextDigitStrings,
-                    DigitType.HOUR_TENS,
-                    secondProgress
-                )
-                drawDigit(
-                    canvas,
-                    left + digitWidth,
-                    top,
-                    currentDigitStrings,
-                    nextDigitStrings,
-                    DigitType.HOUR_UNITS,
-                    secondProgress
-                )
-
-                // Draw minutes.
-                val minutesLeft = left + digitWidth * 2.0f + gapWidth
-                drawDigit(
-                    canvas,
-                    minutesLeft,
-                    top,
-                    currentDigitStrings,
-                    nextDigitStrings,
-                    DigitType.MINUTE_TENS,
-                    secondProgress
-                )
-                drawDigit(
-                    canvas,
-                    minutesLeft + smallDigitWidth,
-                    top,
-                    currentDigitStrings,
-                    nextDigitStrings,
-                    DigitType.MINUTE_UNITS,
-                    secondProgress
-                )
-
-                // Scale the seconds if they're not fully showing, in and out of ambient for
-                // example.
-                val scaleSeconds = drawProperties.secondsScale < 1.0f
-                if (drawProperties.secondsScale > 0f &&
-                    (renderParameters.drawMode != DrawMode.AMBIENT || scaleSeconds)
-                ) {
-                    val restoreCount = canvas.save()
-                    if (scaleSeconds) {
-                        // Scale the canvas around the center of the seconds bounds.
-                        canvas.scale(
-                            drawProperties.secondsScale,
-                            drawProperties.secondsScale,
-                            minutesLeft + smallDigitWidth,
-                            top + digitHeight - smallDigitHeight / 2
+                val animationStartFraction =
+                    DIGIT_ANIMATION_START_TIME_FRACTION[DigitMode.OUTGOING]!!
+                this.interactiveDrawModeUpdateDelayMillis =
+                    if (secondProgress < animationStartFraction &&
+                        !ambientEnterAnimator.isRunning
+                    ) {
+                        // The seconds only animate part of the time so we can sleep until the seconds next need to
+                        // animate, which improves battery life.
+                        max(
+                            INTERACTIVE_UPDATE_RATE_MS,
+                            ((animationStartFraction - secondProgress) * 1000f).toLong()
                         )
+                    } else {
+                        INTERACTIVE_UPDATE_RATE_MS
                     }
 
-                    // Draw seconds.
-                    val secondsTop = top + digitHeight - smallDigitHeight
+                // Move the left position to the left if there are fewer than two hour digits, to
+                // ensure it is centered. If the clock is in transition from one to two hour digits or
+                // vice versa, interpolate to animate the clock's position.
+                // Move the left position to the left if there are fewer than two hour digits, to ensure
+                // it is centered. If the clock is in transition from one to two hour digits or
+                // vice versa, interpolate to animate the clock's position.
+                val centeringAdjustment = (
+                    getInterpolatedValue(
+                        (2 - currentDigitStrings.getNumberOfHoursDigits()).toFloat(),
+                        (2 - nextDigitStrings.getNumberOfHoursDigits()).toFloat(),
+                        POSITION_ANIMATION_START_TIME,
+                        POSITION_ANIMATION_END_TIME,
+                        secondProgress,
+                        CENTERING_ADJUSTMENT_INTERPOLATOR
+                    ) *
+                        digitWidth
+                    )
+
+                // This total width assumes two hours digits.
+                val totalWidth = 2f * digitWidth + gapWidth + 2f * smallDigitWidth
+                val left = clockBounds.exactCenterX() - 0.5f * (totalWidth + centeringAdjustment)
+                val top = clockBounds.exactCenterY() - 0.5f * digitHeight
+
+                val wholeTimeSaveCount = canvas.save()
+                try {
+                    canvas.scale(
+                        drawProperties.timeScale, drawProperties.timeScale,
+                        clockBounds.exactCenterX(), clockBounds.exactCenterY()
+                    )
+
+                    // Draw hours.
+                    drawDigit(
+                        canvas,
+                        left,
+                        top,
+                        currentDigitStrings,
+                        nextDigitStrings,
+                        DigitType.HOUR_TENS,
+                        secondProgress
+                    )
+                    drawDigit(
+                        canvas,
+                        left + digitWidth,
+                        top,
+                        currentDigitStrings,
+                        nextDigitStrings,
+                        DigitType.HOUR_UNITS,
+                        secondProgress
+                    )
+
+                    // Draw minutes.
+                    val minutesLeft = left + digitWidth * 2.0f + gapWidth
                     drawDigit(
                         canvas,
                         minutesLeft,
-                        secondsTop,
+                        top,
                         currentDigitStrings,
                         nextDigitStrings,
-                        DigitType.SECOND_TENS,
+                        DigitType.MINUTE_TENS,
                         secondProgress
                     )
                     drawDigit(
                         canvas,
                         minutesLeft + smallDigitWidth,
-                        secondsTop,
+                        top,
                         currentDigitStrings,
                         nextDigitStrings,
-                        DigitType.SECOND_UNITS,
+                        DigitType.MINUTE_UNITS,
                         secondProgress
                     )
 
-                    canvas.restoreToCount(restoreCount)
-                }
-            } finally {
-                canvas.restoreToCount(wholeTimeSaveCount)
-            }
-        }
-    }
+                    // Scale the seconds if they're not fully showing, in and out of ambient for
+                    // example.
+                    val scaleSeconds = drawProperties.secondsScale < 1.0f
+                    if (drawProperties.secondsScale > 0f &&
+                        (renderParameters.drawMode != DrawMode.AMBIENT || scaleSeconds)
+                    ) {
+                        val restoreCount = canvas.save()
+                        if (scaleSeconds) {
+                            // Scale the canvas around the center of the seconds bounds.
+                            canvas.scale(
+                                drawProperties.secondsScale,
+                                drawProperties.secondsScale,
+                                minutesLeft + smallDigitWidth,
+                                top + digitHeight - smallDigitHeight / 2
+                            )
+                        }
 
-    override fun renderHighlightLayer(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
-        drawComplicationHighlights(canvas, zonedDateTime)
-    }
+                        // Draw seconds.
+                        val secondsTop = top + digitHeight - smallDigitHeight
+                        drawDigit(
+                            canvas,
+                            minutesLeft,
+                            secondsTop,
+                            currentDigitStrings,
+                            nextDigitStrings,
+                            DigitType.SECOND_TENS,
+                            secondProgress
+                        )
+                        drawDigit(
+                            canvas,
+                            minutesLeft + smallDigitWidth,
+                            secondsTop,
+                            currentDigitStrings,
+                            nextDigitStrings,
+                            DigitType.SECOND_UNITS,
+                            secondProgress
+                        )
 
-    override fun getMainClockElementBounds() = clockBounds
-
-    private fun recalculateBoundsIfChanged(bounds: Rect, zonedDateTime: ZonedDateTime) {
-        if (oldBounds == bounds) {
-            return
-        }
-
-        oldBounds.set(bounds)
-        calculateClockBound(bounds, zonedDateTime)
-    }
-
-    private fun calculateClockBound(bounds: Rect, zonedDateTime: ZonedDateTime) {
-        val hasVerticalComplication =
-            VERTICAL_COMPLICATION_IDS.any {
-                complicationSlotsManager[it]!!.isActiveAt(zonedDateTime.toInstant())
-            }
-        val hasHorizontalComplication =
-            HORIZONTAL_COMPLICATION_IDS.any {
-                complicationSlotsManager[it]!!.isActiveAt(zonedDateTime.toInstant())
-            }
-
-        val marginX = if (hasHorizontalComplication) {
-            (MARGIN_FRACTION_WITH_COMPLICATION.x * bounds.width().toFloat()).toInt()
-        } else {
-            (MARGIN_FRACTION_WITHOUT_COMPLICATION.x * bounds.width().toFloat()).toInt()
-        }
-
-        val marginY = if (hasVerticalComplication) {
-            (MARGIN_FRACTION_WITH_COMPLICATION.y * bounds.height().toFloat()).toInt()
-        } else {
-            (MARGIN_FRACTION_WITHOUT_COMPLICATION.y * bounds.height().toFloat()).toInt()
-        }
-
-        clockBounds.set(
-            bounds.left + marginX,
-            bounds.top + marginY,
-            bounds.right - marginX,
-            bounds.bottom - marginY
-        )
-
-        recalculateTextSize()
-    }
-
-    private fun recalculateTextSize() {
-        // Determine the font size to fit by measuring the text for a sample size and compare to
-        // the size. That assume the height of the text scales linearly.
-        val sampleTextSize = clockBounds.height().toFloat()
-        digitTextPaint.setTextSize(sampleTextSize)
-        digitTextPaint.getTextBounds(ALL_DIGITS, 0, ALL_DIGITS.length, digitBounds)
-        var textSize = clockBounds.height() * sampleTextSize / digitBounds.height().toFloat()
-        val textRatio = textSize / clockBounds.height().toFloat()
-
-        // Remain a top and bottom padding.
-        textSize *= (1f - 2f * DIGIT_PADDING_FRACTION)
-
-        // Calculate the total width fraction base on the text height.
-        val totalWidthFraction: Float =
-            (2f * DIGIT_WIDTH_FRACTION) + (DIGIT_WIDTH_FRACTION * GAP_WIDTH_FRACTION) +
-                (2f * SMALL_DIGIT_SIZE_FRACTION * SMALL_DIGIT_WIDTH_FRACTION)
-
-        textSize = min(textSize, (clockBounds.width().toFloat() / totalWidthFraction) * textRatio)
-
-        setDigitTextSize(textSize)
-    }
-
-    private fun setDigitTextSize(textSize: Float) {
-        clearDigitBitmapCache()
-
-        digitTextSize = textSize
-        digitTextPaint.textSize = digitTextSize
-        digitTextPaint.getTextBounds(ALL_DIGITS, 0, 1, digitBounds)
-        digitVerticalPadding = (digitBounds.height() * DIGIT_PADDING_FRACTION).toInt()
-        digitWidth = (digitBounds.height() * DIGIT_WIDTH_FRACTION).toInt()
-        digitHeight = digitBounds.height() + 2 * digitVerticalPadding
-
-        smallDigitTextSize = textSize * SMALL_DIGIT_SIZE_FRACTION
-        digitTextPaint.setTextSize(smallDigitTextSize)
-        digitTextPaint.getTextBounds(ALL_DIGITS, 0, 1, digitBounds)
-        smallDigitHeight = digitBounds.height() + 2 * digitVerticalPadding
-        smallDigitWidth = (digitBounds.height() * SMALL_DIGIT_WIDTH_FRACTION).toInt()
-
-        gapWidth = digitHeight * GAP_WIDTH_FRACTION
-
-        digitTextHoursPaint.textSize = digitTextSize
-        digitTextMinutesPaint.textSize = smallDigitTextSize
-        digitTextSecondsPaint.textSize = smallDigitTextSize
-    }
-
-    private fun drawBackground(canvas: Canvas) {
-        val backgroundColor = if (renderParameters.drawMode == DrawMode.AMBIENT) {
-            watchFaceColorStyle.ambientStyle.backgroundColor
-        } else {
-            watchFaceColorStyle.activeStyle.backgroundColor
-        }
-        canvas.drawColor(
-            getRGBColor(
-                backgroundColor,
-                drawProperties.backgroundAlpha,
-                Color.BLACK
-            )
-        )
-    }
-
-    private fun drawComplications(canvas: Canvas, zonedDateTime: ZonedDateTime) {
-        // First, draw the background complication if not in ambient mode
-        if (renderParameters.drawMode != DrawMode.AMBIENT) {
-            complicationSlotsManager[ComplicationID.BACKGROUND.ordinal]?.let {
-                if (it.complicationData.value.type != ComplicationType.NO_DATA) {
-                    it.render(
-                        canvas,
-                        zonedDateTime,
-                        renderParameters
-                    )
+                        canvas.restoreToCount(restoreCount)
+                    }
+                } finally {
+                    canvas.restoreToCount(wholeTimeSaveCount)
                 }
             }
         }
-        for (i in FOREGROUND_COMPLICATION_IDS) {
-            val complication = complicationSlotsManager[i] as ComplicationSlot
-            complication.render(canvas, zonedDateTime, renderParameters)
+
+        override fun renderHighlightLayer(
+            canvas: Canvas,
+            bounds: Rect,
+            zonedDateTime: ZonedDateTime
+        ) {
+            drawComplicationHighlights(canvas, zonedDateTime)
+        }
+
+        override fun getMainClockElementBounds() = clockBounds
+
+        private fun recalculateBoundsIfChanged(bounds: Rect, zonedDateTime: ZonedDateTime) {
+            if (oldBounds == bounds) {
+                return
+            }
+
+            oldBounds.set(bounds)
+            calculateClockBound(bounds, zonedDateTime)
+        }
+
+        private fun calculateClockBound(bounds: Rect, zonedDateTime: ZonedDateTime) {
+            val hasVerticalComplication =
+                VERTICAL_COMPLICATION_IDS.any {
+                    complicationSlotsManager[it]!!.isActiveAt(zonedDateTime.toInstant())
+                }
+            val hasHorizontalComplication =
+                HORIZONTAL_COMPLICATION_IDS.any {
+                    complicationSlotsManager[it]!!.isActiveAt(zonedDateTime.toInstant())
+                }
+
+            val marginX = if (hasHorizontalComplication) {
+                (MARGIN_FRACTION_WITH_COMPLICATION.x * bounds.width().toFloat()).toInt()
+            } else {
+                (MARGIN_FRACTION_WITHOUT_COMPLICATION.x * bounds.width().toFloat()).toInt()
+            }
+
+            val marginY = if (hasVerticalComplication) {
+                (MARGIN_FRACTION_WITH_COMPLICATION.y * bounds.height().toFloat()).toInt()
+            } else {
+                (MARGIN_FRACTION_WITHOUT_COMPLICATION.y * bounds.height().toFloat()).toInt()
+            }
+
+            clockBounds.set(
+                bounds.left + marginX,
+                bounds.top + marginY,
+                bounds.right - marginX,
+                bounds.bottom - marginY
+            )
+
+            recalculateTextSize()
+        }
+
+        private fun recalculateTextSize() {
+            // Determine the font size to fit by measuring the text for a sample size and compare to
+            // the size. That assume the height of the text scales linearly.
+            val sampleTextSize = clockBounds.height().toFloat()
+            digitTextPaint.setTextSize(sampleTextSize)
+            digitTextPaint.getTextBounds(ALL_DIGITS, 0, ALL_DIGITS.length, digitBounds)
+            var textSize = clockBounds.height() * sampleTextSize / digitBounds.height().toFloat()
+            val textRatio = textSize / clockBounds.height().toFloat()
+
+            // Remain a top and bottom padding.
+            textSize *= (1f - 2f * DIGIT_PADDING_FRACTION)
+
+            // Calculate the total width fraction base on the text height.
+            val totalWidthFraction: Float =
+                (2f * DIGIT_WIDTH_FRACTION) + (DIGIT_WIDTH_FRACTION * GAP_WIDTH_FRACTION) +
+                    (2f * SMALL_DIGIT_SIZE_FRACTION * SMALL_DIGIT_WIDTH_FRACTION)
+
+            textSize =
+                min(textSize, (clockBounds.width().toFloat() / totalWidthFraction) * textRatio)
+
+            setDigitTextSize(textSize)
+        }
+
+        private fun setDigitTextSize(textSize: Float) {
+            clearDigitBitmapCache()
+
+            digitTextSize = textSize
+            digitTextPaint.textSize = digitTextSize
+            digitTextPaint.getTextBounds(ALL_DIGITS, 0, 1, digitBounds)
+            digitVerticalPadding = (digitBounds.height() * DIGIT_PADDING_FRACTION).toInt()
+            digitWidth = (digitBounds.height() * DIGIT_WIDTH_FRACTION).toInt()
+            digitHeight = digitBounds.height() + 2 * digitVerticalPadding
+
+            smallDigitTextSize = textSize * SMALL_DIGIT_SIZE_FRACTION
+            digitTextPaint.setTextSize(smallDigitTextSize)
+            digitTextPaint.getTextBounds(ALL_DIGITS, 0, 1, digitBounds)
+            smallDigitHeight = digitBounds.height() + 2 * digitVerticalPadding
+            smallDigitWidth = (digitBounds.height() * SMALL_DIGIT_WIDTH_FRACTION).toInt()
+
+            gapWidth = digitHeight * GAP_WIDTH_FRACTION
+
+            digitTextHoursPaint.textSize = digitTextSize
+            digitTextMinutesPaint.textSize = smallDigitTextSize
+            digitTextSecondsPaint.textSize = smallDigitTextSize
+        }
+
+        private fun drawBackground(canvas: Canvas) {
+            val backgroundColor = if (renderParameters.drawMode == DrawMode.AMBIENT) {
+                watchFaceColorStyle.ambientStyle.backgroundColor
+            } else {
+                watchFaceColorStyle.activeStyle.backgroundColor
+            }
+            canvas.drawColor(
+                getRGBColor(
+                    backgroundColor,
+                    drawProperties.backgroundAlpha,
+                    Color.BLACK
+                )
+            )
+        }
+
+        private fun drawComplications(canvas: Canvas, zonedDateTime: ZonedDateTime) {
+            // First, draw the background complication if not in ambient mode
+            if (renderParameters.drawMode != DrawMode.AMBIENT) {
+                complicationSlotsManager[ComplicationID.BACKGROUND.ordinal]?.let {
+                    if (it.complicationData.value.type != ComplicationType.NO_DATA) {
+                        it.render(
+                            canvas,
+                            zonedDateTime,
+                            renderParameters
+                        )
+                    }
+                }
+            }
+            for (i in FOREGROUND_COMPLICATION_IDS) {
+                val complication = complicationSlotsManager[i] as ComplicationSlot
+                complication.render(canvas, zonedDateTime, renderParameters)
+            }
+        }
+
+        private fun drawComplicationHighlights(canvas: Canvas, zonedDateTime: ZonedDateTime) {
+            for (i in FOREGROUND_COMPLICATION_IDS) {
+                val complication = complicationSlotsManager[i] as ComplicationSlot
+                complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
+            }
+        }
+
+        private fun clearDigitBitmapCache() {
+            currentCachedDigits.clear()
+            digitBitmapCache.clear()
+        }
+
+        private fun drawDigit(
+            canvas: Canvas,
+            left: Float,
+            top: Float,
+            currentDigitStrings: DigitStrings,
+            nextDigitStrings: DigitStrings,
+            digitType: DigitType,
+            secondProgress: Float
+        ) {
+            val currentDigit = currentDigitStrings.get(digitType)
+            val nextDigit = nextDigitStrings.get(digitType)
+
+            // Draw the current digit bitmap.
+            if (currentDigit != nextDigit) {
+                drawDigitWithAnimation(
+                    canvas,
+                    left,
+                    top,
+                    secondProgress,
+                    currentDigit,
+                    digitType,
+                    DigitMode.OUTGOING
+                )
+                drawDigitWithAnimation(
+                    canvas,
+                    left,
+                    top,
+                    secondProgress,
+                    nextDigit,
+                    digitType,
+                    DigitMode.INCOMING
+                )
+            } else {
+                canvas.drawBitmap(
+                    getDigitBitmap(currentDigit, digitType),
+                    left,
+                    top,
+                    digitBitmapPaint
+                )
+            }
+        }
+
+        private fun drawDigitWithAnimation(
+            canvas: Canvas,
+            left: Float,
+            top: Float,
+            secondProgress: Float,
+            digit: String,
+            digitType: DigitType,
+            digitMode: DigitMode
+        ) {
+            getDigitDrawProperties(
+                secondProgress, getTimeOffsetSeconds(digitType), digitMode, digitDrawProperties
+            )
+
+            if (!digitDrawProperties.shouldDraw) {
+                return
+            }
+
+            val bitmap = getDigitBitmap(digit, digitType)
+            val centerX = left + bitmap.width / 2f
+            val centerY = top + bitmap.height / 2f
+            val restoreCount = canvas.save()
+            canvas.scale(digitDrawProperties.scale, digitDrawProperties.scale, centerX, centerY)
+            canvas.rotate(digitDrawProperties.rotation, centerX, centerY)
+            val bitmapPaint = digitBitmapPaint
+            bitmapPaint.alpha = (digitDrawProperties.opacity * 255.0f).toInt()
+            canvas.drawBitmap(bitmap, left, top, bitmapPaint)
+            bitmapPaint.alpha = 255
+            canvas.restoreToCount(restoreCount)
+        }
+
+        private fun createBitmap(
+            width: Int,
+            height: Int,
+            digitType: DigitType
+        ): Bitmap {
+            val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+            digitBitmapCache.put(digitType.ordinal, bitmap)
+            return bitmap
+        }
+
+        /**
+         * Returns a {@link Bitmap} representing the given {@code digit}, suitable for use as the
+         * given {@code digitType}.
+         */
+        private fun getDigitBitmap(digit: String, digitType: DigitType): Bitmap {
+            val width: Int
+            val height: Int
+            val paint: Paint
+            when (digitType) {
+                DigitType.HOUR_TENS, DigitType.HOUR_UNITS -> {
+                    width = digitWidth
+                    height = digitHeight
+                    paint = digitTextHoursPaint
+                }
+
+                DigitType.MINUTE_TENS, DigitType.MINUTE_UNITS -> {
+                    width = smallDigitWidth
+                    height = smallDigitHeight
+                    paint = digitTextMinutesPaint
+                }
+
+                DigitType.SECOND_TENS, DigitType.SECOND_UNITS -> {
+                    width = smallDigitWidth
+                    height = smallDigitHeight
+                    paint = digitTextSecondsPaint
+                }
+            }
+
+            val bitmap =
+                digitBitmapCache.get(digitType.ordinal) ?: createBitmap(width, height, digitType)
+            if (digit != currentCachedDigits.get(digitType.ordinal)) {
+                currentCachedDigits.put(digitType.ordinal, digit)
+
+                bitmap.eraseColor(Color.TRANSPARENT)
+                val canvas = Canvas(bitmap)
+                val textWidth = paint.measureText(digit)
+
+                // We use the same vertical padding here for all types and sizes of digit so that
+                // their tops and bottoms can be aligned.
+                canvas.drawText(
+                    digit,
+                    (width - textWidth) / 2,
+                    height.toFloat() - digitVerticalPadding,
+                    paint
+                )
+            }
+            return bitmap
         }
     }
 
-    private fun drawComplicationHighlights(canvas: Canvas, zonedDateTime: ZonedDateTime) {
-        for (i in FOREGROUND_COMPLICATION_IDS) {
-            val complication = complicationSlotsManager[i] as ComplicationSlot
-            complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
+    private data class Vec2f(val x: Float, val y: Float)
+
+    private enum class ComplicationID {
+        UPPER,
+        RIGHT,
+        LOWER,
+        LEFT,
+        BACKGROUND
+    }
+
+    // Changing digits are animated. This enum is used to label the start and end animation parameters.
+    private enum class DigitMode {
+        OUTGOING,
+        INCOMING
+    }
+
+    private enum class DigitType {
+        HOUR_TENS,
+        HOUR_UNITS,
+        MINUTE_TENS,
+        MINUTE_UNITS,
+        SECOND_TENS,
+        SECOND_UNITS
+    }
+
+    /** A class to provide string representations of each of the digits of a given time. */
+    private class DigitStrings {
+        private var hourTens = ""
+        private var hourUnits = ""
+        private var minuteTens = ""
+        private var minuteUnits = ""
+        private var secondTens = ""
+        private var secondUnits = ""
+
+        /** Sets the time represented by this instance. */
+        fun set(zonedDateTime: ZonedDateTime, is24Hour: Boolean) {
+            if (is24Hour) {
+                val hourValue = zonedDateTime.hour
+                hourTens = getTensDigitString(hourValue, true)
+                hourUnits = getUnitsDigitString(hourValue)
+            } else {
+                var hourValue = zonedDateTime.hour % 12
+                // We should show 12 for noon and midnight.
+                if (hourValue == 0) {
+                    hourValue = 12
+                }
+                hourTens = getTensDigitString(hourValue, false)
+                hourUnits = getUnitsDigitString(hourValue)
+            }
+
+            val minuteValue = zonedDateTime.minute
+            minuteTens = getTensDigitString(minuteValue, true)
+            minuteUnits = getUnitsDigitString(minuteValue)
+            val secondsValue = zonedDateTime.second
+            secondTens = getTensDigitString(secondsValue, true)
+            secondUnits = getUnitsDigitString(secondsValue)
+        }
+
+        /** Returns a string representing the specified digit of the time represented by this object. */
+        fun get(digitType: DigitType): String {
+            return when (digitType) {
+                DigitType.HOUR_TENS -> hourTens
+                DigitType.HOUR_UNITS -> hourUnits
+                DigitType.MINUTE_TENS -> minuteTens
+                DigitType.MINUTE_UNITS -> minuteUnits
+                DigitType.SECOND_TENS -> secondTens
+                DigitType.SECOND_UNITS -> secondUnits
+            }
+        }
+
+        /**
+         * Returns the number of hour digits in this object. If the representation is 24-hour, this will
+         * always return 2. If 12-hour, this will return 1 or 2.
+         */
+        fun getNumberOfHoursDigits(): Int {
+            return if (hourTens == "") 1 else 2
+        }
+
+        /**
+         * Returns a {@link String} representing the tens digit of the provided non-negative {@code
+         * value}. If {@code padWithZeroes} is true, returns zero if {@code value} < 10. If {@code
+         * padWithZeroes} is false, returns an empty string if {@code value} < 10.
+         */
+        private fun getTensDigitString(value: Int, padWithZeroes: Boolean): String {
+            if (value < 10 && !padWithZeroes) {
+                return ""
+            }
+            // We don't use toString() because during draw calls we don't want to avoid allocating objects.
+            return DIGITS[(value / 10) % 10]
+        }
+
+        /**
+         * Returns a {@link String} representing the units digit of the provided non-negative {@code
+         * value}.
+         */
+        private fun getUnitsDigitString(value: Int): String {
+            // We don't use toString() because during draw calls we don't want to avoid allocating objects.
+            return DIGITS[value % 10]
         }
     }
 
-    private fun clearDigitBitmapCache() {
-        currentCachedDigits.clear()
-        digitBitmapCache.clear()
-    }
+    private data class DigitDrawProperties(
+        var shouldDraw: Boolean = false,
+        var scale: Float = 0f,
+        var rotation: Float = 0f,
+        var opacity: Float = 0f
+    )
 
-    private fun drawDigit(
-        canvas: Canvas,
-        left: Float,
-        top: Float,
-        currentDigitStrings: DigitStrings,
-        nextDigitStrings: DigitStrings,
-        digitType: DigitType,
-        secondProgress: Float
+    private class DrawProperties(
+        var backgroundAlpha: Float = 1f,
+        var timeScale: Float = 1f,
+        var secondsScale: Float = 1f
     ) {
-        val currentDigit = currentDigitStrings.get(digitType)
-        val nextDigit = nextDigitStrings.get(digitType)
+        companion object {
+            val TIME_SCALE = object : FloatProperty<DrawProperties>("timeScale") {
+                override fun setValue(obj: DrawProperties, value: Float) {
+                    obj.timeScale = value
+                }
 
-        // Draw the current digit bitmap.
-        if (currentDigit != nextDigit) {
-            drawDigitWithAnimation(
-                canvas,
-                left,
-                top,
-                secondProgress,
-                currentDigit,
-                digitType,
-                DigitMode.OUTGOING
-            )
-            drawDigitWithAnimation(
-                canvas,
-                left,
-                top,
-                secondProgress,
-                nextDigit,
-                digitType,
-                DigitMode.INCOMING
-            )
-        } else {
-            canvas.drawBitmap(
-                getDigitBitmap(currentDigit, digitType),
-                left,
-                top,
-                digitBitmapPaint
-            )
+                override fun get(obj: DrawProperties): Float {
+                    return obj.timeScale
+                }
+            }
+
+            val SECONDS_SCALE = object : FloatProperty<DrawProperties>("secondsScale") {
+                override fun setValue(obj: DrawProperties, value: Float) {
+                    obj.secondsScale = value
+                }
+
+                override fun get(obj: DrawProperties): Float {
+                    return obj.secondsScale
+                }
+            }
         }
     }
 
-    private fun drawDigitWithAnimation(
-        canvas: Canvas,
-        left: Float,
-        top: Float,
-        secondProgress: Float,
-        digit: String,
-        digitType: DigitType,
-        digitMode: DigitMode
-    ) {
-        getDigitDrawProperties(
-            secondProgress, getTimeOffsetSeconds(digitType), digitMode, digitDrawProperties
+    companion object {
+        private const val COLOR_STYLE_SETTING = "color_style_setting"
+        private const val RED_STYLE = "red_style"
+        private const val GREEN_STYLE = "green_style"
+        private const val BLUE_STYLE = "blue_style"
+
+        // Render at approximately 60fps in interactive mode.
+        private const val INTERACTIVE_UPDATE_RATE_MS = 16L
+
+        // Constants for the size of complication.
+        private val CIRCLE_COMPLICATION_DIAMETER_FRACTION = Vec2f(0.252f, 0.252f)
+        private val ROUND_RECT_COMPLICATION_SIZE_FRACTION = Vec2f(0.645f, 0.168f)
+
+        // Constants for the upper complication location.
+        private val UPPER_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.21f)
+        private val UPPER_ROUND_RECT_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.21f)
+
+        // Constants for the lower complication location.
+        private val LOWER_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.79f)
+        private val LOWER_ROUND_RECT_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.79f)
+
+        // Constants for the left complication location.
+        private val LEFT_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.177f, 0.5f)
+
+        // Constants for the right complication location.
+        private val RIGHT_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.823f, 0.5f)
+
+        // Constants for the clock digits' position, based on the height and width of given bounds.
+        private val MARGIN_FRACTION_WITHOUT_COMPLICATION = Vec2f(0.2f, 0.2f)
+        private val MARGIN_FRACTION_WITH_COMPLICATION = Vec2f(0.4f, 0.4f)
+
+        private val VERTICAL_COMPLICATION_IDS = arrayOf(
+            ComplicationID.UPPER.ordinal,
+            ComplicationID.LOWER.ordinal
+        )
+        private val HORIZONTAL_COMPLICATION_IDS = arrayOf(
+            ComplicationID.LEFT.ordinal,
+            ComplicationID.RIGHT.ordinal
+        )
+        private val FOREGROUND_COMPLICATION_IDS = arrayOf(
+            ComplicationID.UPPER.ordinal, ComplicationID.RIGHT.ordinal,
+            ComplicationID.LOWER.ordinal, ComplicationID.LEFT.ordinal
         )
 
-        if (!digitDrawProperties.shouldDraw) {
-            return
-        }
+        // The name of the font used for drawing the text in the digit watch face.
+        private const val DIGITAL_TYPE_FACE = "sans-serif-condensed-light"
 
-        val bitmap = getDigitBitmap(digit, digitType)
-        val centerX = left + bitmap.width / 2f
-        val centerY = top + bitmap.height / 2f
-        val restoreCount = canvas.save()
-        canvas.scale(digitDrawProperties.scale, digitDrawProperties.scale, centerX, centerY)
-        canvas.rotate(digitDrawProperties.rotation, centerX, centerY)
-        val bitmapPaint = digitBitmapPaint
-        bitmapPaint.alpha = (digitDrawProperties.opacity * 255.0f).toInt()
-        canvas.drawBitmap(bitmap, left, top, bitmapPaint)
-        bitmapPaint.alpha = 255
-        canvas.restoreToCount(restoreCount)
-    }
+        // The width of the large digit bitmaps, as a fraction of their height.
+        private const val DIGIT_WIDTH_FRACTION = 0.65f
 
-    private fun createBitmap(
-        width: Int,
-        height: Int,
-        digitType: DigitType
-    ): Bitmap {
-        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
-        digitBitmapCache.put(digitType.ordinal, bitmap)
-        return bitmap
-    }
+        // The height of the small digits (used for minutes and seconds), given as a fraction of the  height
+        // of the large digits.
+        private const val SMALL_DIGIT_SIZE_FRACTION = 0.45f
 
-    /**
-     * Returns a {@link Bitmap} representing the given {@code digit}, suitable for use as the
-     * given {@code digitType}.
-     */
-    private fun getDigitBitmap(digit: String, digitType: DigitType): Bitmap {
-        val width: Int
-        val height: Int
-        val paint: Paint
-        when (digitType) {
-            DigitType.HOUR_TENS, DigitType.HOUR_UNITS -> {
-                width = digitWidth
-                height = digitHeight
-                paint = digitTextHoursPaint
-            }
-            DigitType.MINUTE_TENS, DigitType.MINUTE_UNITS -> {
-                width = smallDigitWidth
-                height = smallDigitHeight
-                paint = digitTextMinutesPaint
-            }
-            DigitType.SECOND_TENS, DigitType.SECOND_UNITS -> {
-                width = smallDigitWidth
-                height = smallDigitHeight
-                paint = digitTextSecondsPaint
-            }
-        }
+        // The width of the small digit bitmaps, as a fraction of their height.
+        private const val SMALL_DIGIT_WIDTH_FRACTION = 0.7f
 
-        val bitmap =
-            digitBitmapCache.get(digitType.ordinal) ?: createBitmap(width, height, digitType)
-        if (digit != currentCachedDigits.get(digitType.ordinal)) {
-            currentCachedDigits.put(digitType.ordinal, digit)
+        // The padding at the top and bottom of the digit bitmaps, given as a fraction of the height.
+        // Needed as some characters may ascend or descend slightly (e.g. "8").
+        private const val DIGIT_PADDING_FRACTION = 0.05f
 
-            bitmap.eraseColor(Color.TRANSPARENT)
-            val canvas = Canvas(bitmap)
-            val textWidth = paint.measureText(digit)
+        // The gap between the hours and the minutes/seconds, given as a fraction of the width of the large
+        // digits.
+        private const val GAP_WIDTH_FRACTION = 0.1f
 
-            // We use the same vertical padding here for all types and sizes of digit so that
-            // their tops and bottoms can be aligned.
-            canvas.drawText(
-                digit,
-                (width - textWidth) / 2,
-                height.toFloat() - digitVerticalPadding,
-                paint
+        // A string containing all digits, used to measure their height.
+        private const val ALL_DIGITS = "0123456789"
+
+        private val DIGITS = ALL_DIGITS.toCharArray().map { it.toString() }
+
+        // The start and end times of the animations, expressed as a fraction of a second.
+        // (So 0.5 means that the animation of that digit will begin half-way through the second).
+        // Note that because we only cache one digit of each type, the current and next times must
+        // not overlap.
+        private val DIGIT_ANIMATION_START_TIME_FRACTION = mapOf(
+            DigitMode.OUTGOING to 0.5f,
+            DigitMode.INCOMING to 0.667f
+        )
+        private val DIGIT_ANIMATION_END_TIME = mapOf(
+            DigitMode.OUTGOING to 0.667f,
+            DigitMode.INCOMING to 1f
+        )
+        private const val POSITION_ANIMATION_START_TIME = 0.0833f
+        private const val POSITION_ANIMATION_END_TIME = 0.5833f
+
+        // Parameters governing the animation of the current and next digits. NB Scale is a size multiplier.
+        // The first index is the values for the outgoing digit, and the second index for the incoming
+        // digit. If seconds are changing from 1 -> 2 for example, the 1 will scale from 1f to 0.65f, and
+        // rotate from 0f to 82f. The 2 will scale from 0.65f to 1f, and rotate from -97f to 0f.
+        private val DIGIT_SCALE_START = mapOf(DigitMode.OUTGOING to 1f, DigitMode.INCOMING to 0.65f)
+        private val DIGIT_SCALE_END = mapOf(DigitMode.OUTGOING to 0.65f, DigitMode.INCOMING to 1f)
+        private val DIGIT_ROTATE_START_DEGREES = mapOf(
+            DigitMode.OUTGOING to 0f,
+            DigitMode.INCOMING to -97f
+        )
+        private val DIGIT_ROTATE_END_DEGREES =
+            mapOf(DigitMode.OUTGOING to 82f, DigitMode.INCOMING to 0f)
+        private val DIGIT_OPACITY_START =
+            mapOf(DigitMode.OUTGOING to 1f, DigitMode.INCOMING to 0.07f)
+        private val DIGIT_OPACITY_END = mapOf(DigitMode.OUTGOING to 0f, DigitMode.INCOMING to 1f)
+
+        // The offset used to stagger the animation when multiple digits are animating at the same time.
+        private const val TIME_OFFSET_SECONDS_PER_DIGIT_TYPE = -5 / 60f
+
+        // The duration of the ambient mode change animation.
+        private const val AMBIENT_TRANSITION_MS = 333L
+
+        private val DIGIT_SCALE_INTERPOLATOR = mapOf(
+            DigitMode.OUTGOING to PathInterpolator(0.4f, 0f, 0.67f, 1f),
+            DigitMode.INCOMING to PathInterpolator(0.33f, 0f, 0.2f, 1f)
+        )
+        private val DIGIT_ROTATION_INTERPOLATOR = mapOf(
+            DigitMode.OUTGOING to PathInterpolator(0.57f, 0f, 0.73f, 0.49f),
+            DigitMode.INCOMING to PathInterpolator(0.15f, 0.49f, 0.37f, 1f)
+        )
+        private val DIGIT_OPACITY_INTERPOLATOR = mapOf(
+            DigitMode.OUTGOING to PathInterpolator(0.4f, 0f, 1f, 1f),
+            DigitMode.INCOMING to PathInterpolator(0f, 0f, 0.2f, 1f)
+        )
+        private val CENTERING_ADJUSTMENT_INTERPOLATOR =
+            PathInterpolator(0.4f, 0f, 0.2f, 1f)
+
+        @ColorInt
+        private fun colorRgb(red: Float, green: Float, blue: Float) =
+            0xff000000.toInt() or
+                ((red * 255.0f + 0.5f).toInt() shl 16) or
+                ((green * 255.0f + 0.5f).toInt() shl 8) or
+                (blue * 255.0f + 0.5f).toInt()
+
+        private fun redFraction(@ColorInt color: Int) = Color.red(color).toFloat() / 255.0f
+
+        private fun greenFraction(@ColorInt color: Int) = Color.green(color).toFloat() / 255.0f
+
+        private fun blueFraction(@ColorInt color: Int) = Color.blue(color).toFloat() / 255.0f
+
+        /**
+         * Returns an RGB color that has the same effect as drawing `color` with `alphaFraction` over a
+         * `backgroundColor` background.
+         *
+         * @param color the foreground color
+         * @param alphaFraction the fraction of the alpha value, range from 0 to 1
+         * @param backgroundColor the background color
+         */
+        private fun getRGBColor(
+            @ColorInt color: Int,
+            alphaFraction: Float,
+            @ColorInt backgroundColor: Int
+        ): Int {
+            return colorRgb(
+                lerp(redFraction(backgroundColor), redFraction(color), alphaFraction),
+                lerp(greenFraction(backgroundColor), greenFraction(color), alphaFraction),
+                lerp(blueFraction(backgroundColor), blueFraction(color), alphaFraction)
             )
         }
-        return bitmap
+
+        /** Returns a linear interpolation between a and b using the scalar s.  */
+        private fun lerp(a: Float, b: Float, s: Float) = a + s * (b - a)
+
+        /**
+         * Returns the interpolation scalar (s) that satisfies the equation: `value = lerp(a, b, s)`
+         *
+         * If `a == b`, then this function will return 0.
+         */
+        private fun lerpInv(a: Float, b: Float, value: Float) =
+            if (a != b) (value - a) / (b - a) else 0.0f
+
+        private fun getInterpolatedValue(
+            startValue: Float,
+            endValue: Float,
+            startTime: Float,
+            endTime: Float,
+            currentTime: Float,
+            interpolator: TimeInterpolator
+        ): Float {
+            val progress = when {
+                currentTime < startTime -> 0f
+                currentTime > endTime -> 1f
+                else -> interpolator.getInterpolation(lerpInv(startTime, endTime, currentTime))
+            }
+            return lerp(startValue, endValue, progress)
+        }
+
+        /**
+         * Sets the [DigitDrawProperties] that should be used for drawing, given the specified
+         * parameters.
+         *
+         * @param secondProgress the sub-second part of the current time, where 0 means the current second
+         * has just begun, and 1 means the current second has just ended
+         * @param offsetSeconds a value added to the start and end time of the animations
+         * @param digitMode whether the digit is OUTGOING or INCOMING
+         * @param output the [DigitDrawProperties] that will be set
+         */
+        private fun getDigitDrawProperties(
+            secondProgress: Float,
+            offsetSeconds: Float,
+            digitMode: DigitMode,
+            output: DigitDrawProperties
+        ) {
+            val startTime = DIGIT_ANIMATION_START_TIME_FRACTION[digitMode]!! + offsetSeconds
+            val endTime = DIGIT_ANIMATION_END_TIME[digitMode]!! + offsetSeconds
+            output.shouldDraw = if (digitMode == DigitMode.OUTGOING) {
+                secondProgress < endTime
+            } else {
+                secondProgress >= startTime
+            }
+            output.scale = getInterpolatedValue(
+                DIGIT_SCALE_START[digitMode]!!,
+                DIGIT_SCALE_END[digitMode]!!,
+                startTime,
+                endTime,
+                secondProgress,
+                DIGIT_SCALE_INTERPOLATOR[digitMode]!!
+            )
+            output.rotation = getInterpolatedValue(
+                DIGIT_ROTATE_START_DEGREES[digitMode]!!,
+                DIGIT_ROTATE_END_DEGREES[digitMode]!!,
+                startTime,
+                endTime,
+                secondProgress,
+                DIGIT_ROTATION_INTERPOLATOR[digitMode]!!
+            )
+            output.opacity = getInterpolatedValue(
+                DIGIT_OPACITY_START[digitMode]!!,
+                DIGIT_OPACITY_END[digitMode]!!,
+                startTime,
+                endTime,
+                secondProgress,
+                DIGIT_OPACITY_INTERPOLATOR[digitMode]!!
+            )
+        }
+
+        private fun getTimeOffsetSeconds(digitType: DigitType): Float {
+            return when (digitType) {
+                DigitType.HOUR_TENS -> 5f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+                DigitType.HOUR_UNITS -> 4f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+                DigitType.MINUTE_TENS -> 3f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+                DigitType.MINUTE_UNITS -> 2f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+                DigitType.SECOND_TENS -> 1f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+                DigitType.SECOND_UNITS -> 0f
+            }
+        }
+
+        /** Applies a multiplier to a color, e.g. to darken if it's < 1.0 */
+        private fun multiplyColor(colorInt: Int, multiplier: Float): Int {
+            val adjustedMultiplier = multiplier / 255.0f
+            return colorRgb(
+                Color.red(colorInt).toFloat() * adjustedMultiplier,
+                Color.green(colorInt).toFloat() * adjustedMultiplier,
+                Color.blue(colorInt).toFloat() * adjustedMultiplier,
+            )
+        }
+
+        private fun createBoundsRect(
+            centerFraction: PointF,
+            size: Vec2f
+        ): RectF {
+            val halfWidth = size.x / 2.0f
+            val halfHeight = size.y / 2.0f
+            return RectF(
+                (centerFraction.x - halfWidth),
+                (centerFraction.y - halfHeight),
+                (centerFraction.x + halfWidth),
+                (centerFraction.y + halfHeight)
+            )
+        }
     }
 }
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
index 1ecd8a4..45a6c9b3 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
@@ -45,21 +45,6 @@
 import kotlin.math.cos
 import kotlin.math.sin
 
-private val timeText = charArrayOf('1', '0', ':', '0', '9')
-private val secondsText = charArrayOf('3', '0')
-
-@Px
-private val SECONDS_TEXT_HEIGHT = 180f
-
-@Px
-private val TIME_TEXT_ACTIVE_HEIGHT = 64f
-
-@Px
-private val TIME_TEXT_AMBIENT_HEIGHT = 96f
-
-@Px
-private val TEXT_PADDING = 12
-
 open class ExampleHierarchicalStyleWatchFaceService : WatchFaceService() {
 
     internal val twelveHourClockOption by lazy {
@@ -271,159 +256,188 @@
             }
         }
     )
-}
 
-class ExampleHierarchicalStyleWatchFaceRenderer {
-    internal val paint = Paint().apply {
-        textAlign = Paint.Align.CENTER
-        isAntiAlias = true
-    }
-
-    internal val textBounds = Rect()
-
-    @Px
-    internal val timeActiveHeight = Rect().apply {
-        paint.textSize = TIME_TEXT_ACTIVE_HEIGHT
-        paint.getTextBounds(timeText, 0, timeText.size, this)
-    }.height()
-
-    @Px
-    internal val timeAmbientHeight = Rect().apply {
-        paint.textSize = TIME_TEXT_AMBIENT_HEIGHT
-        paint.getTextBounds(timeText, 0, timeText.size, this)
-    }.height()
-
-    @Px
-    internal val secondsHeight = Rect().apply {
-        paint.textSize = SECONDS_TEXT_HEIGHT
-        paint.getTextBounds(secondsText, 0, secondsText.size, this)
-    }.height()
-
-    @Px
-    internal val timeActiveOffset =
-        (timeActiveHeight + secondsHeight + TEXT_PADDING) / 2 - timeActiveHeight
-
-    @Px
-    internal val timeAmbientOffset = timeAmbientHeight / 2 - timeAmbientHeight
-
-    @Px
-    internal val secondsActiveOffset = timeActiveOffset - secondsHeight - TEXT_PADDING
-
-    fun renderDigital(
-        canvas: Canvas,
-        bounds: Rect,
-        zonedDateTime: ZonedDateTime,
-        renderParameters: RenderParameters,
-        watchState: WatchState,
-        twentyFourHour: Boolean,
-        color: IntArray
-    ) {
-        val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
-        val hour = zonedDateTime.hour % if (twentyFourHour) 24 else 12
-        val minute = zonedDateTime.minute
-        val second = zonedDateTime.second
-
-        paint.color = if (isActive) {
-            val scale = 64 + 192 * second / 60
-            Color.rgb(color[0] * scale, color[1] * scale, color[2] * scale)
-        } else {
-           Color.BLACK
+    private class ExampleHierarchicalStyleWatchFaceRenderer {
+        internal val paint = Paint().apply {
+            textAlign = Paint.Align.CENTER
+            isAntiAlias = true
         }
-        canvas.drawRect(bounds, paint)
 
-        paint.color = Color.WHITE
-        timeText[0] = ('0' + hour / 10)
-        timeText[1] = ('0' + hour % 10)
-        timeText[2] = if (second % 2 == 0) ':' else ' '
-        timeText[3] = ('0' + minute / 10)
-        timeText[4] = ('0' + minute % 10)
-        paint.textSize = if (isActive) TIME_TEXT_ACTIVE_HEIGHT else TIME_TEXT_AMBIENT_HEIGHT
-        val timeOffset = if (isActive) timeActiveOffset else timeAmbientOffset
-        canvas.drawText(
-            timeText,
-            0,
-            timeText.size,
-            bounds.centerX().toFloat(),
-            (bounds.centerY() - watchState.chinHeight - timeOffset).toFloat(),
-            paint
-        )
+        internal val textBounds = Rect()
 
-        paint.textSize = SECONDS_TEXT_HEIGHT
-        if (isActive) {
-            secondsText[0] = ('0' + second / 10)
-            secondsText[1] = ('0' + second % 10)
+        @Px
+        internal val timeActiveHeight = Rect().apply {
+            paint.textSize = TIME_TEXT_ACTIVE_HEIGHT
+            paint.getTextBounds(timeText, 0, timeText.size, this)
+        }.height()
+
+        @Px
+        internal val timeAmbientHeight = Rect().apply {
+            paint.textSize = TIME_TEXT_AMBIENT_HEIGHT
+            paint.getTextBounds(timeText, 0, timeText.size, this)
+        }.height()
+
+        @Px
+        internal val secondsHeight = Rect().apply {
+            paint.textSize = SECONDS_TEXT_HEIGHT
+            paint.getTextBounds(secondsText, 0, secondsText.size, this)
+        }.height()
+
+        @Px
+        internal val timeActiveOffset =
+            (timeActiveHeight + secondsHeight + TEXT_PADDING) / 2 - timeActiveHeight
+
+        @Px
+        internal val timeAmbientOffset = timeAmbientHeight / 2 - timeAmbientHeight
+
+        @Px
+        internal val secondsActiveOffset = timeActiveOffset - secondsHeight - TEXT_PADDING
+
+        fun renderDigital(
+            canvas: Canvas,
+            bounds: Rect,
+            zonedDateTime: ZonedDateTime,
+            renderParameters: RenderParameters,
+            watchState: WatchState,
+            twentyFourHour: Boolean,
+            color: IntArray
+        ) {
+            val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
+            val hour = zonedDateTime.hour % if (twentyFourHour) 24 else 12
+            val minute = zonedDateTime.minute
+            val second = zonedDateTime.second
+
+            paint.color = if (isActive) {
+                val scale = 64 + 192 * second / 60
+                Color.rgb(color[0] * scale, color[1] * scale, color[2] * scale)
+            } else {
+                Color.BLACK
+            }
+            canvas.drawRect(bounds, paint)
+
+            paint.color = Color.WHITE
+            timeText[0] = ('0' + hour / 10)
+            timeText[1] = ('0' + hour % 10)
+            timeText[2] = if (second % 2 == 0) ':' else ' '
+            timeText[3] = ('0' + minute / 10)
+            timeText[4] = ('0' + minute % 10)
+            paint.textSize = if (isActive) TIME_TEXT_ACTIVE_HEIGHT else TIME_TEXT_AMBIENT_HEIGHT
+            val timeOffset = if (isActive) timeActiveOffset else timeAmbientOffset
             canvas.drawText(
-                secondsText,
+                timeText,
                 0,
-                secondsText.size,
+                timeText.size,
                 bounds.centerX().toFloat(),
-                (bounds.centerY() - watchState.chinHeight - secondsActiveOffset).toFloat(),
+                (bounds.centerY() - watchState.chinHeight - timeOffset).toFloat(),
                 paint
             )
-        }
-    }
 
-    fun renderAnalog(
-        canvas: Canvas,
-        bounds: Rect,
-        zonedDateTime: ZonedDateTime,
-        renderParameters: RenderParameters,
-        hoursDrawFreq: Int,
-        watchHandLength: Float
-    ) {
-        val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
-
-        paint.color = Color.BLACK
-        canvas.drawRect(bounds, paint)
-
-        paint.color = Color.WHITE
-        paint.textSize = 20.0f
-        if (isActive) {
-            for (i in 12 downTo 1 step hoursDrawFreq) {
-                val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
-                val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
-                val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
-                val mark = i.toString()
-                paint.getTextBounds(mark, 0, mark.length, textBounds)
+            paint.textSize = SECONDS_TEXT_HEIGHT
+            if (isActive) {
+                secondsText[0] = ('0' + second / 10)
+                secondsText[1] = ('0' + second % 10)
                 canvas.drawText(
-                    mark,
-                    bounds.exactCenterX() + dx - textBounds.width() / 2.0f,
-                    bounds.exactCenterY() + dy + textBounds.height() / 2.0f,
+                    secondsText,
+                    0,
+                    secondsText.size,
+                    bounds.centerX().toFloat(),
+                    (bounds.centerY() - watchState.chinHeight - secondsActiveOffset).toFloat(),
                     paint
                 )
             }
         }
 
-        val hours = (zonedDateTime.hour % 12).toFloat()
-        val minutes = zonedDateTime.minute.toFloat()
-        val seconds = zonedDateTime.second.toFloat() +
-            (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
+        fun renderAnalog(
+            canvas: Canvas,
+            bounds: Rect,
+            zonedDateTime: ZonedDateTime,
+            renderParameters: RenderParameters,
+            hoursDrawFreq: Int,
+            watchHandLength: Float
+        ) {
+            val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
 
-        val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
-        val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
+            paint.color = Color.BLACK
+            canvas.drawRect(bounds, paint)
 
-        val hourXRadius = bounds.width() * watchHandLength * 0.35f
-        val hourYRadius = bounds.height() * watchHandLength * 0.35f
+            paint.color = Color.WHITE
+            paint.textSize = 20.0f
+            if (isActive) {
+                for (i in 12 downTo 1 step hoursDrawFreq) {
+                    val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
+                    val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
+                    val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
+                    val mark = i.toString()
+                    paint.getTextBounds(mark, 0, mark.length, textBounds)
+                    canvas.drawText(
+                        mark,
+                        bounds.exactCenterX() + dx - textBounds.width() / 2.0f,
+                        bounds.exactCenterY() + dy + textBounds.height() / 2.0f,
+                        paint
+                    )
+                }
+            }
 
-        paint.strokeWidth = if (isActive) 8f else 5f
-        canvas.drawLine(
-            bounds.exactCenterX(),
-            bounds.exactCenterY(),
-            bounds.exactCenterX() + sin(hourRot) * hourXRadius,
-            bounds.exactCenterY() - cos(hourRot) * hourYRadius,
-            paint
-        )
+            val hours = (zonedDateTime.hour % 12).toFloat()
+            val minutes = zonedDateTime.minute.toFloat()
+            val seconds = zonedDateTime.second.toFloat() +
+                (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
 
-        val minuteXRadius = bounds.width() * watchHandLength * 0.499f
-        val minuteYRadius = bounds.height() * watchHandLength * 0.499f
+            val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
+            val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
 
-        paint.strokeWidth = if (isActive) 4f else 2.5f
-        canvas.drawLine(
-            bounds.exactCenterX(),
-            bounds.exactCenterY(),
-            bounds.exactCenterX() + sin(minuteRot) * minuteXRadius,
-            bounds.exactCenterY() - cos(minuteRot) * minuteYRadius,
-            paint
-        )
+            val hourXRadius = bounds.width() * watchHandLength * 0.35f
+            val hourYRadius = bounds.height() * watchHandLength * 0.35f
+
+            paint.strokeWidth = if (isActive) 8f else 5f
+            canvas.drawLine(
+                bounds.exactCenterX(),
+                bounds.exactCenterY(),
+                bounds.exactCenterX() + sin(hourRot) * hourXRadius,
+                bounds.exactCenterY() - cos(hourRot) * hourYRadius,
+                paint
+            )
+
+            val minuteXRadius = bounds.width() * watchHandLength * 0.499f
+            val minuteYRadius = bounds.height() * watchHandLength * 0.499f
+
+            paint.strokeWidth = if (isActive) 4f else 2.5f
+            canvas.drawLine(
+                bounds.exactCenterX(),
+                bounds.exactCenterY(),
+                bounds.exactCenterX() + sin(minuteRot) * minuteXRadius,
+                bounds.exactCenterY() - cos(minuteRot) * minuteYRadius,
+                paint
+            )
+        }
+    }
+
+    companion object {
+        private const val COLOR_STYLE_SETTING = "color_style_setting"
+        private const val RED_STYLE = "red_style"
+        private const val GREEN_STYLE = "green_style"
+        private const val BLUE_STYLE = "blue_style"
+
+        private const val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
+        private const val HOURS_DRAW_FREQ_STYLE_SETTING = "hours_draw_freq_style_setting"
+        private const val HOURS_DRAW_FREQ_MIN = 1L
+        private const val HOURS_DRAW_FREQ_MAX = 4L
+        private const val HOURS_DRAW_FREQ_DEFAULT = 3L
+        private const val NUMBER_RADIUS_FRACTION = 0.45f
+
+        private val timeText = charArrayOf('1', '0', ':', '0', '9')
+        private val secondsText = charArrayOf('3', '0')
+
+        @Px
+        private val SECONDS_TEXT_HEIGHT = 180f
+
+        @Px
+        private val TIME_TEXT_ACTIVE_HEIGHT = 64f
+
+        @Px
+        private val TIME_TEXT_AMBIENT_HEIGHT = 96f
+
+        @Px
+        private val TEXT_PADDING = 12
     }
 }
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
index 3e014f0..7e40c2a 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
@@ -39,12 +39,6 @@
 import androidx.wear.watchface.style.WatchFaceLayer
 import java.time.ZonedDateTime
 
-/** Expected frame rate in interactive mode.  */
-private const val FPS: Long = 60
-
-/** How long each frame is displayed at expected frame rate.  */
-private const val FRAME_PERIOD_MS: Long = 1000 / FPS
-
 /**
  * Sample watch face using OpenGL with textures loaded on a background thread by [createWatchFace]
  * which are used for rendering on the main thread.
@@ -101,242 +95,250 @@
                 Intent(this, ComplicationRationalActivity::class.java)
             )
     }
-}
 
-// SharedAssets shared between each instance of the watch face.
-internal class ExampleSharedAssets(resources: Resources) : Renderer.SharedAssets {
-    val yellowWatchBodyTexture = loadTextureFromResource(resources, R.drawable.wf_background)
-    val blueWatchBodyTexture = loadTextureFromResource(resources, R.drawable.wf_background_blue)
-    val watchHandTexture = loadTextureFromResource(resources, R.drawable.hand)
+    // SharedAssets shared between each instance of the watch face.
+    private class ExampleSharedAssets(resources: Resources) : Renderer.SharedAssets {
+        val yellowWatchBodyTexture = loadTextureFromResource(resources, R.drawable.wf_background)
+        val blueWatchBodyTexture = loadTextureFromResource(resources, R.drawable.wf_background_blue)
+        val watchHandTexture = loadTextureFromResource(resources, R.drawable.hand)
 
-    val triangleTextureProgram = Gles2TexturedTriangleList.Program()
-    val backgroundQuad = createTexturedQuad(
-        triangleTextureProgram, -10f, -10f, 20f, 20f
-    )
-    val secondHandQuad = createTexturedQuad(
-        triangleTextureProgram, -0.75f, -6f, 1.5f, 8f
-    )
-    val minuteHandQuad = createTexturedQuad(
-        triangleTextureProgram, -0.33f, -4.5f, 0.66f, 6f
-    )
-    val hourHandQuad = createTexturedQuad(
-        triangleTextureProgram, -0.25f, -3f, 0.5f, 4f
-    )
-
-    override fun onDestroy() {
-        GLES20.glDeleteTextures(
-            3,
-            intArrayOf(yellowWatchBodyTexture, blueWatchBodyTexture, watchHandTexture),
-            0
+        val triangleTextureProgram = Gles2TexturedTriangleList.Program()
+        val backgroundQuad = createTexturedQuad(
+            triangleTextureProgram, -10f, -10f, 20f, 20f
         )
-        checkGLError("glDeleteTextures")
+        val secondHandQuad = createTexturedQuad(
+            triangleTextureProgram, -0.75f, -6f, 1.5f, 8f
+        )
+        val minuteHandQuad = createTexturedQuad(
+            triangleTextureProgram, -0.33f, -4.5f, 0.66f, 6f
+        )
+        val hourHandQuad = createTexturedQuad(
+            triangleTextureProgram, -0.25f, -3f, 0.5f, 4f
+        )
 
-        triangleTextureProgram.onDestroy()
+        override fun onDestroy() {
+            GLES20.glDeleteTextures(
+                3,
+                intArrayOf(yellowWatchBodyTexture, blueWatchBodyTexture, watchHandTexture),
+                0
+            )
+            checkGLError("glDeleteTextures")
+
+            triangleTextureProgram.onDestroy()
+        }
+
+        private fun createTexturedQuad(
+            program: Gles2TexturedTriangleList.Program,
+            left: Float,
+            top: Float,
+            width: Float,
+            height: Float
+        ) = Gles2TexturedTriangleList(
+            program,
+            floatArrayOf(
+                top + 0f,
+                left + 0.0f,
+                0.0f,
+
+                top + 0.0f,
+                left + width,
+                0.0f,
+
+                top + height,
+                left + 0.0f,
+                0.0f,
+
+                top + 0.0f,
+                left + width,
+                0.0f,
+
+                top + height,
+                left + 0.0f,
+                0.0f,
+
+                top + height,
+                left + width,
+                0.0f
+            ),
+            floatArrayOf(
+                1.0f,
+                1.0f,
+
+                1.0f,
+                0.0f,
+
+                0.0f,
+                1.0f,
+
+                1.0f,
+                0.0f,
+
+                0.0f,
+                1.0f,
+
+                0.0f,
+                0.0f
+            )
+        )
+
+        private fun loadTextureFromResource(resources: Resources, resourceId: Int): Int {
+            val textureHandle = IntArray(1)
+            GLES20.glGenTextures(1, textureHandle, 0)
+            if (textureHandle[0] != 0) {
+                val bitmap = BitmapFactory.decodeResource(
+                    resources,
+                    resourceId,
+                    BitmapFactory.Options().apply {
+                        inScaled = false // No pre-scaling
+                    }
+                )
+                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0])
+                checkGLError("glBindTexture")
+                GLES20.glTexParameteri(
+                    GLES20.GL_TEXTURE_2D,
+                    GLES20.GL_TEXTURE_MIN_FILTER,
+                    GLES20.GL_NEAREST
+                )
+                GLES20.glTexParameteri(
+                    GLES20.GL_TEXTURE_2D,
+                    GLES20.GL_TEXTURE_MAG_FILTER,
+                    GLES20.GL_NEAREST
+                )
+                GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
+                bitmap.recycle()
+            }
+            return textureHandle[0]
+        }
     }
 
-    private fun createTexturedQuad(
-        program: Gles2TexturedTriangleList.Program,
-        left: Float,
-        top: Float,
-        width: Float,
-        height: Float
-    ) = Gles2TexturedTriangleList(
-        program,
-        floatArrayOf(
-            top + 0f,
-            left + 0.0f,
-            0.0f,
+    private class MainThreadRenderer(
+        surfaceHolder: SurfaceHolder,
+        private val currentUserStyleRepository: CurrentUserStyleRepository,
+        watchState: WatchState,
+        private val resources: Resources,
+        private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
+    ) : Renderer.GlesRenderer2<ExampleSharedAssets>(
+        surfaceHolder,
+        currentUserStyleRepository,
+        watchState,
+        FRAME_PERIOD_MS
+    ) {
+        private val projectionMatrix = FloatArray(16)
+        private val viewMatrix = FloatArray(16).apply {
+            Matrix.setLookAtM(
+                this,
+                0,
+                0f,
+                0f,
+                -1.0f,
+                0f,
+                0f,
+                0f,
+                0f,
+                1f,
+                0f
+            )
+        }
+        private val handPositionMatrix = FloatArray(16)
+        private val handViewMatrix = FloatArray(16)
+        private val vpMatrix = FloatArray(16)
 
-            top + 0.0f,
-            left + width,
-            0.0f,
+        override suspend fun createSharedAssets() = ExampleSharedAssets(resources)
 
-            top + height,
-            left + 0.0f,
-            0.0f,
+        override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
+            GLES20.glEnable(GLES20.GL_TEXTURE_2D)
 
-            top + 0.0f,
-            left + width,
-            0.0f,
+            // Update the projection matrix based on the new aspect ratio.
+            val aspectRatio = width.toFloat() / height.toFloat()
+            Matrix.frustumM(
+                projectionMatrix,
+                0 /* offset */,
+                -aspectRatio /* left */,
+                aspectRatio /* right */,
+                -1f /* bottom */,
+                1f /* top */,
+                0.1f /* near */,
+                100f /* far */
+            )
+        }
 
-            top + height,
-            left + 0.0f,
-            0.0f,
+        override fun render(zonedDateTime: ZonedDateTime, sharedAssets: ExampleSharedAssets) {
+            checkGLError("renders")
+            GLES20.glClearColor(0f, 1f, 0f, 1f)
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
 
-            top + height,
-            left + width,
-            0.0f
-        ),
-        floatArrayOf(
-            1.0f,
-            1.0f,
-
-            1.0f,
-            0.0f,
-
-            0.0f,
-            1.0f,
-
-            1.0f,
-            0.0f,
-
-            0.0f,
-            1.0f,
-
-            0.0f,
-            0.0f
-        )
-    )
-
-    private fun loadTextureFromResource(resources: Resources, resourceId: Int): Int {
-        val textureHandle = IntArray(1)
-        GLES20.glGenTextures(1, textureHandle, 0)
-        if (textureHandle[0] != 0) {
-            val bitmap = BitmapFactory.decodeResource(
-                resources,
-                resourceId,
-                BitmapFactory.Options().apply {
-                    inScaled = false // No pre-scaling
+            sharedAssets.triangleTextureProgram.bindProgramAndAttribs()
+            GLES20.glBindTexture(
+                GLES20.GL_TEXTURE_2D,
+                when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()) {
+                    "yellow_style" -> sharedAssets.yellowWatchBodyTexture
+                    "blue_style" -> sharedAssets.blueWatchBodyTexture
+                    else -> throw UnsupportedOperationException()
                 }
             )
-            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0])
-            checkGLError("glBindTexture")
-            GLES20.glTexParameteri(
-                GLES20.GL_TEXTURE_2D,
-                GLES20.GL_TEXTURE_MIN_FILTER,
-                GLES20.GL_NEAREST
-            )
-            GLES20.glTexParameteri(
-                GLES20.GL_TEXTURE_2D,
-                GLES20.GL_TEXTURE_MAG_FILTER,
-                GLES20.GL_NEAREST
-            )
-            GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
-            bitmap.recycle()
+
+            GLES20.glEnable(GLES20.GL_BLEND)
+            GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
+
+            Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
+            sharedAssets.backgroundQuad.draw(vpMatrix)
+
+            GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
+            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, sharedAssets.watchHandTexture)
+
+            val hours = (zonedDateTime.hour % 12).toFloat()
+            val minutes = zonedDateTime.minute.toFloat()
+            val seconds = zonedDateTime.second.toFloat() +
+                (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
+
+            val secondsRot = seconds / 60.0f * 360.0f
+            Matrix.setRotateM(handPositionMatrix, 0, secondsRot, 0f, 0f, 1f)
+            Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
+            Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
+            sharedAssets.secondHandQuad.draw(vpMatrix)
+
+            val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
+            Matrix.setRotateM(handPositionMatrix, 0, minuteRot, 0f, 0f, 1f)
+            Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
+            Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
+            sharedAssets.minuteHandQuad.draw(vpMatrix)
+
+            val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
+            Matrix.setRotateM(handPositionMatrix, 0, hourRot, 0f, 0f, 1f)
+            Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
+            Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
+            sharedAssets.hourHandQuad.draw(vpMatrix)
+
+            sharedAssets.triangleTextureProgram.unbindAttribs()
         }
-        return textureHandle[0]
-    }
-}
 
-internal fun checkGLError(op: String) {
-    var error: Int
-    while (GLES20.glGetError().also { error = it } != GLES20.GL_NO_ERROR) {
-        System.err.println("OpenGL Error $op: glError $error")
-    }
-}
-
-internal class MainThreadRenderer(
-    surfaceHolder: SurfaceHolder,
-    private val currentUserStyleRepository: CurrentUserStyleRepository,
-    watchState: WatchState,
-    private val resources: Resources,
-    private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
-) : Renderer.GlesRenderer2<ExampleSharedAssets>(
-    surfaceHolder,
-    currentUserStyleRepository,
-    watchState,
-    FRAME_PERIOD_MS
-) {
-    private val projectionMatrix = FloatArray(16)
-    private val viewMatrix = FloatArray(16).apply {
-        Matrix.setLookAtM(
-            this,
-            0,
-            0f,
-            0f,
-            -1.0f,
-            0f,
-            0f,
-            0f,
-            0f,
-            1f,
-            0f
-        )
-    }
-    private val handPositionMatrix = FloatArray(16)
-    private val handViewMatrix = FloatArray(16)
-    private val vpMatrix = FloatArray(16)
-
-    override suspend fun createSharedAssets() = ExampleSharedAssets(resources)
-
-    override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
-        GLES20.glEnable(GLES20.GL_TEXTURE_2D)
-
-        // Update the projection matrix based on the new aspect ratio.
-        val aspectRatio = width.toFloat() / height.toFloat()
-        Matrix.frustumM(
-            projectionMatrix,
-            0 /* offset */,
-            -aspectRatio /* left */,
-            aspectRatio /* right */,
-            -1f /* bottom */,
-            1f /* top */,
-            0.1f /* near */,
-            100f /* far */
-        )
+        override fun renderHighlightLayer(
+            zonedDateTime: ZonedDateTime,
+            sharedAssets: ExampleSharedAssets
+        ) {
+            val highlightLayer = renderParameters.highlightLayer!!
+            GLES20.glClearColor(
+                Color.red(highlightLayer.backgroundTint).toFloat() / 256.0f,
+                Color.green(highlightLayer.backgroundTint).toFloat() / 256.0f,
+                Color.blue(highlightLayer.backgroundTint).toFloat() / 256.0f,
+                Color.alpha(highlightLayer.backgroundTint).toFloat() / 256.0f
+            )
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        }
     }
 
-    override fun render(zonedDateTime: ZonedDateTime, sharedAssets: ExampleSharedAssets) {
-        checkGLError("renders")
-        GLES20.glClearColor(0f, 1f, 0f, 1f)
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+    companion object {
+        /** Expected frame rate in interactive mode.  */
+        private const val FPS: Long = 60
 
-        sharedAssets.triangleTextureProgram.bindProgramAndAttribs()
-        GLES20.glBindTexture(
-            GLES20.GL_TEXTURE_2D,
-            when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()) {
-                "yellow_style" -> sharedAssets.yellowWatchBodyTexture
-                "blue_style" -> sharedAssets.blueWatchBodyTexture
-                else -> throw UnsupportedOperationException()
+        /** How long each frame is displayed at expected frame rate.  */
+        private const val FRAME_PERIOD_MS: Long = 1000 / FPS
+
+        private fun checkGLError(op: String) {
+            var error: Int
+            while (GLES20.glGetError().also { error = it } != GLES20.GL_NO_ERROR) {
+                System.err.println("OpenGL Error $op: glError $error")
             }
-        )
-
-        GLES20.glEnable(GLES20.GL_BLEND)
-        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
-
-        Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
-        sharedAssets.backgroundQuad.draw(vpMatrix)
-
-        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
-        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, sharedAssets.watchHandTexture)
-
-        val hours = (zonedDateTime.hour % 12).toFloat()
-        val minutes = zonedDateTime.minute.toFloat()
-        val seconds = zonedDateTime.second.toFloat() +
-            (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
-
-        val secondsRot = seconds / 60.0f * 360.0f
-        Matrix.setRotateM(handPositionMatrix, 0, secondsRot, 0f, 0f, 1f)
-        Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
-        Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
-        sharedAssets.secondHandQuad.draw(vpMatrix)
-
-        val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
-        Matrix.setRotateM(handPositionMatrix, 0, minuteRot, 0f, 0f, 1f)
-        Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
-        Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
-        sharedAssets.minuteHandQuad.draw(vpMatrix)
-
-        val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
-        Matrix.setRotateM(handPositionMatrix, 0, hourRot, 0f, 0f, 1f)
-        Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
-        Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
-        sharedAssets.hourHandQuad.draw(vpMatrix)
-
-        sharedAssets.triangleTextureProgram.unbindAttribs()
-    }
-
-    override fun renderHighlightLayer(
-        zonedDateTime: ZonedDateTime,
-        sharedAssets: ExampleSharedAssets
-    ) {
-        val highlightLayer = renderParameters.highlightLayer!!
-        GLES20.glClearColor(
-            Color.red(highlightLayer.backgroundTint).toFloat() / 256.0f,
-            Color.green(highlightLayer.backgroundTint).toFloat() / 256.0f,
-            Color.blue(highlightLayer.backgroundTint).toFloat() / 256.0f,
-            Color.alpha(highlightLayer.backgroundTint).toFloat() / 256.0f
-        )
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        }
     }
 }
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index d205f90..72ec126 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -21,17 +21,10 @@
 import android.graphics.RectF
 import android.graphics.drawable.Icon
 import android.opengl.GLES20
-import android.opengl.GLU
-import android.opengl.GLUtils
 import android.opengl.Matrix
-import android.util.Log
 import android.view.Gravity
 import android.view.SurfaceHolder
 import androidx.annotation.RequiresApi
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.ComplicationSlot
 import androidx.wear.watchface.ComplicationSlotsManager
 import androidx.wear.watchface.DrawMode
@@ -42,6 +35,10 @@
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationDeniedActivity
 import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationRationalActivity
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -52,9 +49,6 @@
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import androidx.wear.watchface.style.WatchFaceLayer
-import java.nio.ByteBuffer
-import java.nio.ByteOrder
-import java.nio.FloatBuffer
 import java.time.ZonedDateTime
 import kotlin.math.cos
 import kotlin.math.sin
@@ -62,23 +56,6 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 
-/** Expected frame rate in interactive mode.  */
-private const val FPS: Long = 60
-
-/** Z distance from the camera to the watchface.  */
-private const val EYE_Z = -2.3f
-
-/** How long each frame is displayed at expected frame rate.  */
-private const val FRAME_PERIOD_MS: Long = 1000 / FPS
-
-/** Cycle time before the camera motion repeats.  */
-private const val CYCLE_PERIOD_SECONDS: Long = 5
-
-/** Number of camera angles to precompute. */
-private const val numCameraAngles = (CYCLE_PERIOD_SECONDS * FPS).toInt()
-
-const val EXAMPLE_OPENGL_COMPLICATION_ID = 101
-
 /**
  * Sample watch face using OpenGL. The watch face is rendered using
  * [Gles2ColoredTriangleList]s. The camera moves around in interactive mode and stops moving
@@ -128,6 +105,8 @@
         },
         listOf(
             ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
             ComplicationType.LONG_TEXT,
             ComplicationType.SHORT_TEXT,
             ComplicationType.MONOCHROMATIC_IMAGE,
@@ -174,191 +153,209 @@
         .setComplicationRationaleDialogIntent(
             Intent(this, ComplicationRationalActivity::class.java)
         )
-}
 
-@OptIn(WatchFaceExperimental::class)
-@Suppress("Deprecation")
-@RequiresApi(27)
-class ExampleOpenGLRenderer(
-    surfaceHolder: SurfaceHolder,
-    private val currentUserStyleRepository: CurrentUserStyleRepository,
-    watchState: WatchState,
-    private val colorStyleSetting: ListUserStyleSetting,
-    private val complicationSlot: ComplicationSlot
-) : Renderer.GlesRenderer(surfaceHolder, currentUserStyleRepository, watchState, FRAME_PERIOD_MS) {
+    @OptIn(WatchFaceExperimental::class)
+    @Suppress("Deprecation")
+    @RequiresApi(27)
+    private class ExampleOpenGLRenderer(
+        surfaceHolder: SurfaceHolder,
+        private val currentUserStyleRepository: CurrentUserStyleRepository,
+        watchState: WatchState,
+        private val colorStyleSetting: ListUserStyleSetting,
+        private val complicationSlot: ComplicationSlot
+    ) : Renderer.GlesRenderer(
+        surfaceHolder,
+        currentUserStyleRepository,
+        watchState,
+        FRAME_PERIOD_MS
+    ) {
 
-    /** Projection transformation matrix. Converts from 3D to 2D.  */
-    private val projectionMatrix = FloatArray(16)
+        /** Projection transformation matrix. Converts from 3D to 2D.  */
+        private val projectionMatrix = FloatArray(16)
 
-    /**
-     * View transformation matrices to use in interactive mode. Converts from world to camera-
-     * relative coordinates. One matrix per camera position.
-     */
-    private val viewMatrices = Array(numCameraAngles) { FloatArray(16) }
+        /**
+         * View transformation matrices to use in interactive mode. Converts from world to camera-
+         * relative coordinates. One matrix per camera position.
+         */
+        private val viewMatrices = Array(numCameraAngles) { FloatArray(16) }
 
-    /** The view transformation matrix to use in ambient mode  */
-    private val ambientViewMatrix = FloatArray(16)
+        /** The view transformation matrix to use in ambient mode  */
+        private val ambientViewMatrix = FloatArray(16)
 
-    /**
-     * Model transformation matrices. Converts from model-relative coordinates to world
-     * coordinates. One matrix per degree of rotation.
-     */
-    private val modelMatrices = Array(360) { FloatArray(16) }
+        /**
+         * Model transformation matrices. Converts from model-relative coordinates to world
+         * coordinates. One matrix per degree of rotation.
+         */
+        private val modelMatrices = Array(360) { FloatArray(16) }
 
-    /**
-     * Products of [viewMatrices] and [projectionMatrix]. One matrix per camera
-     * position.
-     */
-    private val vpMatrices = Array(numCameraAngles) { FloatArray(16) }
+        /**
+         * Products of [viewMatrices] and [projectionMatrix]. One matrix per camera
+         * position.
+         */
+        private val vpMatrices = Array(numCameraAngles) { FloatArray(16) }
 
-    /** The product of [ambientViewMatrix] and [projectionMatrix]  */
-    private val ambientVpMatrix = FloatArray(16)
+        /** The product of [ambientViewMatrix] and [projectionMatrix]  */
+        private val ambientVpMatrix = FloatArray(16)
 
-    /**
-     * Product of [modelMatrices], [viewMatrices], and
-     * [projectionMatrix].
-     */
-    private val mvpMatrix = FloatArray(16)
+        /**
+         * Product of [modelMatrices], [viewMatrices], and
+         * [projectionMatrix].
+         */
+        private val mvpMatrix = FloatArray(16)
 
-    /** Triangles for the 4 major ticks. These are grouped together to speed up rendering.  */
-    private lateinit var majorTickTriangles: Gles2ColoredTriangleList
+        /** Triangles for the 4 major ticks. These are grouped together to speed up rendering.  */
+        private lateinit var majorTickTriangles: Gles2ColoredTriangleList
 
-    /** Triangles for the 8 minor ticks. These are grouped together to speed up rendering.  */
-    private lateinit var minorTickTriangles: Gles2ColoredTriangleList
+        /** Triangles for the 8 minor ticks. These are grouped together to speed up rendering.  */
+        private lateinit var minorTickTriangles: Gles2ColoredTriangleList
 
-    /** Triangle for the second hand.  */
-    private lateinit var secondHandTriangleMap: Map<String, Gles2ColoredTriangleList>
+        /** Triangle for the second hand.  */
+        private lateinit var secondHandTriangleMap: Map<String, Gles2ColoredTriangleList>
 
-    /** Triangle for the minute hand.  */
-    private lateinit var minuteHandTriangle: Gles2ColoredTriangleList
+        /** Triangle for the minute hand.  */
+        private lateinit var minuteHandTriangle: Gles2ColoredTriangleList
 
-    /** Triangle for the hour hand.  */
-    private lateinit var hourHandTriangle: Gles2ColoredTriangleList
+        /** Triangle for the hour hand.  */
+        private lateinit var hourHandTriangle: Gles2ColoredTriangleList
 
-    private lateinit var complicationTexture: GlesTextureComplication
+        private lateinit var complicationTexture: GlesTextureComplication
 
-    private lateinit var coloredTriangleProgram: Gles2ColoredTriangleList.Program
-    private lateinit var textureTriangleProgram: Gles2TexturedTriangleList.Program
+        private lateinit var coloredTriangleProgram: Gles2ColoredTriangleList.Program
+        private lateinit var textureTriangleProgram: Gles2TexturedTriangleList.Program
 
-    private lateinit var complicationTriangles: Gles2TexturedTriangleList
-    private lateinit var complicationHighlightTriangles: Gles2ColoredTriangleList
+        private lateinit var complicationTriangles: Gles2TexturedTriangleList
+        private lateinit var complicationHighlightTriangles: Gles2ColoredTriangleList
 
-    init {
-        CoroutineScope(Dispatchers.Main.immediate).launch {
-            currentUserStyleRepository.userStyle.collect { userStyle ->
-                watchfaceColors = when (userStyle[colorStyleSetting]!!.toString()) {
-                    "red_style" -> WatchFaceColors(
-                        Color.valueOf(0.5f, 0.2f, 0.2f, 1f),
-                        Color.valueOf(0.4f, 0.15f, 0.15f, 1f),
-                        Color.valueOf(0.1f, 0.1f, 0.1f, 1f)
-                    )
+        init {
+            CoroutineScope(Dispatchers.Main.immediate).launch {
+                currentUserStyleRepository.userStyle.collect { userStyle ->
+                    watchfaceColors = when (userStyle[colorStyleSetting]!!.toString()) {
+                        "red_style" -> WatchFaceColors(
+                            Color.valueOf(0.5f, 0.2f, 0.2f, 1f),
+                            Color.valueOf(0.4f, 0.15f, 0.15f, 1f),
+                            Color.valueOf(0.1f, 0.1f, 0.1f, 1f)
+                        )
 
-                    "green_style" -> WatchFaceColors(
-                        Color.valueOf(0.2f, 0.5f, 0.2f, 1f),
-                        Color.valueOf(0.15f, 0.4f, 0.15f, 1f),
-                        Color.valueOf(0.1f, 0.1f, 0.1f, 1f)
-                    )
+                        "green_style" -> WatchFaceColors(
+                            Color.valueOf(0.2f, 0.5f, 0.2f, 1f),
+                            Color.valueOf(0.15f, 0.4f, 0.15f, 1f),
+                            Color.valueOf(0.1f, 0.1f, 0.1f, 1f)
+                        )
 
-                    else -> null
+                        else -> null
+                    }
                 }
             }
         }
-    }
 
-    override suspend fun onBackgroundThreadGlContextCreated() {
-        // Create program for drawing triangles.
-        coloredTriangleProgram = Gles2ColoredTriangleList.Program()
+        override suspend fun onBackgroundThreadGlContextCreated() {
+            // Create program for drawing triangles.
+            coloredTriangleProgram = Gles2ColoredTriangleList.Program()
 
-        // Create triangles for the ticks.
-        majorTickTriangles = createMajorTicks(coloredTriangleProgram)
-        minorTickTriangles = createMinorTicks(coloredTriangleProgram)
+            // Create triangles for the ticks.
+            majorTickTriangles = createMajorTicks(coloredTriangleProgram)
+            minorTickTriangles = createMinorTicks(coloredTriangleProgram)
 
-        // Create program for drawing triangles.
-        textureTriangleProgram = Gles2TexturedTriangleList.Program()
-        complicationTriangles = createComplicationQuad(
-            textureTriangleProgram,
-            -0.9f,
-            -0.1f,
-            0.6f,
-            0.6f
-        )
+            // Create program for drawing triangles.
+            textureTriangleProgram = Gles2TexturedTriangleList.Program()
+            complicationTriangles = createComplicationQuad(
+                textureTriangleProgram,
+                -0.9f,
+                -0.1f,
+                0.6f,
+                0.6f
+            )
 
-        complicationHighlightTriangles = createComplicationHighlightQuad(
-            coloredTriangleProgram,
-            -0.9f,
-            -0.1f,
-            0.6f,
-            0.6f
-        )
+            complicationHighlightTriangles = createComplicationHighlightQuad(
+                coloredTriangleProgram,
+                -0.9f,
+                -0.1f,
+                0.6f,
+                0.6f
+            )
 
-        // Create triangles for the hands.
-        secondHandTriangleMap = mapOf(
-            "red_style" to
-                createHand(
-                    coloredTriangleProgram,
-                    0.02f /* width */,
-                    1.0f /* height */,
-                    floatArrayOf(
-                        1.0f /* red */,
-                        0.0f /* green */,
-                        0.0f /* blue */,
-                        1.0f /* alpha */
+            // Create triangles for the hands.
+            secondHandTriangleMap = mapOf(
+                "red_style" to
+                    createHand(
+                        coloredTriangleProgram,
+                        0.02f /* width */,
+                        1.0f /* height */,
+                        floatArrayOf(
+                            1.0f /* red */,
+                            0.0f /* green */,
+                            0.0f /* blue */,
+                            1.0f /* alpha */
+                        )
+                    ),
+                "greenstyle" to
+                    createHand(
+                        coloredTriangleProgram,
+                        0.02f /* width */,
+                        1.0f /* height */,
+                        floatArrayOf(
+                            0.0f /* red */,
+                            1.0f /* green */,
+                            0.0f /* blue */,
+                            1.0f /* alpha */
+                        )
                     )
-                ),
-            "greenstyle" to
-                createHand(
-                    coloredTriangleProgram,
-                    0.02f /* width */,
-                    1.0f /* height */,
-                    floatArrayOf(
-                        0.0f /* red */,
-                        1.0f /* green */,
-                        0.0f /* blue */,
-                        1.0f /* alpha */
-                    )
+            )
+            minuteHandTriangle = createHand(
+                coloredTriangleProgram,
+                0.06f /* width */,
+                1f /* height */,
+                floatArrayOf(
+                    0.7f /* red */,
+                    0.7f /* green */,
+                    0.7f /* blue */,
+                    1.0f /* alpha */
                 )
-        )
-        minuteHandTriangle = createHand(
-            coloredTriangleProgram,
-            0.06f /* width */,
-            1f /* height */,
-            floatArrayOf(
-                0.7f /* red */,
-                0.7f /* green */,
-                0.7f /* blue */,
-                1.0f /* alpha */
             )
-        )
-        hourHandTriangle = createHand(
-            coloredTriangleProgram,
-            0.1f /* width */,
-            0.6f /* height */,
-            floatArrayOf(
-                0.9f /* red */,
-                0.9f /* green */,
-                0.9f /* blue */,
-                1.0f /* alpha */
+            hourHandTriangle = createHand(
+                coloredTriangleProgram,
+                0.1f /* width */,
+                0.6f /* height */,
+                floatArrayOf(
+                    0.9f /* red */,
+                    0.9f /* green */,
+                    0.9f /* blue */,
+                    1.0f /* alpha */
+                )
             )
-        )
 
-        // Precompute the clock angles.
-        for (i in modelMatrices.indices) {
-            Matrix.setRotateM(modelMatrices[i], 0, i.toFloat(), 0f, 0f, 1f)
-        }
+            // Precompute the clock angles.
+            for (i in modelMatrices.indices) {
+                Matrix.setRotateM(modelMatrices[i], 0, i.toFloat(), 0f, 0f, 1f)
+            }
 
-        // Precompute the camera angles.
-        for (i in 0 until numCameraAngles) {
-            // Set the camera position (View matrix). When active, move the eye around to show
-            // off that this is 3D.
-            val cameraAngle =
-                (i.toFloat() / numCameraAngles * 2 * Math.PI).toFloat()
-            val eyeX = Math.cos(cameraAngle.toDouble()).toFloat()
-            val eyeY = Math.sin(cameraAngle.toDouble()).toFloat()
+            // Precompute the camera angles.
+            for (i in 0 until numCameraAngles) {
+                // Set the camera position (View matrix). When active, move the eye around to show
+                // off that this is 3D.
+                val cameraAngle =
+                    (i.toFloat() / numCameraAngles * 2 * Math.PI).toFloat()
+                val eyeX = Math.cos(cameraAngle.toDouble()).toFloat()
+                val eyeY = Math.sin(cameraAngle.toDouble()).toFloat()
+                Matrix.setLookAtM(
+                    viewMatrices[i],
+                    0,
+                    eyeX,
+                    eyeY,
+                    EYE_Z,
+                    0f,
+                    0f,
+                    0f,
+                    0f,
+                    1f,
+                    0f
+                ) // up vector
+            }
             Matrix.setLookAtM(
-                viewMatrices[i],
+                ambientViewMatrix,
                 0,
-                eyeX,
-                eyeY,
+                0f,
+                0f,
                 EYE_Z,
                 0f,
                 0f,
@@ -367,1025 +364,422 @@
                 1f,
                 0f
             ) // up vector
-        }
-        Matrix.setLookAtM(
-            ambientViewMatrix,
-            0,
-            0f,
-            0f,
-            EYE_Z,
-            0f,
-            0f,
-            0f,
-            0f,
-            1f,
-            0f
-        ) // up vector
 
-        complicationTexture = GlesTextureComplication(
-            complicationSlot,
-            128,
-            128,
-            GLES20.GL_TEXTURE_2D
-        )
-    }
-
-    override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
-        // Update the projection matrix based on the new aspect ratio.
-        val aspectRatio = width.toFloat() / height
-        Matrix.frustumM(
-            projectionMatrix,
-            0 /* offset */,
-            -aspectRatio /* left */,
-            aspectRatio /* right */,
-            -1f /* bottom */,
-            1f /* top */,
-            2f /* near */,
-            7f /* far */
-        )
-
-        // Precompute the products of Projection and View matrices for each camera angle.
-        for (i in 0 until numCameraAngles) {
-            Matrix.multiplyMM(vpMatrices[i], 0, projectionMatrix, 0, viewMatrices[i], 0)
-        }
-        Matrix.multiplyMM(ambientVpMatrix, 0, projectionMatrix, 0, ambientViewMatrix, 0)
-    }
-
-    /**
-     * Creates a triangle for a hand on the watch face.
-     *
-     * @param program program for drawing triangles
-     * @param width width of base of triangle
-     * @param length length of triangle
-     * @param color color in RGBA order, each in the range [0, 1]
-     */
-    private fun createHand(
-        program: Gles2ColoredTriangleList.Program,
-        width: Float,
-        length: Float,
-        color: FloatArray
-    ) = Gles2ColoredTriangleList(
-        program,
-        floatArrayOf(
-            0f,
-            length,
-            0f,
-            -width / 2,
-            0f,
-            0f,
-            width / 2,
-            0f,
-            0f
-        ),
-        color
-    )
-
-    /**
-     * Creates a triangle list for the major ticks on the watch face.
-     *
-     * @param program program for drawing triangles
-     */
-    private fun createMajorTicks(
-        program: Gles2ColoredTriangleList.Program
-    ): Gles2ColoredTriangleList {
-        // Create the data for the VBO.
-        val trianglesCoords = FloatArray(9 * 4)
-        for (i in 0..3) {
-            val triangleCoords = getMajorTickTriangleCoords(i)
-            System.arraycopy(
-                triangleCoords,
-                0,
-                trianglesCoords,
-                i * 9,
-                triangleCoords.size
+            complicationTexture = GlesTextureComplication(
+                complicationSlot,
+                128,
+                128,
+                GLES20.GL_TEXTURE_2D
             )
         }
-        return Gles2ColoredTriangleList(
-            program, trianglesCoords,
+
+        override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
+            // Update the projection matrix based on the new aspect ratio.
+            val aspectRatio = width.toFloat() / height
+            Matrix.frustumM(
+                projectionMatrix,
+                0 /* offset */,
+                -aspectRatio /* left */,
+                aspectRatio /* right */,
+                -1f /* bottom */,
+                1f /* top */,
+                2f /* near */,
+                7f /* far */
+            )
+
+            // Precompute the products of Projection and View matrices for each camera angle.
+            for (i in 0 until numCameraAngles) {
+                Matrix.multiplyMM(vpMatrices[i], 0, projectionMatrix, 0, viewMatrices[i], 0)
+            }
+            Matrix.multiplyMM(ambientVpMatrix, 0, projectionMatrix, 0, ambientViewMatrix, 0)
+        }
+
+        /**
+         * Creates a triangle for a hand on the watch face.
+         *
+         * @param program program for drawing triangles
+         * @param width width of base of triangle
+         * @param length length of triangle
+         * @param color color in RGBA order, each in the range [0, 1]
+         */
+        private fun createHand(
+            program: Gles2ColoredTriangleList.Program,
+            width: Float,
+            length: Float,
+            color: FloatArray
+        ) = Gles2ColoredTriangleList(
+            program,
+            floatArrayOf(
+                0f,
+                length,
+                0f,
+                -width / 2,
+                0f,
+                0f,
+                width / 2,
+                0f,
+                0f
+            ),
+            color
+        )
+
+        /**
+         * Creates a triangle list for the major ticks on the watch face.
+         *
+         * @param program program for drawing triangles
+         */
+        private fun createMajorTicks(
+            program: Gles2ColoredTriangleList.Program
+        ): Gles2ColoredTriangleList {
+            // Create the data for the VBO.
+            val trianglesCoords = FloatArray(9 * 4)
+            for (i in 0..3) {
+                val triangleCoords = getMajorTickTriangleCoords(i)
+                System.arraycopy(
+                    triangleCoords,
+                    0,
+                    trianglesCoords,
+                    i * 9,
+                    triangleCoords.size
+                )
+            }
+            return Gles2ColoredTriangleList(
+                program, trianglesCoords,
+                floatArrayOf(
+                    1.0f /* red */,
+                    1.0f /* green */,
+                    1.0f /* blue */,
+                    1.0f /* alpha */
+                )
+            )
+        }
+
+        /**
+         * Creates a triangle list for the minor ticks on the watch face.
+         *
+         * @param program program for drawing triangles
+         */
+        private fun createMinorTicks(
+            program: Gles2ColoredTriangleList.Program
+        ): Gles2ColoredTriangleList {
+            // Create the data for the VBO.
+            val trianglesCoords = FloatArray(9 * (12 - 4))
+            var index = 0
+            for (i in 0..11) {
+                if (i % 3 == 0) {
+                    // This is where a major tick goes, so skip it.
+                    continue
+                }
+                val triangleCoords = getMinorTickTriangleCoords(i)
+                System.arraycopy(
+                    triangleCoords,
+                    0,
+                    trianglesCoords,
+                    index,
+                    triangleCoords.size
+                )
+                index += 9
+            }
+            return Gles2ColoredTriangleList(
+                program, trianglesCoords,
+                floatArrayOf(
+                    0.5f /* red */,
+                    0.5f /* green */,
+                    0.5f /* blue */,
+                    1.0f /* alpha */
+                )
+            )
+        }
+
+        /**
+         * Creates a triangle list for the complication.
+         */
+        private fun createComplicationQuad(
+            program: Gles2TexturedTriangleList.Program,
+            left: Float,
+            top: Float,
+            width: Float,
+            height: Float
+        ) = Gles2TexturedTriangleList(
+            program,
+            floatArrayOf(
+                top + 0.0f,
+                left + 0.0f,
+                0.0f,
+
+                top + 0.0f,
+                left + width,
+                0.0f,
+
+                top + height,
+                left + 0.0f,
+                0.0f,
+
+                top + 0.0f,
+                left + width,
+                0.0f,
+
+                top + height,
+                left + 0.0f,
+                0.0f,
+
+                top + height,
+                left + width,
+                0.0f
+            ),
+            floatArrayOf(
+                1.0f,
+                1.0f,
+
+                1.0f,
+                0.0f,
+
+                0.0f,
+                1.0f,
+
+                1.0f,
+                0.0f,
+
+                0.0f,
+                1.0f,
+
+                0.0f,
+                0.0f
+            )
+        )
+
+        /**
+         * Creates a triangle list for the complication highlight quad.
+         */
+        private fun createComplicationHighlightQuad(
+            program: Gles2ColoredTriangleList.Program,
+            left: Float,
+            top: Float,
+            width: Float,
+            height: Float
+        ) = Gles2ColoredTriangleList(
+            program,
+            floatArrayOf(
+                top + 0.0f,
+                left + 0.0f,
+                0.0f,
+
+                top + 0.0f,
+                left + width,
+                0.0f,
+
+                top + height,
+                left + 0.0f,
+                0.0f,
+
+                top + 0.0f,
+                left + width,
+                0.0f,
+
+                top + height,
+                left + 0.0f,
+                0.0f,
+
+                top + height,
+                left + width,
+                0.0f
+            ),
             floatArrayOf(
                 1.0f /* red */,
                 1.0f /* green */,
                 1.0f /* blue */,
-                1.0f /* alpha */
+                0.0f /* alpha */
             )
         )
-    }
 
-    /**
-     * Creates a triangle list for the minor ticks on the watch face.
-     *
-     * @param program program for drawing triangles
-     */
-    private fun createMinorTicks(
-        program: Gles2ColoredTriangleList.Program
-    ): Gles2ColoredTriangleList {
-        // Create the data for the VBO.
-        val trianglesCoords = FloatArray(9 * (12 - 4))
-        var index = 0
-        for (i in 0..11) {
-            if (i % 3 == 0) {
-                // This is where a major tick goes, so skip it.
-                continue
+        private fun getMajorTickTriangleCoords(index: Int): FloatArray {
+            return getTickTriangleCoords(
+                0.03f /* width */, 0.09f /* length */,
+                index * 360 / 4 /* angleDegrees */
+            )
+        }
+
+        private fun getMinorTickTriangleCoords(index: Int): FloatArray {
+            return getTickTriangleCoords(
+                0.02f /* width */, 0.06f /* length */,
+                index * 360 / 12 /* angleDegrees */
+            )
+        }
+
+        private fun getTickTriangleCoords(
+            width: Float,
+            length: Float,
+            angleDegrees: Int
+        ): FloatArray {
+            // Create the data for the VBO.
+            val coords = floatArrayOf(
+                0f,
+                1f,
+                0f,
+                width / 2,
+                length + 1,
+                0f,
+                -width / 2,
+                length + 1,
+                0f
+            )
+            rotateCoords(coords, angleDegrees)
+            return coords
+        }
+
+        /**
+         * Destructively rotates the given coordinates in the XY plane about the origin by the given
+         * angle.
+         *
+         * @param coords flattened 3D coordinates
+         * @param angleDegrees angle in degrees clockwise when viewed from negative infinity on the
+         * Z axis
+         */
+        private fun rotateCoords(
+            coords: FloatArray,
+            angleDegrees: Int
+        ) {
+            val angleRadians = Math.toRadians(angleDegrees.toDouble())
+            val cos = cos(angleRadians)
+            val sin = sin(angleRadians)
+            var i = 0
+            while (i < coords.size) {
+                val x = coords[i]
+                val y = coords[i + 1]
+                coords[i] = (cos * x - sin * y).toFloat()
+                coords[i + 1] = (sin * x + cos * y).toFloat()
+                i += 3
             }
-            val triangleCoords = getMinorTickTriangleCoords(i)
-            System.arraycopy(
-                triangleCoords,
-                0,
-                trianglesCoords,
-                index,
-                triangleCoords.size
-            )
-            index += 9
         }
-        return Gles2ColoredTriangleList(
-            program, trianglesCoords,
-            floatArrayOf(
-                0.5f /* red */,
-                0.5f /* green */,
-                0.5f /* blue */,
-                1.0f /* alpha */
-            )
-        )
-    }
 
-    /**
-     * Creates a triangle list for the complication.
-     */
-    private fun createComplicationQuad(
-        program: Gles2TexturedTriangleList.Program,
-        left: Float,
-        top: Float,
-        width: Float,
-        height: Float
-    ) = Gles2TexturedTriangleList(
-        program,
-        floatArrayOf(
-            top + 0.0f,
-            left + 0.0f,
-            0.0f,
-
-            top + 0.0f,
-            left + width,
-            0.0f,
-
-            top + height,
-            left + 0.0f,
-            0.0f,
-
-            top + 0.0f,
-            left + width,
-            0.0f,
-
-            top + height,
-            left + 0.0f,
-            0.0f,
-
-            top + height,
-            left + width,
-            0.0f
-        ),
-        floatArrayOf(
-            1.0f,
-            1.0f,
-
-            1.0f,
-            0.0f,
-
-            0.0f,
-            1.0f,
-
-            1.0f,
-            0.0f,
-
-            0.0f,
-            1.0f,
-
-            0.0f,
-            0.0f
-        )
-    )
-
-    /**
-     * Creates a triangle list for the complication highlight quad.
-     */
-    private fun createComplicationHighlightQuad(
-        program: Gles2ColoredTriangleList.Program,
-        left: Float,
-        top: Float,
-        width: Float,
-        height: Float
-    ) = Gles2ColoredTriangleList(
-        program,
-        floatArrayOf(
-            top + 0.0f,
-            left + 0.0f,
-            0.0f,
-
-            top + 0.0f,
-            left + width,
-            0.0f,
-
-            top + height,
-            left + 0.0f,
-            0.0f,
-
-            top + 0.0f,
-            left + width,
-            0.0f,
-
-            top + height,
-            left + 0.0f,
-            0.0f,
-
-            top + height,
-            left + width,
-            0.0f
-        ),
-        floatArrayOf(
-            1.0f /* red */,
-            1.0f /* green */,
-            1.0f /* blue */,
-            0.0f /* alpha */
-        )
-    )
-
-    private fun getMajorTickTriangleCoords(index: Int): FloatArray {
-        return getTickTriangleCoords(
-            0.03f /* width */, 0.09f /* length */,
-            index * 360 / 4 /* angleDegrees */
-        )
-    }
-
-    private fun getMinorTickTriangleCoords(index: Int): FloatArray {
-        return getTickTriangleCoords(
-            0.02f /* width */, 0.06f /* length */,
-            index * 360 / 12 /* angleDegrees */
-        )
-    }
-
-    private fun getTickTriangleCoords(
-        width: Float,
-        length: Float,
-        angleDegrees: Int
-    ): FloatArray {
-        // Create the data for the VBO.
-        val coords = floatArrayOf(
-            0f,
-            1f,
-            0f,
-            width / 2,
-            length + 1,
-            0f,
-            -width / 2,
-            length + 1,
-            0f
-        )
-        rotateCoords(coords, angleDegrees)
-        return coords
-    }
-
-    /**
-     * Destructively rotates the given coordinates in the XY plane about the origin by the given
-     * angle.
-     *
-     * @param coords flattened 3D coordinates
-     * @param angleDegrees angle in degrees clockwise when viewed from negative infinity on the
-     * Z axis
-     */
-    private fun rotateCoords(
-        coords: FloatArray,
-        angleDegrees: Int
-    ) {
-        val angleRadians = Math.toRadians(angleDegrees.toDouble())
-        val cos = cos(angleRadians)
-        val sin = sin(angleRadians)
-        var i = 0
-        while (i < coords.size) {
-            val x = coords[i]
-            val y = coords[i + 1]
-            coords[i] = (cos * x - sin * y).toFloat()
-            coords[i + 1] = (sin * x + cos * y).toFloat()
-            i += 3
-        }
-    }
-
-    override fun render(zonedDateTime: ZonedDateTime) {
-        // Draw background color and select the appropriate view projection matrix. The background
-        // should always be black in ambient mode. The view projection matrix used is overhead in
-        // ambient. In interactive mode, it's tilted depending on the current time.
-        val vpMatrix = if (renderParameters.drawMode == DrawMode.AMBIENT) {
-            GLES20.glClearColor(0f, 0f, 0f, 1f)
-            ambientVpMatrix
-        } else {
-            when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()) {
-                "red_style" -> GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1f)
-                "green_style" -> GLES20.glClearColor(0.2f, 0.5f, 0.2f, 1f)
+        override fun render(zonedDateTime: ZonedDateTime) {
+            // Draw background color and select the appropriate view projection matrix. The background
+            // should always be black in ambient mode. The view projection matrix used is overhead in
+            // ambient. In interactive mode, it's tilted depending on the current time.
+            val vpMatrix = if (renderParameters.drawMode == DrawMode.AMBIENT) {
+                GLES20.glClearColor(0f, 0f, 0f, 1f)
+                ambientVpMatrix
+            } else {
+                when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()) {
+                    "red_style" -> GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1f)
+                    "green_style" -> GLES20.glClearColor(0.2f, 0.5f, 0.2f, 1f)
+                }
+                val cameraIndex =
+                    (zonedDateTime.toInstant().toEpochMilli() / FRAME_PERIOD_MS % numCameraAngles)
+                        .toInt()
+                vpMatrices[cameraIndex]
             }
-            val cameraIndex =
-                (zonedDateTime.toInstant().toEpochMilli() / FRAME_PERIOD_MS % numCameraAngles)
-                    .toInt()
-            vpMatrices[cameraIndex]
-        }
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
 
-        // Draw the complication first.
-        if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
-            complicationTexture.renderToTexture(zonedDateTime, renderParameters)
+            // Draw the complication first.
+            if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
+                complicationTexture.renderToTexture(zonedDateTime, renderParameters)
 
-            textureTriangleProgram.bindProgramAndAttribs()
-            complicationTexture.bind()
+                textureTriangleProgram.bindProgramAndAttribs()
+                complicationTexture.bind()
 
-            GLES20.glEnable(GLES20.GL_BLEND)
-            GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE)
-            complicationTriangles.draw(vpMatrix)
-            textureTriangleProgram.unbindAttribs()
+                GLES20.glEnable(GLES20.GL_BLEND)
+                GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE)
+                complicationTriangles.draw(vpMatrix)
+                textureTriangleProgram.unbindAttribs()
 
-            GLES20.glDisable(GLES20.GL_BLEND)
-            GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
-            coloredTriangleProgram.bindProgramAndAttribs()
-        }
+                GLES20.glDisable(GLES20.GL_BLEND)
+                GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
+                coloredTriangleProgram.bindProgramAndAttribs()
+            }
 
-        // Compute angle indices for the three hands.
-        val seconds = zonedDateTime.second + (zonedDateTime.nano / 1000000000.0)
-        val minutes = zonedDateTime.minute + seconds / 60f
-        val hours = (zonedDateTime.hour % 12) + minutes / 60f
-        val secIndex = (seconds / 60f * 360f).toInt()
-        val minIndex = (minutes / 60f * 360f).toInt()
-        val hoursIndex = (hours / 12f * 360f).toInt()
+            // Compute angle indices for the three hands.
+            val seconds = zonedDateTime.second + (zonedDateTime.nano / 1000000000.0)
+            val minutes = zonedDateTime.minute + seconds / 60f
+            val hours = (zonedDateTime.hour % 12) + minutes / 60f
+            val secIndex = (seconds / 60f * 360f).toInt()
+            val minIndex = (minutes / 60f * 360f).toInt()
+            val hoursIndex = (hours / 12f * 360f).toInt()
 
-        // Render hands.
-        if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)) {
-            Matrix.multiplyMM(
-                mvpMatrix,
-                0,
-                vpMatrix,
-                0,
-                modelMatrices[hoursIndex],
-                0
-            )
-            hourHandTriangle.draw(mvpMatrix)
-
-            Matrix.multiplyMM(
-                mvpMatrix,
-                0,
-                vpMatrix,
-                0,
-                modelMatrices[minIndex],
-                0
-            )
-            minuteHandTriangle.draw(mvpMatrix)
-
-            if (renderParameters.drawMode != DrawMode.AMBIENT) {
+            // Render hands.
+            if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)) {
                 Matrix.multiplyMM(
                     mvpMatrix,
                     0,
                     vpMatrix,
                     0,
-                    modelMatrices[secIndex],
+                    modelMatrices[hoursIndex],
                     0
                 )
-                secondHandTriangleMap[
-                    currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()
-                ]?.draw(mvpMatrix)
-            }
-        }
+                hourHandTriangle.draw(mvpMatrix)
 
-        if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
-            majorTickTriangles.draw(vpMatrix)
-            minorTickTriangles.draw(vpMatrix)
-            coloredTriangleProgram.unbindAttribs()
-        }
-    }
-
-    override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {
-        val cameraIndex =
-            (zonedDateTime.toInstant().toEpochMilli() / FRAME_PERIOD_MS % numCameraAngles).toInt()
-        val vpMatrix = vpMatrices[cameraIndex]
-
-        val highlightLayer = renderParameters.highlightLayer!!
-        GLES20.glClearColor(
-            Color.red(highlightLayer.backgroundTint).toFloat() / 256.0f,
-            Color.green(highlightLayer.backgroundTint).toFloat() / 256.0f,
-            Color.blue(highlightLayer.backgroundTint).toFloat() / 256.0f,
-            Color.alpha(highlightLayer.backgroundTint).toFloat() / 256.0f
-        )
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
-
-        if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
-            GLES20.glEnable(GLES20.GL_BLEND)
-            GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
-            complicationHighlightTriangles.draw(vpMatrix)
-            coloredTriangleProgram.unbindAttribs()
-        }
-    }
-}
-
-/**
- * A list of triangles drawn in a single solid color using OpenGL ES 2.0.
- */
-class Gles2ColoredTriangleList(
-    private val program: Program,
-    triangleCoords: FloatArray,
-    private val color: FloatArray
-) {
-    init {
-        require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
-            ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
-        }
-        require(color.size == NUM_COLOR_COMPONENTS) { "wrong number of color components" }
-    }
-
-    /** The VBO containing the vertex coordinates.  */
-    private val vertexBuffer =
-        ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
-            .apply { order(ByteOrder.nativeOrder()) }
-            .asFloatBuffer().apply {
-                put(triangleCoords)
-                position(0)
-            }
-
-    /** Number of coordinates in this triangle list.  */
-    private val numCoords = triangleCoords.size / COORDS_PER_VERTEX
-
-    /**
-     * Draws this triangle list using OpenGL commands.
-     *
-     * @param mvpMatrix the Model View Project matrix to draw this triangle list
-     */
-    fun draw(mvpMatrix: FloatArray?) {
-        // Pass the MVP matrix, vertex data, and color to OpenGL.
-        program.bind(mvpMatrix, vertexBuffer, color)
-
-        // Draw the triangle list.
-        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
-        if (CHECK_GL_ERRORS) checkGlError(
-            "glDrawArrays"
-        )
-    }
-
-    /** OpenGL shaders for drawing solid colored triangle lists.  */
-    class Program {
-        /** ID OpenGL uses to identify this program.  */
-        private val programId: Int
-
-        /** Handle for uMvpMatrix uniform in vertex shader.  */
-        private val mvpMatrixHandle: Int
-
-        /** Handle for aPosition attribute in vertex shader.  */
-        private val positionHandle: Int
-
-        /** Handle for uColor uniform in fragment shader.  */
-        private val colorHandle: Int
-
-        companion object {
-            /** Trivial vertex shader that transforms the input vertex by the MVP matrix.  */
-            private const val VERTEX_SHADER_CODE = "" +
-                "uniform mat4 uMvpMatrix;\n" +
-                "attribute vec4 aPosition;\n" +
-                "void main() {\n" +
-                "    gl_Position = uMvpMatrix * aPosition;\n" +
-                "}\n"
-
-            /** Trivial fragment shader that draws with a fixed color.  */
-            private const val FRAGMENT_SHADER_CODE = "" +
-                "precision mediump float;\n" +
-                "uniform vec4 uColor;\n" +
-                "void main() {\n" +
-                "    gl_FragColor = uColor;\n" +
-                "}\n"
-        }
-
-        /**
-         * Tells OpenGL to use this program. Call this method before drawing a sequence of
-         * triangle lists.
-         */
-        fun bindProgramAndAttribs() {
-            GLES20.glUseProgram(programId)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glUseProgram")
-            }
-
-            // Enable vertex array (VBO).
-            GLES20.glEnableVertexAttribArray(positionHandle)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glEnableVertexAttribArray")
-            }
-        }
-
-        fun unbindAttribs() {
-            GLES20.glDisableVertexAttribArray(positionHandle)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glDisableVertexAttribArray")
-            }
-        }
-
-        /** Sends the given MVP matrix, vertex data, and color to OpenGL.  */
-        fun bind(
-            mvpMatrix: FloatArray?,
-            vertexBuffer: FloatBuffer?,
-            color: FloatArray?
-        ) {
-            // Pass the MVP matrix to OpenGL.
-            GLES20.glUniformMatrix4fv(
-                mvpMatrixHandle, 1 /* count */, false /* transpose */,
-                mvpMatrix, 0 /* offset */
-            )
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glUniformMatrix4fv")
-            }
-
-            // Pass the VBO with the triangle list's vertices to OpenGL.
-            GLES20.glEnableVertexAttribArray(positionHandle)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glEnableVertexAttribArray")
-            }
-            GLES20.glVertexAttribPointer(
-                positionHandle,
-                COORDS_PER_VERTEX,
-                GLES20.GL_FLOAT,
-                false /* normalized */,
-                VERTEX_STRIDE,
-                vertexBuffer
-            )
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glVertexAttribPointer")
-            }
-
-            // Pass the triangle list's color to OpenGL.
-            GLES20.glUniform4fv(colorHandle, 1 /* count */, color, 0 /* offset */)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glUniform4fv")
-            }
-        }
-
-        /**
-         * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
-         * should be used for all triangle lists being drawn.
-         */
-        init {
-            // Prepare shaders.
-            val vertexShader = loadShader(
-                GLES20.GL_VERTEX_SHADER,
-                VERTEX_SHADER_CODE
-            )
-            val fragmentShader = loadShader(
-                GLES20.GL_FRAGMENT_SHADER,
-                FRAGMENT_SHADER_CODE
-            )
-
-            // Create empty OpenGL Program.
-            programId = GLES20.glCreateProgram()
-            if (CHECK_GL_ERRORS) checkGlError(
-                "glCreateProgram"
-            )
-            check(programId != 0) { "glCreateProgram failed" }
-
-            // Add the shaders to the program.
-            GLES20.glAttachShader(programId, vertexShader)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glAttachShader")
-            }
-            GLES20.glAttachShader(programId, fragmentShader)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glAttachShader")
-            }
-
-            // Link the program so it can be executed.
-            GLES20.glLinkProgram(programId)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glLinkProgram")
-            }
-
-            // Get a handle to the uMvpMatrix uniform in the vertex shader.
-            mvpMatrixHandle = GLES20.glGetUniformLocation(programId, "uMvpMatrix")
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glGetUniformLocation")
-            }
-
-            // Get a handle to the vertex shader's aPosition attribute.
-            positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glGetAttribLocation")
-            }
-
-            // Get a handle to fragment shader's uColor uniform.
-            colorHandle = GLES20.glGetUniformLocation(programId, "uColor")
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glGetUniformLocation")
-            }
-        }
-    }
-
-    companion object {
-        private const val TAG = "GlColoredTriangleList"
-
-        /** Whether to check for GL errors. This is slow, so not appropriate for production builds.  */
-        private const val CHECK_GL_ERRORS = false
-
-        /** Number of coordinates per vertex in this array: one for each of x, y, and z.  */
-        private const val COORDS_PER_VERTEX = 3
-
-        /** Number of bytes to store a float in GL.  */
-        const val BYTES_PER_FLOAT = 4
-
-        /** Number of bytes per vertex.  */
-        private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT
-
-        /** Triangles have three vertices.  */
-        private const val VERTICES_PER_TRIANGLE = 3
-
-        /**
-         * Number of components in an OpenGL color. The components are:
-         *  1. red
-         *  1. green
-         *  1. blue
-         *  1. alpha
-         *
-         */
-        private const val NUM_COLOR_COMPONENTS = 4
-
-        /**
-         * Checks if any of the GL calls since the last time this method was called set an error
-         * condition. Call this method immediately after calling a GL method. Pass the name of the GL
-         * operation. For example:
-         *
-         * <pre>
-         * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
-         * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
-         *
-         * If the operation is not successful, the check throws an exception.
-         *
-         *
-         * *Note* This is quite slow so it's best to use it sparingly in production builds.
-         *
-         * @param glOperation name of the OpenGL call to check
-         */
-        internal fun checkGlError(glOperation: String) {
-            val error = GLES20.glGetError()
-            if (error != GLES20.GL_NO_ERROR) {
-                var errorString = GLU.gluErrorString(error)
-                if (errorString == null) {
-                    errorString = GLUtils.getEGLErrorString(error)
-                }
-                val message =
-                    glOperation + " caused GL error 0x" + Integer.toHexString(error) +
-                        ": " + errorString
-                Log.e(TAG, message)
-                throw RuntimeException(message)
-            }
-        }
-
-        /**
-         * Compiles an OpenGL shader.
-         *
-         * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
-         * @param shaderCode string containing the shader source code
-         * @return ID for the shader
-         */
-        internal fun loadShader(type: Int, shaderCode: String): Int {
-            // Create a vertex or fragment shader.
-            val shader = GLES20.glCreateShader(type)
-            if (CHECK_GL_ERRORS) checkGlError(
-                "glCreateShader"
-            )
-            check(shader != 0) { "glCreateShader failed" }
-
-            // Add the source code to the shader and compile it.
-            GLES20.glShaderSource(shader, shaderCode)
-            if (CHECK_GL_ERRORS) checkGlError(
-                "glShaderSource"
-            )
-            GLES20.glCompileShader(shader)
-            if (CHECK_GL_ERRORS) checkGlError(
-                "glCompileShader"
-            )
-            return shader
-        }
-    }
-}
-
-/**
- * A list of triangles drawn with a texture using OpenGL ES 2.0.
- */
-class Gles2TexturedTriangleList(
-    private val program: Program,
-    triangleCoords: FloatArray,
-    private val textureCoords: FloatArray
-) {
-    init {
-        require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
-            ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
-        }
-        require(textureCoords.size % (VERTICES_PER_TRIANGLE * TEXTURE_COORDS_PER_VERTEX) == 0) {
-            (
-                "must be multiple of VERTICES_PER_TRIANGLE * NUM_TEXTURE_COMPONENTS texture " +
-                    "coordinates"
+                Matrix.multiplyMM(
+                    mvpMatrix,
+                    0,
+                    vpMatrix,
+                    0,
+                    modelMatrices[minIndex],
+                    0
                 )
-        }
-    }
+                minuteHandTriangle.draw(mvpMatrix)
 
-    /** The VBO containing the vertex coordinates. */
-    private val vertexBuffer =
-        ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
-            .apply { order(ByteOrder.nativeOrder()) }
-            .asFloatBuffer().apply {
-                put(triangleCoords)
-                position(0)
+                if (renderParameters.drawMode != DrawMode.AMBIENT) {
+                    Matrix.multiplyMM(
+                        mvpMatrix,
+                        0,
+                        vpMatrix,
+                        0,
+                        modelMatrices[secIndex],
+                        0
+                    )
+                    secondHandTriangleMap[
+                        currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()
+                    ]?.draw(mvpMatrix)
+                }
             }
 
-    /** The VBO containing the vertex coordinates. */
-    private val textureCoordsBuffer =
-        ByteBuffer.allocateDirect(textureCoords.size * BYTES_PER_FLOAT)
-            .apply { order(ByteOrder.nativeOrder()) }
-            .asFloatBuffer().apply {
-                put(textureCoords)
-                position(0)
-            }
-
-    /** Number of coordinates in this triangle list.  */
-    private val numCoords = triangleCoords.size / COORDS_PER_VERTEX
-
-    /**
-     * Draws this triangle list using OpenGL commands.
-     *
-     * @param mvpMatrix the Model View Project matrix to draw this triangle list
-     */
-    fun draw(mvpMatrix: FloatArray?) {
-        // Pass the MVP matrix, vertex data, and color to OpenGL.
-        program.bind(mvpMatrix, vertexBuffer, textureCoordsBuffer)
-
-        // Draw the triangle list.
-        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
-        if (CHECK_GL_ERRORS) checkGlError(
-            "glDrawArrays"
-        )
-    }
-
-    /** OpenGL shaders for drawing textured triangle lists.  */
-    class Program {
-        /** ID OpenGL uses to identify this program.  */
-        private val programId: Int
-
-        /** Handle for uMvpMatrix uniform in vertex shader.  */
-        private val mvpMatrixHandle: Int
-
-        /** Handle for aPosition attribute in vertex shader.  */
-        private val positionHandle: Int
-
-        /** Handle for aTextureCoordinate uniform in fragment shader.  */
-        private val textureCoordinateHandle: Int
-
-        companion object {
-            /** Trivial vertex shader that transforms the input vertex by the MVP matrix.  */
-            private const val VERTEX_SHADER_CODE = "" +
-                "uniform mat4 uMvpMatrix;\n" +
-                "attribute vec4 aPosition;\n" +
-                "attribute vec4 aTextureCoordinate;\n" +
-                "varying vec2 textureCoordinate;\n" +
-                "void main() {\n" +
-                "    gl_Position = uMvpMatrix * aPosition;\n" +
-                "    textureCoordinate = aTextureCoordinate.xy;" +
-                "}\n"
-
-            /** Trivial fragment shader that draws with a texture.  */
-            private const val FRAGMENT_SHADER_CODE = "" +
-                "varying highp vec2 textureCoordinate;\n" +
-                "uniform sampler2D texture;\n" +
-                "void main() {\n" +
-                "    gl_FragColor = texture2D(texture, textureCoordinate);\n" +
-                "}\n"
-        }
-
-        /**
-         * Tells OpenGL to use this program. Call this method before drawing a sequence of
-         * triangle lists.
-         */
-        fun bindProgramAndAttribs() {
-            GLES20.glUseProgram(programId)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glUseProgram")
-            }
-
-            // Enable vertex array (VBO).
-            GLES20.glEnableVertexAttribArray(positionHandle)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glEnableVertexAttribArray")
-            }
-
-            GLES20.glEnableVertexAttribArray(textureCoordinateHandle)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glEnableVertexAttribArray")
+            if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
+                majorTickTriangles.draw(vpMatrix)
+                minorTickTriangles.draw(vpMatrix)
+                coloredTriangleProgram.unbindAttribs()
             }
         }
 
-        fun unbindAttribs() {
-            GLES20.glDisableVertexAttribArray(positionHandle)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glDisableVertexAttribArray")
-            }
+        override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {
+            val cameraIndex =
+                (zonedDateTime.toInstant()
+                    .toEpochMilli() / FRAME_PERIOD_MS % numCameraAngles).toInt()
+            val vpMatrix = vpMatrices[cameraIndex]
 
-            GLES20.glDisableVertexAttribArray(textureCoordinateHandle)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glDisableVertexAttribArray")
-            }
-        }
-
-        fun onDestroy() {
-            GLES20.glDeleteProgram(programId)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glDeleteProgram")
-            }
-        }
-
-        /** Sends the given MVP matrix, vertex data, and color to OpenGL.  */
-        fun bind(
-            mvpMatrix: FloatArray?,
-            vertexBuffer: FloatBuffer?,
-            textureCoordinatesBuffer: FloatBuffer?
-        ) {
-            // Pass the MVP matrix to OpenGL.
-            GLES20.glUniformMatrix4fv(
-                mvpMatrixHandle, 1 /* count */, false /* transpose */,
-                mvpMatrix, 0 /* offset */
+            val highlightLayer = renderParameters.highlightLayer!!
+            GLES20.glClearColor(
+                Color.red(highlightLayer.backgroundTint).toFloat() / 256.0f,
+                Color.green(highlightLayer.backgroundTint).toFloat() / 256.0f,
+                Color.blue(highlightLayer.backgroundTint).toFloat() / 256.0f,
+                Color.alpha(highlightLayer.backgroundTint).toFloat() / 256.0f
             )
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glUniformMatrix4fv")
-            }
+            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
 
-            // Pass the VBO with the triangle list's vertices to OpenGL.
-            GLES20.glVertexAttribPointer(
-                positionHandle,
-                COORDS_PER_VERTEX,
-                GLES20.GL_FLOAT,
-                false /* normalized */,
-                VERTEX_STRIDE,
-                vertexBuffer
-            )
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glVertexAttribPointer")
-            }
-
-            // Pass the VBO with the triangle list's texture coordinates to OpenGL.
-            GLES20.glVertexAttribPointer(
-                textureCoordinateHandle,
-                TEXTURE_COORDS_PER_VERTEX,
-                GLES20.GL_FLOAT,
-                false /* normalized */,
-                TEXTURE_COORDS_VERTEX_STRIDE,
-                textureCoordinatesBuffer
-            )
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glVertexAttribPointer")
-            }
-        }
-
-        /**
-         * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
-         * should be used for all triangle lists being drawn.
-         */
-        init {
-            // Prepare shaders.
-            val vertexShader = loadShader(
-                GLES20.GL_VERTEX_SHADER,
-                VERTEX_SHADER_CODE
-            )
-            val fragmentShader = loadShader(
-                GLES20.GL_FRAGMENT_SHADER,
-                FRAGMENT_SHADER_CODE
-            )
-
-            // Create empty OpenGL Program.
-            programId = GLES20.glCreateProgram()
-            if (CHECK_GL_ERRORS) checkGlError(
-                "glCreateProgram"
-            )
-            check(programId != 0) { "glCreateProgram failed" }
-
-            // Add the shaders to the program.
-            GLES20.glAttachShader(programId, vertexShader)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glAttachShader")
-            }
-            GLES20.glAttachShader(programId, fragmentShader)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glAttachShader")
-            }
-
-            // Link the program so it can be executed.
-            GLES20.glLinkProgram(programId)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glLinkProgram")
-            }
-
-            // Get a handle to the uMvpMatrix uniform in the vertex shader.
-            mvpMatrixHandle = GLES20.glGetUniformLocation(programId, "uMvpMatrix")
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glGetUniformLocation")
-            }
-
-            // Get a handle to the vertex shader's aPosition attribute.
-            positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glGetAttribLocation")
-            }
-
-            // Get a handle to vertex shader's aUV attribute.
-            textureCoordinateHandle =
-                GLES20.glGetAttribLocation(programId, "aTextureCoordinate")
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glGetAttribLocation")
-            }
-
-            // Enable vertex array (VBO).
-            GLES20.glEnableVertexAttribArray(positionHandle)
-            if (CHECK_GL_ERRORS) {
-                checkGlError("glEnableVertexAttribArray")
+            if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
+                GLES20.glEnable(GLES20.GL_BLEND)
+                GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
+                complicationHighlightTriangles.draw(vpMatrix)
+                coloredTriangleProgram.unbindAttribs()
             }
         }
     }
 
     companion object {
-        private const val TAG = "Gles2TexturedTriangleList"
+        /** Expected frame rate in interactive mode.  */
+        private const val FPS: Long = 60
 
-        /**
-         * Whether to check for GL errors. This is slow, so not appropriate for production builds.
-         */
-        private const val CHECK_GL_ERRORS = false
+        /** Z distance from the camera to the watchface.  */
+        private const val EYE_Z = -2.3f
 
-        /** Number of coordinates per vertex in this array: one for each of x, y, and z.  */
-        private const val COORDS_PER_VERTEX = 3
+        /** How long each frame is displayed at expected frame rate.  */
+        private const val FRAME_PERIOD_MS: Long = 1000 / FPS
 
-        /** Number of texture coordinates per vertex in this array: one for u & v */
-        private const val TEXTURE_COORDS_PER_VERTEX = 2
+        /** Cycle time before the camera motion repeats.  */
+        private const val CYCLE_PERIOD_SECONDS: Long = 5
 
-        /** Number of bytes to store a float in GL.  */
-        const val BYTES_PER_FLOAT = 4
+        /** Number of camera angles to precompute. */
+        private const val numCameraAngles = (CYCLE_PERIOD_SECONDS * FPS).toInt()
 
-        /** Number of bytes per vertex.  */
-        private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT
-
-        /** Number of bytes per vertex for texture coords.  */
-        private const val TEXTURE_COORDS_VERTEX_STRIDE = TEXTURE_COORDS_PER_VERTEX * BYTES_PER_FLOAT
-
-        /** Triangles have three vertices. */
-        private const val VERTICES_PER_TRIANGLE = 3
-
-        /**
-         * Checks if any of the GL calls since the last time this method was called set an error
-         * condition. Call this method immediately after calling a GL method. Pass the name of the
-         * GL operation. For example:
-         *
-         * <pre>
-         * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
-         * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
-         *
-         * If the operation is not successful, the check throws an exception.
-         *
-         *
-         * *Note* This is quite slow so it's best to use it sparingly in production builds.
-         *
-         * @param glOperation name of the OpenGL call to check
-         */
-        internal fun checkGlError(glOperation: String) {
-            val error = GLES20.glGetError()
-            if (error != GLES20.GL_NO_ERROR) {
-                var errorString = GLU.gluErrorString(error)
-                if (errorString == null) {
-                    errorString = GLUtils.getEGLErrorString(error)
-                }
-                val message =
-                    glOperation + " caused GL error 0x" + Integer.toHexString(error) +
-                        ": " + errorString
-                Log.e(TAG, message)
-                throw RuntimeException(message)
-            }
-        }
-
-        /**
-         * Compiles an OpenGL shader.
-         *
-         * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
-         * @param shaderCode string containing the shader source code
-         * @return ID for the shader
-         */
-        internal fun loadShader(type: Int, shaderCode: String): Int {
-            // Create a vertex or fragment shader.
-            val shader = GLES20.glCreateShader(type)
-            if (CHECK_GL_ERRORS) checkGlError(
-                "glCreateShader"
-            )
-            check(shader != 0) { "glCreateShader failed" }
-
-            // Add the source code to the shader and compile it.
-            GLES20.glShaderSource(shader, shaderCode)
-            if (CHECK_GL_ERRORS) checkGlError(
-                "glShaderSource"
-            )
-            GLES20.glCompileShader(shader)
-            if (CHECK_GL_ERRORS) checkGlError(
-                "glCompileShader"
-            )
-            return shader
-        }
+        const val EXAMPLE_OPENGL_COMPLICATION_ID = 101
     }
-}
\ No newline at end of file
+}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2ColoredTriangleList.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2ColoredTriangleList.kt
new file mode 100644
index 0000000..21dcca8
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2ColoredTriangleList.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.samples
+
+import android.opengl.GLES20
+import android.opengl.GLU
+import android.opengl.GLUtils
+import android.util.Log
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+
+/**
+ * A list of triangles drawn in a single solid color using OpenGL ES 2.0.
+ */
+internal class Gles2ColoredTriangleList(
+    private val program: Program,
+    triangleCoords: FloatArray,
+    private val color: FloatArray
+) {
+    init {
+        require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
+            ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
+        }
+        require(color.size == NUM_COLOR_COMPONENTS) { "wrong number of color components" }
+    }
+
+    /** The VBO containing the vertex coordinates.  */
+    private val vertexBuffer =
+        ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
+            .apply { order(ByteOrder.nativeOrder()) }
+            .asFloatBuffer().apply {
+                put(triangleCoords)
+                position(0)
+            }
+
+    /** Number of coordinates in this triangle list.  */
+    private val numCoords = triangleCoords.size / COORDS_PER_VERTEX
+
+    /**
+     * Draws this triangle list using OpenGL commands.
+     *
+     * @param mvpMatrix the Model View Project matrix to draw this triangle list
+     */
+    fun draw(mvpMatrix: FloatArray?) {
+        // Pass the MVP matrix, vertex data, and color to OpenGL.
+        program.bind(mvpMatrix, vertexBuffer, color)
+
+        // Draw the triangle list.
+        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
+        if (CHECK_GL_ERRORS) checkGlError(
+            "glDrawArrays"
+        )
+    }
+
+    /** OpenGL shaders for drawing solid colored triangle lists.  */
+    class Program {
+        /** ID OpenGL uses to identify this program.  */
+        private val programId: Int
+
+        /** Handle for uMvpMatrix uniform in vertex shader.  */
+        private val mvpMatrixHandle: Int
+
+        /** Handle for aPosition attribute in vertex shader.  */
+        private val positionHandle: Int
+
+        /** Handle for uColor uniform in fragment shader.  */
+        private val colorHandle: Int
+
+        companion object {
+            /** Trivial vertex shader that transforms the input vertex by the MVP matrix.  */
+            private const val VERTEX_SHADER_CODE = "" +
+                "uniform mat4 uMvpMatrix;\n" +
+                "attribute vec4 aPosition;\n" +
+                "void main() {\n" +
+                "    gl_Position = uMvpMatrix * aPosition;\n" +
+                "}\n"
+
+            /** Trivial fragment shader that draws with a fixed color.  */
+            private const val FRAGMENT_SHADER_CODE = "" +
+                "precision mediump float;\n" +
+                "uniform vec4 uColor;\n" +
+                "void main() {\n" +
+                "    gl_FragColor = uColor;\n" +
+                "}\n"
+        }
+
+        /**
+         * Tells OpenGL to use this program. Call this method before drawing a sequence of
+         * triangle lists.
+         */
+        fun bindProgramAndAttribs() {
+            GLES20.glUseProgram(programId)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glUseProgram")
+            }
+
+            // Enable vertex array (VBO).
+            GLES20.glEnableVertexAttribArray(positionHandle)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glEnableVertexAttribArray")
+            }
+        }
+
+        fun unbindAttribs() {
+            GLES20.glDisableVertexAttribArray(positionHandle)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glDisableVertexAttribArray")
+            }
+        }
+
+        /** Sends the given MVP matrix, vertex data, and color to OpenGL.  */
+        fun bind(
+            mvpMatrix: FloatArray?,
+            vertexBuffer: FloatBuffer?,
+            color: FloatArray?
+        ) {
+            // Pass the MVP matrix to OpenGL.
+            GLES20.glUniformMatrix4fv(
+                mvpMatrixHandle, 1 /* count */, false /* transpose */,
+                mvpMatrix, 0 /* offset */
+            )
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glUniformMatrix4fv")
+            }
+
+            // Pass the VBO with the triangle list's vertices to OpenGL.
+            GLES20.glEnableVertexAttribArray(positionHandle)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glEnableVertexAttribArray")
+            }
+            GLES20.glVertexAttribPointer(
+                positionHandle,
+                COORDS_PER_VERTEX,
+                GLES20.GL_FLOAT,
+                false /* normalized */,
+                VERTEX_STRIDE,
+                vertexBuffer
+            )
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glVertexAttribPointer")
+            }
+
+            // Pass the triangle list's color to OpenGL.
+            GLES20.glUniform4fv(colorHandle, 1 /* count */, color, 0 /* offset */)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glUniform4fv")
+            }
+        }
+
+        /**
+         * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
+         * should be used for all triangle lists being drawn.
+         */
+        init {
+            // Prepare shaders.
+            val vertexShader = loadShader(
+                GLES20.GL_VERTEX_SHADER,
+                VERTEX_SHADER_CODE
+            )
+            val fragmentShader = loadShader(
+                GLES20.GL_FRAGMENT_SHADER,
+                FRAGMENT_SHADER_CODE
+            )
+
+            // Create empty OpenGL Program.
+            programId = GLES20.glCreateProgram()
+            if (CHECK_GL_ERRORS) checkGlError(
+                "glCreateProgram"
+            )
+            check(programId != 0) { "glCreateProgram failed" }
+
+            // Add the shaders to the program.
+            GLES20.glAttachShader(programId, vertexShader)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glAttachShader")
+            }
+            GLES20.glAttachShader(programId, fragmentShader)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glAttachShader")
+            }
+
+            // Link the program so it can be executed.
+            GLES20.glLinkProgram(programId)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glLinkProgram")
+            }
+
+            // Get a handle to the uMvpMatrix uniform in the vertex shader.
+            mvpMatrixHandle = GLES20.glGetUniformLocation(programId, "uMvpMatrix")
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glGetUniformLocation")
+            }
+
+            // Get a handle to the vertex shader's aPosition attribute.
+            positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glGetAttribLocation")
+            }
+
+            // Get a handle to fragment shader's uColor uniform.
+            colorHandle = GLES20.glGetUniformLocation(programId, "uColor")
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glGetUniformLocation")
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "GlColoredTriangleList"
+
+        /** Whether to check for GL errors. This is slow, so not appropriate for production builds.  */
+        private const val CHECK_GL_ERRORS = false
+
+        /** Number of coordinates per vertex in this array: one for each of x, y, and z.  */
+        private const val COORDS_PER_VERTEX = 3
+
+        /** Number of bytes to store a float in GL.  */
+        const val BYTES_PER_FLOAT = 4
+
+        /** Number of bytes per vertex.  */
+        private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT
+
+        /** Triangles have three vertices.  */
+        private const val VERTICES_PER_TRIANGLE = 3
+
+        /**
+         * Number of components in an OpenGL color. The components are:
+         *  1. red
+         *  1. green
+         *  1. blue
+         *  1. alpha
+         *
+         */
+        private const val NUM_COLOR_COMPONENTS = 4
+
+        /**
+         * Checks if any of the GL calls since the last time this method was called set an error
+         * condition. Call this method immediately after calling a GL method. Pass the name of the GL
+         * operation. For example:
+         *
+         * <pre>
+         * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+         * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
+         *
+         * If the operation is not successful, the check throws an exception.
+         *
+         *
+         * *Note* This is quite slow so it's best to use it sparingly in production builds.
+         *
+         * @param glOperation name of the OpenGL call to check
+         */
+        internal fun checkGlError(glOperation: String) {
+            val error = GLES20.glGetError()
+            if (error != GLES20.GL_NO_ERROR) {
+                var errorString = GLU.gluErrorString(error)
+                if (errorString == null) {
+                    errorString = GLUtils.getEGLErrorString(error)
+                }
+                val message =
+                    glOperation + " caused GL error 0x" + Integer.toHexString(error) +
+                        ": " + errorString
+                Log.e(TAG, message)
+                throw RuntimeException(message)
+            }
+        }
+
+        /**
+         * Compiles an OpenGL shader.
+         *
+         * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
+         * @param shaderCode string containing the shader source code
+         * @return ID for the shader
+         */
+        internal fun loadShader(type: Int, shaderCode: String): Int {
+            // Create a vertex or fragment shader.
+            val shader = GLES20.glCreateShader(type)
+            if (CHECK_GL_ERRORS) checkGlError(
+                "glCreateShader"
+            )
+            check(shader != 0) { "glCreateShader failed" }
+
+            // Add the source code to the shader and compile it.
+            GLES20.glShaderSource(shader, shaderCode)
+            if (CHECK_GL_ERRORS) checkGlError(
+                "glShaderSource"
+            )
+            GLES20.glCompileShader(shader)
+            if (CHECK_GL_ERRORS) checkGlError(
+                "glCompileShader"
+            )
+            return shader
+        }
+    }
+}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2TexturedTriangleList.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2TexturedTriangleList.kt
new file mode 100644
index 0000000..0161406
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2TexturedTriangleList.kt
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.samples
+
+import android.opengl.GLES20
+import android.opengl.GLU
+import android.opengl.GLUtils
+import android.util.Log
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+
+/**
+ * A list of triangles drawn with a texture using OpenGL ES 2.0.
+ */
+internal class Gles2TexturedTriangleList(
+    private val program: Program,
+    triangleCoords: FloatArray,
+    private val textureCoords: FloatArray
+) {
+    init {
+        require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
+            ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
+        }
+        require(textureCoords.size % (VERTICES_PER_TRIANGLE * TEXTURE_COORDS_PER_VERTEX) == 0) {
+            (
+                "must be multiple of VERTICES_PER_TRIANGLE * NUM_TEXTURE_COMPONENTS texture " +
+                    "coordinates"
+                )
+        }
+    }
+
+    /** The VBO containing the vertex coordinates. */
+    private val vertexBuffer =
+        ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
+            .apply { order(ByteOrder.nativeOrder()) }
+            .asFloatBuffer().apply {
+                put(triangleCoords)
+                position(0)
+            }
+
+    /** The VBO containing the vertex coordinates. */
+    private val textureCoordsBuffer =
+        ByteBuffer.allocateDirect(textureCoords.size * BYTES_PER_FLOAT)
+            .apply { order(ByteOrder.nativeOrder()) }
+            .asFloatBuffer().apply {
+                put(textureCoords)
+                position(0)
+            }
+
+    /** Number of coordinates in this triangle list.  */
+    private val numCoords = triangleCoords.size / COORDS_PER_VERTEX
+
+    /**
+     * Draws this triangle list using OpenGL commands.
+     *
+     * @param mvpMatrix the Model View Project matrix to draw this triangle list
+     */
+    fun draw(mvpMatrix: FloatArray?) {
+        // Pass the MVP matrix, vertex data, and color to OpenGL.
+        program.bind(mvpMatrix, vertexBuffer, textureCoordsBuffer)
+
+        // Draw the triangle list.
+        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
+        if (CHECK_GL_ERRORS) checkGlError(
+            "glDrawArrays"
+        )
+    }
+
+    /** OpenGL shaders for drawing textured triangle lists.  */
+    class Program {
+        /** ID OpenGL uses to identify this program.  */
+        private val programId: Int
+
+        /** Handle for uMvpMatrix uniform in vertex shader.  */
+        private val mvpMatrixHandle: Int
+
+        /** Handle for aPosition attribute in vertex shader.  */
+        private val positionHandle: Int
+
+        /** Handle for aTextureCoordinate uniform in fragment shader.  */
+        private val textureCoordinateHandle: Int
+
+        companion object {
+            /** Trivial vertex shader that transforms the input vertex by the MVP matrix.  */
+            private const val VERTEX_SHADER_CODE = "" +
+                "uniform mat4 uMvpMatrix;\n" +
+                "attribute vec4 aPosition;\n" +
+                "attribute vec4 aTextureCoordinate;\n" +
+                "varying vec2 textureCoordinate;\n" +
+                "void main() {\n" +
+                "    gl_Position = uMvpMatrix * aPosition;\n" +
+                "    textureCoordinate = aTextureCoordinate.xy;" +
+                "}\n"
+
+            /** Trivial fragment shader that draws with a texture.  */
+            private const val FRAGMENT_SHADER_CODE = "" +
+                "varying highp vec2 textureCoordinate;\n" +
+                "uniform sampler2D texture;\n" +
+                "void main() {\n" +
+                "    gl_FragColor = texture2D(texture, textureCoordinate);\n" +
+                "}\n"
+        }
+
+        /**
+         * Tells OpenGL to use this program. Call this method before drawing a sequence of
+         * triangle lists.
+         */
+        fun bindProgramAndAttribs() {
+            GLES20.glUseProgram(programId)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glUseProgram")
+            }
+
+            // Enable vertex array (VBO).
+            GLES20.glEnableVertexAttribArray(positionHandle)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glEnableVertexAttribArray")
+            }
+
+            GLES20.glEnableVertexAttribArray(textureCoordinateHandle)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glEnableVertexAttribArray")
+            }
+        }
+
+        fun unbindAttribs() {
+            GLES20.glDisableVertexAttribArray(positionHandle)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glDisableVertexAttribArray")
+            }
+
+            GLES20.glDisableVertexAttribArray(textureCoordinateHandle)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glDisableVertexAttribArray")
+            }
+        }
+
+        fun onDestroy() {
+            GLES20.glDeleteProgram(programId)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glDeleteProgram")
+            }
+        }
+
+        /** Sends the given MVP matrix, vertex data, and color to OpenGL.  */
+        fun bind(
+            mvpMatrix: FloatArray?,
+            vertexBuffer: FloatBuffer?,
+            textureCoordinatesBuffer: FloatBuffer?
+        ) {
+            // Pass the MVP matrix to OpenGL.
+            GLES20.glUniformMatrix4fv(
+                mvpMatrixHandle, 1 /* count */, false /* transpose */,
+                mvpMatrix, 0 /* offset */
+            )
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glUniformMatrix4fv")
+            }
+
+            // Pass the VBO with the triangle list's vertices to OpenGL.
+            GLES20.glVertexAttribPointer(
+                positionHandle,
+                COORDS_PER_VERTEX,
+                GLES20.GL_FLOAT,
+                false /* normalized */,
+                VERTEX_STRIDE,
+                vertexBuffer
+            )
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glVertexAttribPointer")
+            }
+
+            // Pass the VBO with the triangle list's texture coordinates to OpenGL.
+            GLES20.glVertexAttribPointer(
+                textureCoordinateHandle,
+                TEXTURE_COORDS_PER_VERTEX,
+                GLES20.GL_FLOAT,
+                false /* normalized */,
+                TEXTURE_COORDS_VERTEX_STRIDE,
+                textureCoordinatesBuffer
+            )
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glVertexAttribPointer")
+            }
+        }
+
+        /**
+         * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
+         * should be used for all triangle lists being drawn.
+         */
+        init {
+            // Prepare shaders.
+            val vertexShader = loadShader(
+                GLES20.GL_VERTEX_SHADER,
+                VERTEX_SHADER_CODE
+            )
+            val fragmentShader = loadShader(
+                GLES20.GL_FRAGMENT_SHADER,
+                FRAGMENT_SHADER_CODE
+            )
+
+            // Create empty OpenGL Program.
+            programId = GLES20.glCreateProgram()
+            if (CHECK_GL_ERRORS) checkGlError(
+                "glCreateProgram"
+            )
+            check(programId != 0) { "glCreateProgram failed" }
+
+            // Add the shaders to the program.
+            GLES20.glAttachShader(programId, vertexShader)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glAttachShader")
+            }
+            GLES20.glAttachShader(programId, fragmentShader)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glAttachShader")
+            }
+
+            // Link the program so it can be executed.
+            GLES20.glLinkProgram(programId)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glLinkProgram")
+            }
+
+            // Get a handle to the uMvpMatrix uniform in the vertex shader.
+            mvpMatrixHandle = GLES20.glGetUniformLocation(programId, "uMvpMatrix")
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glGetUniformLocation")
+            }
+
+            // Get a handle to the vertex shader's aPosition attribute.
+            positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glGetAttribLocation")
+            }
+
+            // Get a handle to vertex shader's aUV attribute.
+            textureCoordinateHandle =
+                GLES20.glGetAttribLocation(programId, "aTextureCoordinate")
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glGetAttribLocation")
+            }
+
+            // Enable vertex array (VBO).
+            GLES20.glEnableVertexAttribArray(positionHandle)
+            if (CHECK_GL_ERRORS) {
+                checkGlError("glEnableVertexAttribArray")
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "Gles2TexturedTriangleList"
+
+        /**
+         * Whether to check for GL errors. This is slow, so not appropriate for production builds.
+         */
+        private const val CHECK_GL_ERRORS = false
+
+        /** Number of coordinates per vertex in this array: one for each of x, y, and z.  */
+        private const val COORDS_PER_VERTEX = 3
+
+        /** Number of texture coordinates per vertex in this array: one for u & v */
+        private const val TEXTURE_COORDS_PER_VERTEX = 2
+
+        /** Number of bytes to store a float in GL.  */
+        const val BYTES_PER_FLOAT = 4
+
+        /** Number of bytes per vertex.  */
+        private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT
+
+        /** Number of bytes per vertex for texture coords.  */
+        private const val TEXTURE_COORDS_VERTEX_STRIDE =
+            TEXTURE_COORDS_PER_VERTEX * BYTES_PER_FLOAT
+
+        /** Triangles have three vertices. */
+        private const val VERTICES_PER_TRIANGLE = 3
+
+        /**
+         * Checks if any of the GL calls since the last time this method was called set an error
+         * condition. Call this method immediately after calling a GL method. Pass the name of the
+         * GL operation. For example:
+         *
+         * <pre>
+         * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+         * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
+         *
+         * If the operation is not successful, the check throws an exception.
+         *
+         *
+         * *Note* This is quite slow so it's best to use it sparingly in production builds.
+         *
+         * @param glOperation name of the OpenGL call to check
+         */
+        internal fun checkGlError(glOperation: String) {
+            val error = GLES20.glGetError()
+            if (error != GLES20.GL_NO_ERROR) {
+                var errorString = GLU.gluErrorString(error)
+                if (errorString == null) {
+                    errorString = GLUtils.getEGLErrorString(error)
+                }
+                val message =
+                    glOperation + " caused GL error 0x" + Integer.toHexString(error) +
+                        ": " + errorString
+                Log.e(TAG, message)
+                throw RuntimeException(message)
+            }
+        }
+
+        /**
+         * Compiles an OpenGL shader.
+         *
+         * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
+         * @param shaderCode string containing the shader source code
+         * @return ID for the shader
+         */
+        internal fun loadShader(type: Int, shaderCode: String): Int {
+            // Create a vertex or fragment shader.
+            val shader = GLES20.glCreateShader(type)
+            if (CHECK_GL_ERRORS) checkGlError(
+                "glCreateShader"
+            )
+            check(shader != 0) { "glCreateShader failed" }
+
+            // Add the source code to the shader and compile it.
+            GLES20.glShaderSource(shader, shaderCode)
+            if (CHECK_GL_ERRORS) checkGlError(
+                "glShaderSource"
+            )
+            GLES20.glCompileShader(shader)
+            if (CHECK_GL_ERRORS) checkGlError(
+                "glCompileShader"
+            )
+            return shader
+        }
+    }
+}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index ca0c213..c2404ff 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -23,10 +23,6 @@
 import android.graphics.RectF
 import android.view.SurfaceHolder
 import androidx.annotation.Sampled
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.CanvasComplicationFactory
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.ComplicationSlot
@@ -36,6 +32,10 @@
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
 import androidx.wear.watchface.style.CurrentUserStyleRepository
@@ -44,11 +44,20 @@
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import androidx.wear.watchface.style.WatchFaceLayer
+import java.time.ZonedDateTime
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
-import java.time.ZonedDateTime
+
+private const val COLOR_STYLE_SETTING = "color_style_setting"
+private const val RED_STYLE = "red_style"
+private const val GREEN_STYLE = "green_style"
+private const val BLUE_STYLE = "blue_style"
+
+private const val HAND_STYLE_SETTING = "hand_style_setting"
+private const val CLASSIC_STYLE = "classic_style"
+private const val MODERN_STYLE = "modern_style"
+private const val GOTHIC_STYLE = "gothic_style"
 
 @Sampled
 fun kDocCreateExampleWatchFaceService(): WatchFaceService {
@@ -57,24 +66,28 @@
             UserStyleSchema(
                 listOf(
                     ListUserStyleSetting(
-                        UserStyleSetting.Id("color_style_setting"),
-                        "Colors",
-                        "Watchface colorization",
+                        UserStyleSetting.Id(COLOR_STYLE_SETTING),
+                        resources,
+                        R.string.colors_style_setting,
+                        R.string.colors_style_setting_description,
                         icon = null,
                         options = listOf(
                             ListUserStyleSetting.ListOption(
-                                Option.Id("red_style"),
-                                "Red",
+                                Option.Id(RED_STYLE),
+                                resources,
+                                R.string.colors_style_red,
                                 icon = null
                             ),
                             ListUserStyleSetting.ListOption(
-                                Option.Id("green_style"),
-                                "Green",
+                                Option.Id(GREEN_STYLE),
+                                resources,
+                                R.string.colors_style_green,
                                 icon = null
                             ),
                             ListUserStyleSetting.ListOption(
-                                Option.Id("blue_style"),
-                                "Blue",
+                                Option.Id(BLUE_STYLE),
+                                resources,
+                                R.string.colors_style_blue,
                                 icon = null
                             )
                         ),
@@ -85,20 +98,28 @@
                         )
                     ),
                     ListUserStyleSetting(
-                        UserStyleSetting.Id("hand_style_setting"),
-                        "Hand Style",
-                        "Hand visual look",
+                        UserStyleSetting.Id(HAND_STYLE_SETTING),
+                        resources,
+                        R.string.hand_style_setting,
+                        R.string.hand_style_setting_description,
                         icon = null,
                         options = listOf(
                             ListUserStyleSetting.ListOption(
-                                Option.Id("classic_style"), "Classic", icon = null
+                                Option.Id(CLASSIC_STYLE),
+                                resources,
+                                R.string.hand_style_classic,
+                                icon = null
                             ),
                             ListUserStyleSetting.ListOption(
-                                Option.Id("modern_style"), "Modern", icon = null
+                                Option.Id(MODERN_STYLE),
+                                resources,
+                                R.string.hand_style_modern,
+                                icon = null
                             ),
                             ListUserStyleSetting.ListOption(
-                                Option.Id("gothic_style"),
-                                "Gothic",
+                                Option.Id(GOTHIC_STYLE),
+                                resources,
+                                R.string.hand_style_gothic,
                                 icon = null
                             )
                         ),
diff --git a/wear/watchface/watchface/samples/src/main/res/values/strings.xml b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
index 776c74e..a5d1d53 100644
--- a/wear/watchface/watchface/samples/src/main/res/values/strings.xml
+++ b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
@@ -54,6 +54,21 @@
     <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
     <string name="colors_style_yellow">Yellow</string>
 
+    <!-- Name of watchface style category for selecting the hand style [CHAR LIMIT=20] -->
+    <string name="hand_style_setting" translatable="false">Hand Style</string>
+
+    <!-- Subtitle for the menu option to select the watch face hand style [CHAR LIMIT=20] -->
+    <string name="hand_style_setting_description" translatable="false">Hand visual look</string>
+
+    <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+    <string name="hand_style_classic" translatable="false">Classic</string>
+
+    <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+    <string name="hand_style_modern" translatable="false">Modern</string>
+
+    <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+    <string name="hand_style_gothic" translatable="false">Gothic</string>
+
     <!-- An option within the analog watch face to draw pips to mark each hour [CHAR LIMIT=20] -->
     <string name="watchface_pips_setting">Hour Pips</string>
 
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index 0d7e7cb..8084e48 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.Intent
 import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Rect
 import android.os.Build
 import android.support.wearable.watchface.SharedMemoryImage
@@ -42,7 +43,11 @@
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.data.ColorRamp
+import androidx.wear.watchface.complications.data.GoalProgressComplicationData
 import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.RangedValueTypes
+import androidx.wear.watchface.complications.data.WeightedElementsComplicationData
 import androidx.wear.watchface.control.IHeadlessWatchFace
 import androidx.wear.watchface.control.IWatchFaceControlService
 import androidx.wear.watchface.control.WatchFaceControlService
@@ -51,11 +56,11 @@
 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
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
-import androidx.wear.watchface.samples.EXAMPLE_OPENGL_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleOpenGLWatchFaceService
+import androidx.wear.watchface.samples.ExampleOpenGLWatchFaceService.Companion.EXAMPLE_OPENGL_COMPLICATION_ID
 import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.WatchFaceLayer
 import com.google.common.truth.Truth.assertThat
@@ -68,6 +73,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.time.ZonedDateTime
+import org.junit.After
 
 // This service constructs a WatchFace with a task that's posted on the UI thread.
 internal class AsyncInitWithUiThreadTaskWatchFace : WatchFaceService() {
@@ -101,267 +107,408 @@
     }
 }
 
+const val TIME_MILLIS: Long = 123456789
+val DEVICE_CONFIG = DeviceConfig(
+    /* hasLowBitAmbient = */ false,
+    /* hasBurnInProtection = */ false,
+    /* analogPreviewReferenceTimeMillis = */ 0,
+    /* digitalPreviewReferenceTimeMillis = */ 0
+)
+
 @RunWith(AndroidJUnit4::class)
 @RequiresApi(Build.VERSION_CODES.O_MR1)
 @MediumTest
 public class WatchFaceControlServiceTest {
 
-    @get:Rule
-    internal val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
+   @get:Rule
+   internal val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
 
-    @Before
-    public fun setUp() {
-        Assume.assumeTrue("This test suite assumes API 27", Build.VERSION.SDK_INT >= 27)
-    }
+   private lateinit var instance: IHeadlessWatchFace
 
-    private fun createInstance(width: Int, height: Int): IHeadlessWatchFace {
-        val instanceService = IWatchFaceControlService.Stub.asInterface(
-            WatchFaceControlService().apply {
-                setContext(ApplicationProvider.getApplicationContext<Context>())
-            }.onBind(
-                Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
-            )
-        )
-        return instanceService.createHeadlessWatchFaceInstance(
-            HeadlessWatchFaceInstanceParams(
-                ComponentName(
-                    ApplicationProvider.getApplicationContext<Context>(),
-                    ExampleCanvasAnalogWatchFaceService::class.java
-                ),
-                DeviceConfig(
-                    false,
-                    false,
-                    0,
-                    0
-                ),
-                width,
-                height,
-                null
-            )
-        )
-    }
+   @Before
+   public fun setUp() {
+       Assume.assumeTrue("This test suite assumes API 27", Build.VERSION.SDK_INT >= 27)
+   }
 
-    private fun createOpenGlInstance(width: Int, height: Int): IHeadlessWatchFace {
-        val instanceService = IWatchFaceControlService.Stub.asInterface(
-            WatchFaceControlService().apply {
-                setContext(ApplicationProvider.getApplicationContext<Context>())
-            }.onBind(
-                Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
-            )
-        )
-        return instanceService.createHeadlessWatchFaceInstance(
-            HeadlessWatchFaceInstanceParams(
-                ComponentName(
-                    ApplicationProvider.getApplicationContext<Context>(),
-                    ExampleOpenGLWatchFaceService::class.java
-                ),
-                DeviceConfig(
-                    false,
-                    false,
-                    0,
-                    0
-                ),
-                width,
-                height,
-                null
-            )
-        )
-    }
+   @After
+   public fun tearDown() {
+       if (this::instance.isInitialized) {
+           instance.release()
+       }
+   }
 
-    @Test
-    public fun createWatchFaceInstanceWithRangedValueComplications() {
-        val instance = createInstance(400, 400)
-        try {
-            val bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                instance.renderWatchFaceToBitmap(
-                    WatchFaceRenderParams(
-                        RenderParameters(
-                            DrawMode.INTERACTIVE,
-                            WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                            null
-                        ).toWireFormat(),
-                        1234567890,
-                        null,
-                        listOf(
-                            IdAndComplicationDataWireFormat(
-                                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
-                                RangedValueComplicationData.Builder(
-                                    100.0f, 0.0f, 100.0f,
-                                    ComplicationText.EMPTY
-                                )
-                                    .setText(PlainComplicationText.Builder("100%").build())
-                                    .build()
-                                    .asWireComplicationData()
-                            ),
-                            IdAndComplicationDataWireFormat(
-                                EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
-                                RangedValueComplicationData.Builder(
-                                    75.0f, 0.0f, 100.0f,
-                                    ComplicationText.EMPTY
-                                )
-                                    .setText(PlainComplicationText.Builder("75%").build())
-                                    .build()
-                                    .asWireComplicationData()
-                            )
-                        )
-                    )
-                )
-            )
+   private fun createInstance(width: Int, height: Int) {
+       val instanceService = IWatchFaceControlService.Stub.asInterface(
+           WatchFaceControlService().apply {
+               setContext(ApplicationProvider.getApplicationContext<Context>())
+           }.onBind(
+               Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+           )
+       )
+       instance = instanceService.createHeadlessWatchFaceInstance(
+           HeadlessWatchFaceInstanceParams(
+               ComponentName(
+                   ApplicationProvider.getApplicationContext<Context>(),
+                   ExampleCanvasAnalogWatchFaceService::class.java
+               ),
+               DEVICE_CONFIG,
+               width,
+               height,
+               null
+           )
+       )
+   }
 
-            bitmap.assertAgainstGolden(screenshotRule, "ranged_value_complications")
-        } finally {
-            instance.release()
-        }
-    }
+   private fun createOpenGlInstance(width: Int, height: Int) {
+       val instanceService = IWatchFaceControlService.Stub.asInterface(
+           WatchFaceControlService().apply {
+               setContext(ApplicationProvider.getApplicationContext<Context>())
+           }.onBind(
+               Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+           )
+       )
+       instance = instanceService.createHeadlessWatchFaceInstance(
+           HeadlessWatchFaceInstanceParams(
+               ComponentName(
+                   ApplicationProvider.getApplicationContext<Context>(),
+                   ExampleOpenGLWatchFaceService::class.java
+               ),
+               DEVICE_CONFIG,
+               width,
+               height,
+               null
+           )
+       )
+   }
 
-    @Test
-    public fun createHeadlessWatchFaceInstance() {
-        val instance = createInstance(100, 100)
-        try {
-            val bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                instance.renderWatchFaceToBitmap(
-                    WatchFaceRenderParams(
-                        RenderParameters(
-                            DrawMode.INTERACTIVE,
-                            WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                            null
-                        ).toWireFormat(),
-                        1234567890,
-                        null,
-                        listOf(
-                            IdAndComplicationDataWireFormat(
-                                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
-                                ShortTextComplicationData.Builder(
-                                    PlainComplicationText.Builder("Mon").build(),
-                                    ComplicationText.EMPTY
-                                )
-                                    .setTitle(PlainComplicationText.Builder("23rd").build())
-                                    .build()
-                                    .asWireComplicationData()
-                            ),
-                            IdAndComplicationDataWireFormat(
-                                EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
-                                ShortTextComplicationData.Builder(
-                                    PlainComplicationText.Builder("100").build(),
-                                    ComplicationText.EMPTY
-                                )
-                                    .setTitle(PlainComplicationText.Builder("Steps").build())
-                                    .build()
-                                    .asWireComplicationData()
-                            )
-                        )
-                    )
-                )
-            )
+   @Test
+   public fun createWatchFaceInstanceWithRangedValueComplications() {
+       createInstance(width = 400, height = 400)
+       val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+           instance.renderWatchFaceToBitmap(
+               WatchFaceRenderParams(
+                   RenderParameters(
+                       DrawMode.INTERACTIVE,
+                       WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                       null
+                   ).toWireFormat(),
+                   TIME_MILLIS,
+                   null,
+                   listOf(
+                       IdAndComplicationDataWireFormat(
+                           EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+                           RangedValueComplicationData.Builder(
+                               value = 100.0f,
+                               min = 0.0f,
+                               max = 100.0f,
+                               ComplicationText.EMPTY
+                           )
+                               .setText(PlainComplicationText.Builder("100%").build())
+                               .build()
+                               .asWireComplicationData()
+                       ),
+                       IdAndComplicationDataWireFormat(
+                           EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
+                           RangedValueComplicationData.Builder(
+                               value = 75.0f,
+                               min = 0.0f,
+                               max = 100.0f,
+                               ComplicationText.EMPTY
+                           )
+                               .setText(PlainComplicationText.Builder("75%").build())
+                               .build()
+                               .asWireComplicationData()
+                       )
+                   )
+               )
+           )
+       )
 
-            bitmap.assertAgainstGolden(screenshotRule, "service_interactive")
-        } finally {
-            instance.release()
-        }
-    }
+       bitmap.assertAgainstGolden(screenshotRule, "ranged_value_complications")
+   }
 
-    @Test
-    public fun createHeadlessOpenglWatchFaceInstance() {
-        val instance = createOpenGlInstance(400, 400)
-        try {
-            val bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                instance.renderWatchFaceToBitmap(
-                    WatchFaceRenderParams(
-                        RenderParameters(
-                            DrawMode.INTERACTIVE,
-                            WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                            null
-                        ).toWireFormat(),
-                        1234567890,
-                        null,
-                        listOf(
-                            IdAndComplicationDataWireFormat(
-                                EXAMPLE_OPENGL_COMPLICATION_ID,
-                                ShortTextComplicationData.Builder(
-                                    PlainComplicationText.Builder("Mon").build(),
-                                    ComplicationText.EMPTY
-                                )
-                                    .setTitle(PlainComplicationText.Builder("23rd").build())
-                                    .build()
-                                    .asWireComplicationData()
-                            )
-                        )
-                    )
-                )
-            )
+   @Test
+   public fun createHeadlessWatchFaceInstance() {
+       createInstance(width = 100, height = 100)
+       val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+           instance.renderWatchFaceToBitmap(
+               WatchFaceRenderParams(
+                   RenderParameters(
+                       DrawMode.INTERACTIVE,
+                       WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                       null
+                   ).toWireFormat(),
+                   TIME_MILLIS,
+                   null,
+                   listOf(
+                       IdAndComplicationDataWireFormat(
+                           EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+                           ShortTextComplicationData.Builder(
+                               PlainComplicationText.Builder("Mon").build(),
+                               ComplicationText.EMPTY
+                           )
+                               .setTitle(PlainComplicationText.Builder("23rd").build())
+                               .build()
+                               .asWireComplicationData()
+                       ),
+                       IdAndComplicationDataWireFormat(
+                           EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
+                           ShortTextComplicationData.Builder(
+                               PlainComplicationText.Builder("100").build(),
+                               ComplicationText.EMPTY
+                           )
+                               .setTitle(PlainComplicationText.Builder("Steps").build())
+                               .build()
+                               .asWireComplicationData()
+                       )
+                   )
+               )
+           )
+       )
 
-            bitmap.assertAgainstGolden(screenshotRule, "opengl_headless")
-        } finally {
-            instance.release()
-        }
-    }
+       bitmap.assertAgainstGolden(screenshotRule, "service_interactive")
+   }
 
-    @Test
-    public fun testCommandTakeComplicationScreenShot() {
-        val instance = createInstance(400, 400)
-        try {
-            val bitmap = SharedMemoryImage.ashmemReadImageBundle(
-                instance.renderComplicationToBitmap(
-                    ComplicationRenderParams(
-                        EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
-                        RenderParameters(
-                            DrawMode.INTERACTIVE,
-                            WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                            null,
-                        ).toWireFormat(),
-                        123456789,
-                        ShortTextComplicationData.Builder(
-                            PlainComplicationText.Builder("Mon").build(),
-                            ComplicationText.EMPTY
-                        )
-                            .setTitle(PlainComplicationText.Builder("23rd").build())
-                            .build()
-                            .asWireComplicationData(),
-                        null
-                    )
-                )
-            )
+   @Test
+   public fun createHeadlessOpenglWatchFaceInstance() {
+       createOpenGlInstance(width = 400, height = 400)
+       val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+           instance.renderWatchFaceToBitmap(
+               WatchFaceRenderParams(
+                   RenderParameters(
+                       DrawMode.INTERACTIVE,
+                       WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                       null
+                   ).toWireFormat(),
+                   TIME_MILLIS,
+                   null,
+                   listOf(
+                       IdAndComplicationDataWireFormat(
+                           EXAMPLE_OPENGL_COMPLICATION_ID,
+                           ShortTextComplicationData.Builder(
+                               PlainComplicationText.Builder("Mon").build(),
+                               ComplicationText.EMPTY
+                           )
+                               .setTitle(PlainComplicationText.Builder("23rd").build())
+                               .build()
+                               .asWireComplicationData()
+                       )
+                   )
+               )
+           )
+       )
 
-            bitmap.assertAgainstGolden(
-                screenshotRule,
-                "leftComplication"
-            )
-        } finally {
-            instance.release()
-        }
-    }
+       bitmap.assertAgainstGolden(screenshotRule, "opengl_headless")
+   }
 
-    @Test
-    public fun asyncInitWithUiThreadTaskWatchFace() {
-        val instanceService = IWatchFaceControlService.Stub.asInterface(
-            WatchFaceControlService().apply {
-                setContext(ApplicationProvider.getApplicationContext<Context>())
-            }.onBind(
-                Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
-            )
-        )
-        // This shouldn't hang.
-        val headlessInstance = instanceService.createHeadlessWatchFaceInstance(
-            HeadlessWatchFaceInstanceParams(
-                ComponentName(
-                    ApplicationProvider.getApplicationContext<Context>(),
-                    AsyncInitWithUiThreadTaskWatchFace::class.java
-                ),
-                DeviceConfig(
-                    false,
-                    false,
-                    0,
-                    0
-                ),
-                100,
-                100,
-                null
-            )
-        )
-        try {
-            assertThat(headlessInstance.userStyleSchema.mSchema).isEmpty()
-        } finally {
-            headlessInstance.release()
-        }
-    }
+   @Test
+   public fun testCommandTakeComplicationScreenShot() {
+       createInstance(width = 400, height = 400)
+       val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+           instance.renderComplicationToBitmap(
+               ComplicationRenderParams(
+                   EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+                   RenderParameters(
+                       DrawMode.INTERACTIVE,
+                       WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                       null,
+                   ).toWireFormat(),
+                   TIME_MILLIS,
+                   ShortTextComplicationData.Builder(
+                       PlainComplicationText.Builder("Mon").build(),
+                       ComplicationText.EMPTY
+                   )
+                       .setTitle(PlainComplicationText.Builder("23rd").build())
+                       .build()
+                       .asWireComplicationData(),
+                   null
+               )
+           )
+       )
+
+       bitmap.assertAgainstGolden(
+           screenshotRule,
+           "leftComplication"
+       )
+   }
+
+   @Test
+   @Suppress("NewApi")
+   public fun testGoalProgressComplication() {
+       createInstance(width = 400, height = 400)
+       val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+           instance.renderComplicationToBitmap(
+               ComplicationRenderParams(
+                   EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+                   RenderParameters(
+                       DrawMode.INTERACTIVE,
+                       WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                       null,
+                   ).toWireFormat(),
+                   TIME_MILLIS,
+                   GoalProgressComplicationData.Builder(
+                       value = 12345.0f,
+                       targetValue = 10000.0f,
+                       PlainComplicationText.Builder("12345 steps").build()
+                   ).setText(PlainComplicationText.Builder("12345").build())
+                       .setTitle(PlainComplicationText.Builder("Steps").build())
+                       .build()
+                       .asWireComplicationData(),
+                   null
+               )
+           )
+       )
+
+       bitmap.assertAgainstGolden(
+           screenshotRule,
+           "goalProgressComplication"
+       )
+   }
+
+   @Test
+   public fun testColorRampRangedValueComplication() {
+       createInstance(400, 400)
+       val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+           instance.renderComplicationToBitmap(
+               ComplicationRenderParams(
+                   EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+                   RenderParameters(
+                       DrawMode.INTERACTIVE,
+                       WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                       null,
+                   ).toWireFormat(),
+                   TIME_MILLIS,
+                   RangedValueComplicationData.Builder(
+                       value = 75f,
+                       min = 0.0f,
+                       max = 100.0f,
+                       PlainComplicationText.Builder("Rainbow colors").build()
+                   ).setText(PlainComplicationText.Builder("Colors").build())
+                       .setValueType(RangedValueTypes.SCORE)
+                       .setColorRamp(
+                           ColorRamp(
+                               intArrayOf(
+                                   Color.GREEN,
+                                   Color.YELLOW,
+                                   Color.argb(255, 255, 255, 0),
+                                   Color.RED,
+                                   Color.argb(255, 255, 0, 255),
+                                   Color.argb(255, 92, 64, 51)
+                               ),
+                               interpolated = true
+                           )
+                       )
+                       .build()
+                       .asWireComplicationData(),
+                   null
+               )
+           )
+       )
+
+       bitmap.assertAgainstGolden(
+           screenshotRule,
+           "colorRampRangedValueComplication"
+       )
+   }
+
+   @Test
+   public fun testNonInterpolatedColorRampRangedValueComplication() {
+       createInstance(width = 400, height = 400)
+       val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+           instance.renderComplicationToBitmap(
+               ComplicationRenderParams(
+                   EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+                   RenderParameters(
+                       DrawMode.INTERACTIVE,
+                       WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                       null,
+                   ).toWireFormat(),
+                   TIME_MILLIS,
+                   RangedValueComplicationData.Builder(
+                       value = 75f,
+                       min = 0.0f,
+                       max = 100.0f,
+                       PlainComplicationText.Builder("Rainbow colors").build()
+                   ).setText(PlainComplicationText.Builder("Colors").build())
+                       .setValueType(RangedValueTypes.SCORE)
+                       .setColorRamp(
+                           ColorRamp(
+                               intArrayOf(Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW),
+                               interpolated = false
+                           )
+                       )
+                       .build()
+                       .asWireComplicationData(),
+                   null
+               )
+           )
+       )
+
+       bitmap.assertAgainstGolden(
+           screenshotRule,
+           "nonInterpolatedColorRampRangedValueComplication"
+       )
+   }
+
+   @Test
+   @Suppress("NewApi")
+   public fun testWeightedElementComplication() {
+       createInstance(width = 400, height = 400)
+       val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+           instance.renderComplicationToBitmap(
+               ComplicationRenderParams(
+                   EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+                   RenderParameters(
+                       DrawMode.INTERACTIVE,
+                       WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                       null,
+                   ).toWireFormat(),
+                   TIME_MILLIS,
+                   WeightedElementsComplicationData.Builder(
+                       listOf(
+                           WeightedElementsComplicationData.Element(weight = 1.0f, Color.RED),
+                           WeightedElementsComplicationData.Element(weight = 1.0f, Color.GREEN),
+                           WeightedElementsComplicationData.Element(weight = 2.0f, Color.BLUE),
+                           WeightedElementsComplicationData.Element(weight = 3.0f, Color.YELLOW)
+                       ),
+                       PlainComplicationText.Builder("Example").build()
+                   ).setText(PlainComplicationText.Builder("Calories").build())
+                       .build()
+                       .asWireComplicationData(),
+                   null
+               )
+           )
+       )
+
+       bitmap.assertAgainstGolden(
+           screenshotRule,
+           "weightedElementComplication"
+       )
+   }
+
+   @Test
+   public fun asyncInitWithUiThreadTaskWatchFace() {
+       val instanceService = IWatchFaceControlService.Stub.asInterface(
+           WatchFaceControlService().apply {
+               setContext(ApplicationProvider.getApplicationContext<Context>())
+           }.onBind(
+               Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+           )
+       )
+       // This shouldn't hang.
+       instance = instanceService.createHeadlessWatchFaceInstance(
+           HeadlessWatchFaceInstanceParams(
+               ComponentName(
+                   ApplicationProvider.getApplicationContext<Context>(),
+                   AsyncInitWithUiThreadTaskWatchFace::class.java
+               ),
+               DEVICE_CONFIG,
+               /* width = */ 100,
+               /* height = */100,
+               /* instanceId = */null
+           )
+       )
+
+       assertThat(instance.userStyleSchema.mSchema).isEmpty()
+   }
 }
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index bab4cc7..548403e 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -70,10 +70,10 @@
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 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
-import androidx.wear.watchface.samples.GREEN_STYLE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.COLOR_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.GREEN_STYLE
 import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.WatchFaceLayer
@@ -665,25 +665,25 @@
             listOf(
                 IdAndComplicationDataWireFormat(
                     EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
-                        ShortTextComplicationData.Builder(
-                            PlainComplicationText.Builder("Test").build(),
-                            ComplicationText.EMPTY
-                        )
-                            .setSmallImage(smallImage)
-                            .build()
-                            .asWireComplicationData()
+                    ShortTextComplicationData.Builder(
+                        PlainComplicationText.Builder("Test").build(),
+                        ComplicationText.EMPTY
+                    )
+                        .setSmallImage(smallImage)
+                        .build()
+                        .asWireComplicationData()
                 ),
                 IdAndComplicationDataWireFormat(
                     EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
-                        RangedValueComplicationData.Builder(
-                            25f,
-                            0f,
-                            100f,
-                            ComplicationText.EMPTY
-                        )
-                            .setSmallImage(smallImage)
-                            .build()
-                            .asWireComplicationData()
+                    RangedValueComplicationData.Builder(
+                        25f,
+                        0f,
+                        100f,
+                        ComplicationText.EMPTY
+                    )
+                        .setSmallImage(smallImage)
+                        .build()
+                        .asWireComplicationData()
                 )
             )
         )
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index d557218..bd6657e 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -374,7 +374,7 @@
     @ComplicationSlotBoundsType public val boundsType: Int,
     bounds: ComplicationSlotBounds,
     public val canvasComplicationFactory: CanvasComplicationFactory,
-    supportedTypes: List<ComplicationType>,
+    public val supportedTypes: List<ComplicationType>,
     defaultPolicy: DefaultComplicationDataSourcePolicy,
     defaultDataSourceType: ComplicationType,
     @get:JvmName("isInitiallyEnabled")
@@ -861,12 +861,6 @@
             enabledDirty = true
         }
 
-    /** The types of complicationSlots the complication supports. Must be non-empty. */
-
-    public val supportedTypes: List<ComplicationType> = supportedTypes
-        @UiThread // TODO(b/229727216): Remove this annotation.
-        get
-
     internal var defaultDataSourcePolicyDirty = true
 
     /**
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 3916ab2..b5b5e6c 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -2662,7 +2662,10 @@
             writer.println("WatchFaceEngine:")
             writer.increaseIndent()
             when {
-                wslFlow.iWatchFaceServiceInitialized() -> writer.println("WSL style init flow")
+                wslFlow.iWatchFaceServiceInitialized() -> {
+                    writer.println("WSL style init flow")
+                    writer.println("watchFaceInitStarted=${wslFlow.watchFaceInitStarted}")
+                }
                 this.watchFaceCreatedOrPending() -> writer.println("Androidx style init flow")
                 isPreAndroidR() -> writer.println("Expecting WSL style init")
                 else -> writer.println("Expecting androidx style style init")
@@ -2681,7 +2684,6 @@
                 }
             }
             writer.println("createdBy=$createdBy")
-            writer.println("watchFaceInitStarted=$wslFlow.watchFaceInitStarted")
             writer.println("asyncWatchFaceConstructionPending=$asyncWatchFaceConstructionPending")
             writer.println(
                 "systemViewOfContentDescriptionLabelsIsStale=" +
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index 11ef49f..b41c16a 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -58,7 +58,7 @@
 @VisibleForTesting
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public open class WatchFaceControlService : Service() {
-    private val watchFaceInstanceServiceStub by lazy { createServiceStub() }
+    private var watchFaceInstanceServiceStub: IWatchFaceInstanceServiceStub? = null
 
     /** @hide */
     public companion object {
@@ -69,6 +69,9 @@
     override fun onBind(intent: Intent?): IBinder? =
         TraceEvent("WatchFaceControlService.onBind").use {
             if (ACTION_WATCHFACE_CONTROL_SERVICE == intent?.action) {
+                if (watchFaceInstanceServiceStub == null) {
+                    watchFaceInstanceServiceStub = createServiceStub()
+                }
                 watchFaceInstanceServiceStub
             } else {
                 null
@@ -94,13 +97,19 @@
         HeadlessWatchFaceImpl.dump(indentingPrintWriter)
         indentingPrintWriter.flush()
     }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        watchFaceInstanceServiceStub?.onDestroy()
+    }
 }
 
 /** @hide */
 @RequiresApi(27)
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public open class IWatchFaceInstanceServiceStub(
-    private val service: Service,
+    // We need to explicitly null this object in onDestroy to avoid a memory leak.
+    private var service: Service?,
     private val uiThreadCoroutineScope: CoroutineScope
 ) : IWatchFaceControlService.Stub() {
     override fun getApiVersion(): Int = IWatchFaceControlService.API_VERSION
@@ -169,11 +178,11 @@
                     method!!.isAccessible = true
                     method.invoke(
                         watchFaceService,
-                        service as Context,
+                        service!! as Context,
                         null,
                         watchFaceService::class.qualifiedName,
                         null,
-                        service.application,
+                        service!!.application,
                         null
                     )
                 } catch (e: Exception) {
@@ -280,4 +289,8 @@
             throw e
         }
     }
+
+    fun onDestroy() {
+        service = null
+    }
 }
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 038d5ce..0bdb170 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -4071,6 +4071,7 @@
         ).isEqualTo("Example")
 
         assertThat(engineWrapper.contentDescriptionLabels[1].tapAction).isEqualTo(pendingIntent)
+        engineWrapper.onDestroy()
     }
 
     @Test
@@ -4826,7 +4827,7 @@
     }
 
     @Test
-    public fun dump() {
+    public fun dump_androidXFlow() {
         // Advance time a little so timestamps are not zero
         looperTimeMillis = 1000
 
@@ -4878,6 +4879,24 @@
         assertThat(dump).contains("screenBounds=Rect(0, 0 - 100, 100)")
         assertThat(dump).contains("interactiveDrawModeUpdateDelayMillis=16")
         assertThat(dump).contains("watchFaceLayers=BASE, COMPLICATIONS, COMPLICATIONS_OVERLAY")
+        assertThat(dump).doesNotContain("watchFaceInitStarted=")
+    }
+
+    @Test
+    public fun dump_wslFlow() {
+        initEngine(
+            WatchFaceType.DIGITAL,
+            listOf(leftComplication, rightComplication),
+            UserStyleSchema(emptyList())
+        )
+
+        val writer = StringWriter()
+        val indentingPrintWriter = IndentingPrintWriter(writer)
+
+        engineWrapper.dump(indentingPrintWriter)
+
+        val dump = writer.toString()
+        assertThat(dump).contains("watchFaceInitStarted=true")
     }
 
     @Test
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
new file mode 100644
index 0000000..19549fb
--- /dev/null
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.wear.watchface.control.data
+
+import android.content.ComponentName
+import androidx.versionedparcelable.ParcelUtils
+import androidx.wear.watchface.data.DeviceConfig
+import androidx.wear.watchface.data.WatchUiState
+import androidx.wear.watchface.style.UserStyle
+import java.io.ByteArrayOutputStream
+import org.junit.Test
+
+class WallpaperInteractiveWatchFaceInstanceParamsTest {
+    @Test
+    fun canBeWrittenToOutputStream() {
+        val params = WallpaperInteractiveWatchFaceInstanceParams(
+            "instanceId",
+            DeviceConfig(
+                false,
+                false,
+                10,
+                10
+            ),
+            WatchUiState(
+                false,
+                0,
+            ),
+            UserStyle(emptyMap()).toWireFormat(),
+            null,
+            ComponentName("some.package", "SomeClass")
+        )
+
+        val dummyOutputStream = ByteArrayOutputStream()
+
+        // Should not throw an exception
+        ParcelUtils.toOutputStream(params, dummyOutputStream)
+    }
+}
\ No newline at end of file
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
index 4afca04..eaa4585 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
@@ -50,7 +50,7 @@
     @Suppress("DEPRECATION") // b/251211092
     @Before
     fun setUp() {
-        MockitoAnnotations.initMocks(this)
+        MockitoAnnotations.openMocks(this)
         ShadowContentResolver.registerProviderInternal(
             PhoneTypeHelper.SETTINGS_AUTHORITY,
             mockContentProvider
@@ -167,4 +167,4 @@
         )
             .thenReturn(createFakeBluetoothModeCursor(phoneType))
     }
-}
\ No newline at end of file
+}
diff --git a/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java b/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java
index f3a455a..47c60499 100644
--- a/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java
+++ b/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java
@@ -41,7 +41,7 @@
     @SuppressWarnings("deprecation") // b/251211092
     @Before
     public void setUp() {
-        MockitoAnnotations.initMocks(this);
+        MockitoAnnotations.openMocks(this);
         mContext = ApplicationProvider.getApplicationContext();
         mShadowPackageManager = Shadows.shadowOf(mContext.getPackageManager());
     }
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
new file mode 100644
index 0000000..1c4b8ce
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.webkit;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.core.content.ContextCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.webkit.WebViewFeature;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/**
+ * Integration test for {@link ProcessGlobalConfigActivity}.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class ProcessGlobalConfigActivityTestAppTest {
+    @Rule
+    public ActivityScenarioRule<MainActivity> mRule =
+            new ActivityScenarioRule<>(MainActivity.class);
+
+    @Before
+    public void setUp() {
+        WebkitTestHelpers.assumeStartupFeature(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
+                ApplicationProvider.getApplicationContext());
+    }
+
+    @Test
+    public void testSetDataDirectorySuffix() throws Throwable {
+        final String dataDirPrefix = "app_webview_";
+        final String dataDirSuffix = "per_process_webview_data_0";
+
+        WebkitTestHelpers.clickMenuListItemWithString(
+                R.string.process_global_config_activity_title);
+        Thread.sleep(1000);
+
+        File file = new File(ContextCompat.getDataDir(ApplicationProvider.getApplicationContext()),
+                dataDirPrefix + dataDirSuffix);
+
+        assertTrue(file.exists());
+    }
+}
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
index 28d12ab..7f5257d 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
@@ -16,11 +16,10 @@
 
 package com.example.androidx.webkit;
 
+import static androidx.test.espresso.Espresso.onData;
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.click;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
-import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
@@ -29,8 +28,8 @@
 import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
 
 import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.core.AllOf.allOf;
-import static org.hamcrest.core.Is.is;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasToString;
 
 import android.content.Context;
 
@@ -54,8 +53,8 @@
      * @param resourceId string id of menu item
      */
     public static void clickMenuListItemWithString(@StringRes int resourceId) {
-        onView(allOf(isDescendantOfA(withClassName(is(MenuListView.class.getName()))),
-                withText(resourceId))).perform(click());
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        onData(hasToString(equalTo(context.getString(resourceId)))).perform(click());
     }
 
     /**
@@ -128,6 +127,39 @@
     }
 
     /**
+     * Throws {@link org.junit.AssumptionViolatedException} if the device does not support the
+     * particular startup feature, otherwise returns.
+     *
+     * <p>
+     * This provides a more descriptive message than a bare {@code assumeTrue} call.
+     *
+     * @param featureName the feature to be checked
+     */
+    public static void assumeStartupFeature(
+            @WebViewFeature.WebViewStartupFeature String featureName,
+            Context context) {
+        final String msg = "This device does not have the feature '" +  featureName + "'";
+        final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(featureName, context);
+        Assume.assumeTrue(msg, hasFeature);
+    }
+
+    /**
+     * Throws {@link org.junit.AssumptionViolatedException} if the device supports the
+     * particular startup feature, otherwise returns.
+     *
+     * <p>
+     * This provides a more descriptive message than a bare {@code assumeFalse} call.
+     *
+     * @param featureName the feature to be checked
+     */
+    public static void assumeStartupFeatureNotAvailable(
+            @WebViewFeature.WebViewStartupFeature String featureName, Context context) {
+        final String msg = "This device has the feature '" +  featureName + "'";
+        final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(featureName, context);
+        Assume.assumeFalse(msg, hasFeature);
+    }
+
+    /**
      * Javascript has to be enabled for espresso tests to work.
      *
      * @param webViewIds WebView IDs for which to enable JavaScript
diff --git a/webkit/integration-tests/testapp/src/main/AndroidManifest.xml b/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
index 74e90b23..0428b05 100644
--- a/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -123,12 +123,24 @@
         <activity
             android:name=".GetVariationsHeaderActivity"
             android:exported="true" />
-        <activity android:name=".ProcessGlobalConfigActivity"
+        <activity
+            android:name=".ProcessGlobalConfigActivity"
+            android:process=":processGlobalConfigTest"
             android:exported="true" />
         <activity
             android:name=".RequestedWithHeaderActivity"
             android:exported="true" />
         <activity android:name=".CookieManagerActivity"
             android:exported="true" />
+        <activity
+            android:name=".ImageDragActivity"
+            android:exported="false" />
+
+        <provider
+            android:authorities="com.example.androidx.webkit.DropDataProvider"
+            android:name="androidx.webkit.DropDataContentProvider"
+            android:exported="false"
+            android:grantUriPermissions="true"/>
+
     </application>
 </manifest>
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
new file mode 100644
index 0000000..1ae07c2
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.webkit;
+
+import android.os.Bundle;
+import android.webkit.WebView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * An Activity to demonstrate example for dragging image out to other apps.
+ * You can use google logo to test.
+ */
+public class ImageDragActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_image_drag);
+        WebView demoWebview = findViewById(R.id.image_webview);
+
+        demoWebview.loadUrl("www.google.com");
+    }
+}
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
index b5525525..bf8f85e 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
@@ -84,6 +84,9 @@
                 new MenuListView.MenuItem(
                         getResources().getString(R.string.cookie_manager_activity_title),
                         new Intent(activityContext, CookieManagerActivity.class)),
+                new MenuListView.MenuItem(
+                        getResources().getString(R.string.image_drag_drop_activity_title),
+                        new Intent(activityContext, ImageDragActivity.class)),
         };
         listView.setItems(menuItems);
     }
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
index d0080db..c74f667 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
@@ -35,14 +35,19 @@
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        if (WebViewFeature.isStartupFeatureSupported(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
+        setTitle(R.string.process_global_config_activity_title);
+        WebkitHelpers.appendWebViewVersionToTitle(this);
+
+        if (!WebViewFeature.isStartupFeatureSupported(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
                 this)) {
-            ProcessGlobalConfig.createInstance()
-                    .setDataDirectorySuffix("per_process_webview_data_0", this)
-                    .apply();
+            WebkitHelpers.showMessageInActivity(this, R.string.webkit_api_not_available);
+            return;
         }
-        WebView wv = new WebView(this);
-        setContentView(wv);
+        ProcessGlobalConfig.createInstance()
+                .setDataDirectorySuffix("per_process_webview_data_0", this)
+                .apply();
+        setContentView(R.layout.activity_process_global_config);
+        WebView wv = findViewById(R.id.process_global_config_webview);
         wv.setWebViewClient(new WebViewClient());
         wv.loadUrl("www.google.com");
     }
diff --git a/webkit/integration-tests/testapp/src/main/res/layout/activity_image_drag.xml b/webkit/integration-tests/testapp/src/main/res/layout/activity_image_drag.xml
new file mode 100644
index 0000000..dab2b9a
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/res/layout/activity_image_drag.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <WebView
+        android:id="@+id/image_webview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml b/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
new file mode 100644
index 0000000..c2717b2
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<WebView
+xmlns:android="http://schemas.android.com/apk/res/android"
+android:id="@+id/process_global_config_webview"
+android:layout_width="match_parent"
+android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index 92d9e9c..fd3734d 100644
--- a/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -88,6 +88,7 @@
     <string name="cookie_manager_old_api_text">Old cookie API: </string>
     <string name="cookie_manager_new_api_text">New cookie API: </string>
     <string name="cookie_manager_get_cookie_info_not_supported">CookieManagerCompat#getCookieInfo not supported.</string>
+    <string name="image_drag_drop_activity_title">Image Drag And Drop</string>
 
     <!-- Proxy Override -->
     <string name="proxy_override_activity_title">Proxy Override</string>
diff --git a/webkit/webkit/api/current.ignore b/webkit/webkit/api/current.ignore
new file mode 100644
index 0000000..2da110a
--- /dev/null
+++ b/webkit/webkit/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.webkit.ServiceWorkerWebSettingsCompat#getRequestedWithHeaderOriginAllowList():
+    Added method androidx.webkit.ServiceWorkerWebSettingsCompat.getRequestedWithHeaderOriginAllowList()
+AddedAbstractMethod: androidx.webkit.ServiceWorkerWebSettingsCompat#setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String>):
+    Added method androidx.webkit.ServiceWorkerWebSettingsCompat.setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String>)
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index c19a92e..772b183 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -65,10 +65,12 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
   }
 
   public class TracingConfig {
@@ -137,6 +139,7 @@
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
@@ -145,6 +148,7 @@
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
     field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
     field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
diff --git a/webkit/webkit/api/public_plus_experimental_current.txt b/webkit/webkit/api/public_plus_experimental_current.txt
index c19a92e..772b183 100644
--- a/webkit/webkit/api/public_plus_experimental_current.txt
+++ b/webkit/webkit/api/public_plus_experimental_current.txt
@@ -65,10 +65,12 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
   }
 
   public class TracingConfig {
@@ -137,6 +139,7 @@
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
@@ -145,6 +148,7 @@
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
     field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
     field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
diff --git a/webkit/webkit/api/restricted_current.ignore b/webkit/webkit/api/restricted_current.ignore
new file mode 100644
index 0000000..2da110a
--- /dev/null
+++ b/webkit/webkit/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.webkit.ServiceWorkerWebSettingsCompat#getRequestedWithHeaderOriginAllowList():
+    Added method androidx.webkit.ServiceWorkerWebSettingsCompat.getRequestedWithHeaderOriginAllowList()
+AddedAbstractMethod: androidx.webkit.ServiceWorkerWebSettingsCompat#setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String>):
+    Added method androidx.webkit.ServiceWorkerWebSettingsCompat.setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String>)
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index c19a92e..772b183 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -65,10 +65,12 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
   }
 
   public class TracingConfig {
@@ -137,6 +139,7 @@
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
@@ -145,6 +148,7 @@
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
     method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
+    method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
     field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
     field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java
index e86763f..c71942e 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java
@@ -135,6 +135,7 @@
      */
     @SuppressWarnings("deprecation")
     @FlakyTest(bugId = 240432254)
+    @SdkSuppress(maxSdkVersion = 32) // b/254572377
     @Test
     public void testForceDark_webThemeDarkeningOnly() {
         WebkitUtils.checkFeature(WebViewFeature.FORCE_DARK);
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
index 4606dc4..f2a5a06 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
@@ -55,6 +55,7 @@
     /**
      * Test web content is always light regardless the algorithmic darkening is allowed or not.
      */
+    @SdkSuppress(maxSdkVersion = 32) // b/254572377
     @Test
     public void testSimplifiedDarkMode_rendersLight() throws Throwable {
         WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
@@ -79,6 +80,7 @@
     /**
      * Test web content is always light (if supported) on the light theme app.
      */
+    @SdkSuppress(maxSdkVersion = 32) // b/254572377
     @Test
     public void testSimplifiedDarkMode_pageSupportDarkTheme() {
         WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
diff --git a/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
new file mode 100644
index 0000000..6c38760
--- /dev/null
+++ b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.webkit.internal.WebViewGlueCommunicator;
+
+import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
+
+import java.io.FileNotFoundException;
+
+/**
+ * TODO(1353048): Un-hide this after finishing the feature.
+ *
+ * @hide
+ * This should be added to the manifest in order to enable dragging images out.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DropDataContentProvider extends ContentProvider {
+    DropDataContentProviderBoundaryInterface mImpl;
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Nullable
+    @Override
+    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
+            throws FileNotFoundException {
+        return getDropImpl().openFile(this, uri);
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+            @Nullable String selection, @Nullable String[] selectionArgs,
+            @Nullable String sortOrder) {
+        return getDropImpl().query(uri, projection, selection, selectionArgs, sortOrder);
+    }
+
+    @Nullable
+    @Override
+    public String getType(@NonNull Uri uri) {
+        return getDropImpl().getType(uri);
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
+        throw new UnsupportedOperationException("Insert method is not supported.");
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        throw new UnsupportedOperationException("delete method is not supported.");
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s,
+            @Nullable String[] strings) {
+        throw new UnsupportedOperationException("update method is not supported.");
+    }
+
+    @Nullable
+    @Override
+    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
+        return getDropImpl().call(method, arg, extras);
+    }
+
+    private DropDataContentProviderBoundaryInterface getDropImpl() {
+        if (mImpl == null) {
+            mImpl = WebViewGlueCommunicator.getFactory().getDropDataProvider();
+            mImpl.onCreate();
+        }
+        return mImpl;
+    }
+}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
index 692fec2..ae5ac6a 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
@@ -184,13 +184,14 @@
      * <p>
      * Any origin <em>not</em> on this allow-list may not receive the header, depending on the
      * current installed WebView provider.
+     * <p>
+     * The format of the strings in the allow-list follows the origin rules of
+     * {@link WebViewCompat#addWebMessageListener(WebView, String, Set, WebViewCompat.WebMessageListener)}.
      *
      * @return The configured set of allow-listed origins.
      * @see #setRequestedWithHeaderOriginAllowList(Set)
      * @see WebSettingsCompat#getRequestedWithHeaderOriginAllowList(WebSettings)
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @RequiresFeature(name = WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     @NonNull
@@ -209,15 +210,13 @@
      * the deprecated header, but it should not be used to identify the webview to first-party
      * servers under the control of the app developer.
      * <p>
-     * The format of the allow-list follows the origin rules of
+     * The format of the strings in the allow-list follows the origin rules of
      * {@link WebViewCompat#addWebMessageListener(WebView, String, Set, WebViewCompat.WebMessageListener)}.
      *
      * @param allowList Set of origins to allow-list.
      * @see WebSettingsCompat#setRequestedWithHeaderOriginAllowList(WebSettings, Set)
      * @throws IllegalArgumentException if the allow-list contains a malformed origin.
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @RequiresFeature(name = WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public abstract void setRequestedWithHeaderOriginAllowList(@NonNull Set<String> allowList);
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 1cb77cb..b612e86 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -695,10 +695,6 @@
         }
     }
 
-    private static WebSettingsAdapter getAdapter(WebSettings settings) {
-        return WebViewGlueCommunicator.getCompatConverter().convertSettings(settings);
-    }
-
     /**
      * Get the currently configured allow-list of origins, which is guaranteed to receive the
      * {@code X-Requested-With} HTTP header on requests from the {@link WebView} owning the passed
@@ -706,12 +702,14 @@
      * <p>
      * Any origin <em>not</em> on this allow-list may not receive the header, depending on the
      * current installed WebView provider.
+     * <p>
+     * The format of the strings in the allow-list follows the origin rules of
+     * {@link WebViewCompat#addWebMessageListener(WebView, String, Set, WebViewCompat.WebMessageListener)}.
      *
+     * @param settings Settings retrieved from {@link WebView#getSettings()}.
      * @return The configured set of allow-listed origins.
      * @see #setRequestedWithHeaderOriginAllowList(WebSettings, Set)
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @RequiresFeature(name = WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     @NonNull
@@ -738,14 +736,13 @@
      * the deprecated header, but it should not be used to identify the webview to first-party
      * servers under the control of the app developer.
      * <p>
-     * The format of the allow-list follows the origin rules of
+     * The format of the strings in the allow-list follows the origin rules of
      * {@link WebViewCompat#addWebMessageListener(WebView, String, Set, WebViewCompat.WebMessageListener)}.
      *
+     * @param settings Settings retrieved from {@link WebView#getSettings()}.
      * @param allowList Set of origins to allow-list.
      * @throws IllegalArgumentException if the allow-list contains a malformed origin.
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @RequiresFeature(name = WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
     public static void setRequestedWithHeaderOriginAllowList(@NonNull WebSettings settings,
@@ -758,5 +755,9 @@
             throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
     }
+
+    private static WebSettingsAdapter getAdapter(WebSettings settings) {
+        return WebViewGlueCommunicator.getCompatConverter().convertSettings(settings);
+    }
 }
 
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java b/webkit/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
index 1ef162a..508ff54 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.NonNull;
 
+import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
 import org.chromium.support_lib_boundary.ProxyControllerBoundaryInterface;
 import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
 import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
@@ -80,4 +81,10 @@
     public ProxyControllerBoundaryInterface getProxyController() {
         throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
     }
+
+    @NonNull
+    @Override
+    public DropDataContentProviderBoundaryInterface getDropDataProvider() {
+        throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+    }
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java b/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
index f406876..6007a34 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
@@ -27,5 +27,5 @@
     }
 
     // ProcessGlobalConfig#setDataDirectorySuffix(String)
-    public static final String SET_DATA_DIRECTORY_SUFFIX =  "SET_DATA_DIRECTORY_SUFFIX";
+    public static final String SET_DATA_DIRECTORY_SUFFIX =  "SET_DATA_DIRECTORY_SUFFIX:DEV";
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
index 9eb736b..4c85e7d 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
@@ -21,6 +21,7 @@
 
 import androidx.annotation.NonNull;
 
+import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
 import org.chromium.support_lib_boundary.ProxyControllerBoundaryInterface;
 import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
 import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
@@ -80,4 +81,10 @@
      */
     @NonNull
     ProxyControllerBoundaryInterface getProxyController();
+
+    /**
+     * Fetch the boundary interface representing image drag drop implementation.
+     */
+    @NonNull
+    DropDataContentProviderBoundaryInterface getDropDataProvider();
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
index b30a66b..3843c26 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.NonNull;
 
+import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
 import org.chromium.support_lib_boundary.ProxyControllerBoundaryInterface;
 import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
 import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
@@ -117,4 +118,15 @@
         return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
                 ProxyControllerBoundaryInterface.class, mImpl.getProxyController());
     }
+
+    /**
+     * Adapter method for fetching the support library class representing Drag drop
+     * Image implementation.
+     */
+    @NonNull
+    @Override
+    public DropDataContentProviderBoundaryInterface getDropDataProvider() {
+        return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+                DropDataContentProviderBoundaryInterface.class, mImpl.getDropDataProvider());
+    }
 }
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index e3dc726..45ead03 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -82,7 +82,6 @@
   }
 
   public final class SplitPairRule extends androidx.window.embedding.SplitRule {
-    ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
     method public boolean getClearTop();
     method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
     method public int getFinishPrimaryWithSecondary();
@@ -104,7 +103,6 @@
   }
 
   public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
-    ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
     method public int getFinishPrimaryWithPlaceholder();
     method public android.content.Intent getPlaceholderIntent();
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 7c76c1d..00f20da 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -89,7 +89,6 @@
   }
 
   public final class SplitPairRule extends androidx.window.embedding.SplitRule {
-    ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
     method public boolean getClearTop();
     method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
     method public int getFinishPrimaryWithSecondary();
@@ -111,7 +110,6 @@
   }
 
   public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
-    ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
     method public int getFinishPrimaryWithPlaceholder();
     method public android.content.Intent getPlaceholderIntent();
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index e3dc726..45ead03 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -82,7 +82,6 @@
   }
 
   public final class SplitPairRule extends androidx.window.embedding.SplitRule {
-    ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
     method public boolean getClearTop();
     method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
     method public int getFinishPrimaryWithSecondary();
@@ -104,7 +103,6 @@
   }
 
   public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
-    ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
     method public int getFinishPrimaryWithPlaceholder();
     method public android.content.Intent getPlaceholderIntent();
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
index 54b6ebd..f2b811c 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
@@ -26,9 +26,10 @@
 import androidx.window.embedding.MatcherUtils.sMatchersTag
 
 /**
- * Filter for [ActivityRule] that checks for component name match. Allows a wildcard symbol in the
- * end or instead of the package name, and a wildcard symbol in the end or instead of the class
- * name.
+ * Filter for [ActivityRule] and [SplitPlaceholderRule] that checks for component name match when
+ * a new activity is started. If the filter matches the started activity [Intent], the activity will
+ * then apply the rule based on the match result. This filter allows a wildcard symbol in the end or
+ * instead of the package name, and a wildcard symbol in the end or instead of the class name.
  */
 class ActivityFilter internal constructor(
     /**
@@ -89,6 +90,7 @@
         ) { "Wildcard in class name is only allowed at the end." }
     }
 
+    /** Returns `true` if the [ActivityFilter] matches this [intent]. */
     fun matchesIntent(intent: Intent): Boolean {
         val match = if (!isIntentMatching(intent, activityComponentInfo)) {
                 false
@@ -105,6 +107,7 @@
         return match
     }
 
+    /** Returns `true` if the [ActivityFilter] matches this [activity]. */
     fun matchesActivity(activity: Activity): Boolean {
         val match =
             isActivityOrIntentMatching(activity, activityComponentInfo) &&
@@ -119,10 +122,14 @@
         return match
     }
 
+    /** Returns `true` if the [ActivityFilter] matches the [Class.getName]. */
     fun <T : Activity> matchesClassName(clazz: Class<T>): Boolean {
         return activityComponentInfo.className == clazz.name
     }
 
+    /**
+     * Returns `true` if the [ActivityFilter] matches the [Class.getName] or includes wildcard (`*`)
+     */
     fun <T : Activity> matchesClassNameOrWildCard(clazz: Class<T>?): Boolean {
         return clazz?.let(::matchesClassName) ?: activityComponentInfo.className.contains("*")
     }
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
index 95e1625..daf4a46 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
@@ -26,8 +26,10 @@
 import androidx.window.embedding.MatcherUtils.sMatchersTag
 
 /**
- * Filter used to find if a pair of activities should be put in a split. Applied to the base /
- * primary activity and an intent starting a secondary activity.
+ * Filter for [SplitPairRule] and used to find if a pair of activities should be put in a split.
+ * It is used when a new activity is started from the primary activity.
+ * If the filter matches the primary [Activity.getComponentName] and the new started activity
+ * [Intent], it matches the [SplitPairRule] that holds this filter.
  */
 class SplitPairFilter(
     /**
@@ -59,6 +61,9 @@
     private val secondaryActivityInfo: ActivityComponentInfo
         get() = ActivityComponentInfo(secondaryActivityName)
 
+    /**
+     * Returns `true` if this [SplitPairFilter] matches [primaryActivity] and [secondaryActivity].
+     */
     fun matchesActivityPair(primaryActivity: Activity, secondaryActivity: Activity): Boolean {
         // Check if the activity component names match
         var match = areComponentsMatching(primaryActivity.componentName, primaryActivityName) &&
@@ -79,6 +84,10 @@
         return match
     }
 
+    /**
+     * Returns `true` if this [SplitPairFilter] matches the [primaryActivity] and the
+     * [secondaryActivityIntent]
+     */
     fun matchesActivityIntentPair(
         primaryActivity: Activity,
         secondaryActivityIntent: Intent
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
index de62416..24c497f 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -61,12 +61,7 @@
      */
     val clearTop: Boolean
 
-    // TODO(b/229656253): Reduce visibility to remove from public API.
-    @Deprecated(
-        message = "Visibility of the constructor will be reduced.",
-        replaceWith = ReplaceWith("androidx.window.embedding.SplitPairRule.Builder")
-    )
-    constructor(
+    internal constructor(
         filters: Set<SplitPairFilter>,
         @SplitFinishBehavior finishPrimaryWithSecondary: Int = FINISH_NEVER,
         @SplitFinishBehavior finishSecondaryWithPrimary: Int = FINISH_ALWAYS,
@@ -144,7 +139,6 @@
         fun setLayoutDir(@LayoutDir layoutDir: Int): Builder =
             apply { this.layoutDir = layoutDir }
 
-        @Suppress("DEPRECATION")
         fun build() = SplitPairRule(filters, finishPrimaryWithSecondary, finishSecondaryWithPrimary,
             clearTop, minWidth, minSmallestWidth, splitRatio, layoutDir)
     }
@@ -157,17 +151,13 @@
         val newSet = mutableSetOf<SplitPairFilter>()
         newSet.addAll(filters)
         newSet.add(filter)
-        @Suppress("DEPRECATION")
-        return SplitPairRule(
-            newSet.toSet(),
-            finishPrimaryWithSecondary,
-            finishSecondaryWithPrimary,
-            clearTop,
-            minWidth,
-            minSmallestWidth,
-            splitRatio,
-            layoutDirection
-        )
+        return Builder(newSet.toSet(), minWidth, minSmallestWidth)
+            .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
+            .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
+            .setClearTop(clearTop)
+            .setSplitRatio(splitRatio)
+            .setLayoutDir(layoutDirection)
+            .build()
     }
 
     override fun equals(other: Any?): Boolean {
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
index ae4b432..5c2c6ee 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
@@ -63,12 +63,7 @@
     @SplitPlaceholderFinishBehavior
     val finishPrimaryWithPlaceholder: Int
 
-    // TODO(b/229656253): Reduce visibility to remove from public API.
-    @Deprecated(
-        message = "Visibility of the constructor will be reduced.",
-        replaceWith = ReplaceWith("androidx.window.embedding.SplitPlaceholderRule.Builder")
-    )
-    constructor(
+    internal constructor(
         filters: Set<ActivityFilter>,
         placeholderIntent: Intent,
         isSticky: Boolean,
@@ -142,7 +137,6 @@
         fun setLayoutDir(@LayoutDir layoutDir: Int): Builder =
             apply { this.layoutDir = layoutDir }
 
-        @Suppress("DEPRECATION")
         fun build() = SplitPlaceholderRule(filters, placeholderIntent, isSticky,
             finishPrimaryWithPlaceholder, minWidth, minSmallestWidth, splitRatio, layoutDir)
     }
@@ -155,17 +149,12 @@
         val newSet = mutableSetOf<ActivityFilter>()
         newSet.addAll(filters)
         newSet.add(filter)
-        @Suppress("DEPRECATION")
-        return SplitPlaceholderRule(
-            newSet.toSet(),
-            placeholderIntent,
-            isSticky,
-            finishPrimaryWithPlaceholder,
-            minWidth,
-            minSmallestWidth,
-            splitRatio,
-            layoutDirection
-        )
+        return Builder(newSet.toSet(), placeholderIntent, minWidth, minSmallestWidth)
+            .setSticky(isSticky)
+            .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
+            .setSplitRatio(splitRatio)
+            .setLayoutDir(layoutDirection)
+            .build()
     }
 
     override fun equals(other: Any?): Boolean {
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt b/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
index b9b8ea1..5cfea09 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
@@ -156,17 +156,13 @@
             clearTop =
                 getBoolean(R.styleable.SplitPairRule_clearTop, false)
         }
-        @Suppress("DEPRECATION")
-        return SplitPairRule(
-            emptySet(),
-            finishPrimaryWithSecondary,
-            finishSecondaryWithPrimary,
-            clearTop,
-            minWidth,
-            minSmallestWidth,
-            ratio,
-            layoutDir
-        )
+        return SplitPairRule.Builder(emptySet(), minWidth, minSmallestWidth)
+            .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
+            .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
+            .setClearTop(clearTop)
+            .setSplitRatio(ratio)
+            .setLayoutDir(layoutDir)
+            .build()
     }
 
     private fun parseSplitPlaceholderRule(
@@ -219,17 +215,16 @@
             placeholderActivityIntentName
         )
 
-        @Suppress("DEPRECATION")
-        return SplitPlaceholderRule(
+        return SplitPlaceholderRule.Builder(
             emptySet(),
             Intent().setComponent(placeholderActivityClassName),
-            stickyPlaceholder,
-            finishPrimaryWithPlaceholder,
             minWidth,
-            minSmallestWidth,
-            ratio,
-            layoutDir
-        )
+            minSmallestWidth
+        ).setSticky(stickyPlaceholder)
+            .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
+            .setSplitRatio(ratio)
+            .setLayoutDir(layoutDir)
+            .build()
     }
 
     private fun parseSplitPairFilter(